From bf6000ef3d38bf663d4fc2d69a220d5a64889e78 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Mon, 23 Mar 2026 23:50:28 +0100 Subject: [PATCH 001/115] [substitutions] substitutions pass and !include redesign (package refactor part 2b) (#14918) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/components/packages/__init__.py | 9 +- esphome/components/substitutions/__init__.py | 444 +++++++++++++----- esphome/components/substitutions/jinja.py | 94 +--- esphome/config.py | 30 +- esphome/yaml_util.py | 41 +- .../component_tests/packages/test_packages.py | 2 + .../substitutions/00-simple_var.approved.yaml | 17 + .../substitutions/00-simple_var.input.yaml | 10 + .../02-expressions.approved.yaml | 6 + .../substitutions/02-expressions.input.yaml | 6 + .../07-package_merging.approved.yaml | 46 ++ .../07-package_merging.input.yaml | 63 +++ ...-include_vars_without_substs.approved.yaml | 5 + .../09-include_vars_without_substs.input.yaml | 7 + tests/unit_tests/test_substitutions.py | 244 +++++++++- tests/unit_tests/test_yaml_util.py | 2 +- 16 files changed, 753 insertions(+), 273 deletions(-) create mode 100644 tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/07-package_merging.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.input.yaml diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 6d353ccf11..793cb946dd 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -226,7 +226,7 @@ def _process_remote_package(config: dict, skip_update: bool = False) -> dict: raise cv.Invalid( f"Current ESPHome Version is too old to use this package: {ESPHOME_VERSION} < {min_version}" ) - new_yaml = yaml_util.substitute_vars(new_yaml, vars) + new_yaml = yaml_util.add_context(new_yaml, vars or None) packages[f"{filename}{idx}"] = new_yaml except EsphomeError as e: raise cv.Invalid( @@ -296,6 +296,13 @@ def do_packages_pass(config: dict, skip_update: bool = False) -> dict: def process_package_callback(package_config: dict) -> dict: """This will be called for each package found in the config.""" + if isinstance(package_config, yaml_util.ConfigContext): + context_vars = package_config.vars + if CONF_PACKAGES in package_config or CONF_URL in package_config: + # Remote package definition: eagerly resolve before PACKAGE_SCHEMA validation. + from esphome.components.substitutions import substitute_context_vars + + substitute_context_vars(package_config, context_vars) package_config = PACKAGE_SCHEMA(package_config) if isinstance(package_config, str): return package_config # Jinja string, skip processing diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 7e15f714f7..ecee816ce9 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -1,31 +1,50 @@ +from collections import ChainMap import logging -from re import Match from typing import Any from esphome import core from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered import esphome.config_validation as cv from esphome.const import CONF_SUBSTITUTIONS, VALID_SUBSTITUTIONS_CHARACTERS -from esphome.yaml_util import ESPHomeDataBase, ESPLiteralValue, make_data_base +from esphome.types import ConfigType +from esphome.util import OrderedDict +from esphome.yaml_util import ( + ConfigContext, + ESPHomeDataBase, + ESPLiteralValue, + make_data_base, +) -from .jinja import Jinja, JinjaError, JinjaStr, has_jinja +from .jinja import Jinja, JinjaError, Missing, Resolver, UndefinedError, has_jinja CODEOWNERS = ["@esphome/core"] _LOGGER = logging.getLogger(__name__) +ContextVars = ChainMap[str, Any] +SubstitutionPath = list[int | str] +ErrList = list[tuple[UndefinedError, SubstitutionPath, Any]] +# Module-level instance is safe: context_vars is passed per-call, and context_trace +# is stack-saved/restored within expand(). Not thread-safe — only use from one thread. +jinja = Jinja() -def validate_substitution_key(value): + +def validate_substitution_key(value: Any) -> str: + """Validate and normalize a substitution key, stripping a leading ``$`` if present.""" value = cv.string(value) if not value: raise cv.Invalid("Substitution key must not be empty") if value[0] == "$": value = value[1:] + if not value: + raise cv.Invalid("Substitution key must not be empty") if value[0].isdigit(): raise cv.Invalid("First character in substitutions cannot be a digit.") for char in value: if char not in VALID_SUBSTITUTIONS_CHARACTERS: raise cv.Invalid( - f"Substitution must only consist of upper/lowercase characters, the underscore and numbers. The character '{char}' cannot be used" + f"Substitution must only consist of upper/lowercase characters," + f" the underscore and numbers." + f" The character '{char}' cannot be used" ) return value @@ -37,8 +56,8 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): - pass +async def to_code(config: ConfigType) -> None: + """No runtime code generation needed — substitutions are resolved at config time.""" def _restore_data_base(value: Any, orig_value: ESPHomeDataBase) -> ESPHomeDataBase: @@ -62,91 +81,122 @@ def _restore_data_base(value: Any, orig_value: ESPHomeDataBase) -> ESPHomeDataBa return value -def _expand_jinja( - value: str | JinjaStr, - orig_value: str | JinjaStr, - path, - jinja: Jinja, - ignore_missing: bool, -) -> Any: - if has_jinja(value): - # If the original value passed in to this function is a JinjaStr, it means it contains an unresolved - # Jinja expression from a previous pass. - if isinstance(orig_value, JinjaStr): - # Rebuild the JinjaStr in case it was lost while replacing substitutions. - value = JinjaStr(value, orig_value.upvalues) - try: - # Invoke the jinja engine to evaluate the expression. - value, err = jinja.expand(value) - if err is not None and not ignore_missing and "password" not in path: - _LOGGER.warning( - "Found '%s' (see %s) which looks like an expression," - " but could not resolve all the variables: %s", - value, - "->".join(str(x) for x in path), - err.message, - ) - except JinjaError as err: - raise cv.Invalid( - f"{err.error_name()} Error evaluating jinja expression '{value}': {str(err.parent())}." - f"\nEvaluation stack: (most recent evaluation last)\n{err.stack_trace_str()}" - f"\nRelevant context:\n{err.context_trace_str()}" - f"\nSee {'->'.join(str(x) for x in path)}", - path, - ) - # If the original, unexpanded string, contained document metadata (ESPHomeDatabase), - # assign this same document metadata to the resulting value. - if isinstance(orig_value, ESPHomeDataBase): - value = _restore_data_base(value, orig_value) +def _try_substitute(value: Any, context: ContextVars) -> Any: + """Substitute variables in value, returning the result or the original if unchanged.""" + result = _substitute_item(value, [], context, strict_undefined=True) + return result if result is not None else value - return value + +def _resolve_var(name: str, context_vars: ContextVars) -> Any: + """Look up a substitution variable, falling back to the resolver callback.""" + sub = context_vars.get(name, Missing) + if sub is Missing: + resolver = context_vars.get(Resolver) + if resolver: + sub = resolver(name) + return sub + + +def _handle_undefined( + err: UndefinedError, + path: SubstitutionPath, + value: Any, + strict_undefined: bool, + errors: ErrList | None, +) -> None: + """Handle an undefined variable. + + In strict mode, raises immediately. Otherwise, appends to the errors + list for deferred warning at the end of the substitution pass. + """ + if strict_undefined: + raise err + if errors is not None: + errors.append((err, path, value)) def _expand_substitutions( - substitutions: dict, value: str, path, jinja: Jinja, ignore_missing: bool + value: str, + path: SubstitutionPath, + context_vars: ContextVars, + strict_undefined: bool, + errors: ErrList | None, ) -> Any: + """Expand ``$var``, ``${var}``, and Jinja expressions in a string. + + Works in two phases: + + 1. **Simple substitution** — scan for ``$name`` / ``${name}`` tokens + and replace them with the value from *context_vars*. If the token + spans the entire string, return the raw value (preserving type). + 2. **Jinja evaluation** — if the result still contains Jinja syntax + (e.g. ``${a * b}``), render it through the Jinja engine with the + full *context_vars* as template variables. + + Returns the expanded value (may be a non-string type) or the + original *value* unchanged if there is nothing to substitute. + """ if "$" not in value: return value orig_value = value - i = 0 - while True: - m: Match[str] = cv.VARIABLE_PROG.search(value, i) - if not m: - # No more variable substitutions found. See if the remainder looks like a jinja template - value = _expand_jinja(value, orig_value, path, jinja, ignore_missing) - break - - i, j = m.span(0) + # Phase 1: Replace $var and ${var} references + search_pos = 0 + while (m := cv.VARIABLE_PROG.search(value, search_pos)) is not None: + match_start, match_end = m.span(0) name: str = m.group(1) if name.startswith("{") and name.endswith("}"): name = name[1:-1] - if name not in substitutions: - if not ignore_missing and "password" not in path: - _LOGGER.warning( - "Found '%s' (see %s) which looks like a substitution, but '%s' was " - "not declared", - orig_value, - "->".join(str(x) for x in path), - name, - ) - i = j + sub = _resolve_var(name, context_vars) + if sub is Missing: + _handle_undefined( + err=UndefinedError(f"'{name}' is undefined"), + path=path, + value=value, + strict_undefined=strict_undefined, + errors=errors, + ) + search_pos = match_end continue - sub: Any = substitutions[name] - - if i == 0 and j == len(value): - # The variable spans the whole expression, e.g., "${varName}". Return its resolved value directly - # to conserve its type. + if match_start == 0 and match_end == len(value): + # The variable spans the whole expression, e.g., "${varName}". + # Return its resolved value directly to conserve its type. value = sub break - tail = value[j:] - value = value[:i] + str(sub) - i = len(value) + tail = value[match_end:] + value = value[:match_start] + str(sub) + search_pos = len(value) value += tail + # Phase 2: Evaluate any remaining jinja expressions (e.g., "${a * b}") + if isinstance(value, str) and has_jinja(value): + try: + value = jinja.expand(value, context_vars) + except UndefinedError as err: + _handle_undefined( + err=err, + path=path, + value=value, + strict_undefined=strict_undefined, + errors=errors, + ) + except JinjaError as err: + raise cv.Invalid( + f"{err.error_name()} Error evaluating jinja expression" + f" '{value}': {str(err.parent())}." + f"\nEvaluation stack: (most recent evaluation last)" + f"\n{err.stack_trace_str()}" + f"\nRelevant context:\n{err.context_trace_str()}" + f"\nSee {'->'.join(str(x) for x in path)}", + path, + ) + else: + if isinstance(orig_value, ESPHomeDataBase): + value = _restore_data_base(value, orig_value) + # orig_value can also already be a lambda with esp_range info, and only # a plain string is sent in orig_value if isinstance(orig_value, ESPHomeDataBase): @@ -157,83 +207,221 @@ def _expand_substitutions( return value +def _push_context( + local_vars: dict[str, Any], + parent_context: ContextVars, + errors: ErrList | None = None, +) -> tuple[ContextVars, dict[str, Any]]: + """Resolve local_vars and layer them on top of parent_context. + + Returns ``(child_context, resolved_vars)`` where *child_context* is a + new :class:`ChainMap` whose front map is *resolved_vars* (an + :class:`OrderedDict` of successfully-resolved variables). + + Variables may reference each other (e.g. ``b: ${a + 1}``). + Dependencies are resolved recursively via a *resolver* callback + that Jinja invokes on cache-miss. If vars are already in + dependency order, the loop iterates exactly once per variable. + + The ChainMap stack used during resolution is:: + + resolver_context → resolved_vars → parent maps … + ↑ ↑ + holds Resolver filled as vars + callback are resolved + """ + # Vars still waiting to be resolved — popped one-by-one by resolve(). + unresolved_vars = local_vars.copy() + # Accumulates resolved values in dependency order; becomes the front + # map of the returned child context so later lookups find them first. + resolved_vars = OrderedDict() + # The context callees will search: resolved_vars (initially empty) + # shadowing whatever the parent already provides. + context_vars = parent_context.new_child(resolved_vars) + + # Vars that failed resolution (missing or circular references). + # Maps name → (original_value, cause_error) for deferred warnings. + unresolvables: dict[str, tuple[Any, UndefinedError]] = {} + + # One extra child layer so the Resolver callback lives in its own + # map and doesn't pollute resolved_vars. + resolver_context = context_vars.new_child() + + def resolve(key: str) -> Any: + """Resolve a variable, recursively resolving any dependencies it references.""" + value = unresolved_vars.pop(key, Missing) + if value is Missing: + return Missing + try: + value = _try_substitute(value, resolver_context) + except UndefinedError as err: + unresolvables[key] = (value, err) + return Missing + resolved_vars[key] = value + return value + + # Set up the resolver for use during substitution + resolver_context[Resolver] = resolve + + # Resolve all variables, recursively resolving dependencies as needed. + # Each call to resolve() resolves that variable and any variables it depends on. + while unresolved_vars: + resolve(next(iter(unresolved_vars))) + + for name, (value, cause) in unresolvables.items(): + resolved_vars[name] = value + if errors is not None: + _handle_undefined( + err=UndefinedError( + f"Could not resolve substitution variable '{name}': {cause}" + ), + path=["substitutions", name], + value=value, + strict_undefined=False, + errors=errors, + ) + + return context_vars, resolved_vars + + +def push_context( + config_node: Any, + parent_context: ContextVars, + errors: ErrList | None = None, +) -> ContextVars: + """Returns the context vars this config node must be evaluated with.""" + if isinstance(config_node, ConfigContext): + return _push_context(config_node.vars, parent_context, errors)[0] + + # This node does not define any vars itself, so just return parent context + return parent_context + + def _substitute_item( - substitutions: dict, item: Any, - path: list[int | str], - jinja: Jinja, - ignore_missing: bool, + path: SubstitutionPath, + parent_context: ContextVars, + strict_undefined: bool, + errors: ErrList | None = None, ) -> Any | None: - if isinstance(item, ESPLiteralValue): - return None # do not substitute inside literal blocks - if isinstance(item, list): - for i, it in enumerate(item): - sub = _substitute_item(substitutions, it, path + [i], jinja, ignore_missing) - if sub is not None: - item[i] = sub - elif isinstance(item, dict): - replace_keys = [] - for k, v in item.items(): - if path or k != CONF_SUBSTITUTIONS: - sub = _substitute_item( - substitutions, k, path + [k], jinja, ignore_missing - ) + """Recursively substitute variables in a config item. + + Walks dicts, lists, strings, Lambdas, Extend, and Remove nodes, + replacing variable references with values from context_vars. + Mutates containers in-place; returns a replacement value for + strings/scalars, or None if the item was unchanged. + """ + + def _walk(item: Any, path: SubstitutionPath, parent_ctx: ContextVars) -> Any | None: + if isinstance(item, ESPLiteralValue): + return None # do not substitute inside literal blocks + + ctx = push_context(item, parent_ctx, errors) + + if isinstance(item, list): + for idx, it in enumerate(item): + sub = _walk(it, path + [idx], ctx) if sub is not None: - replace_keys.append((k, sub)) - sub = _substitute_item(substitutions, v, path + [k], jinja, ignore_missing) - if sub is not None: - item[k] = sub - for old, new in replace_keys: - if str(new) == str(old): - item[new] = item[old] - else: - item[new] = merge_config(item.get(old), item.get(new)) - del item[old] - elif isinstance(item, str): - sub = _expand_substitutions(substitutions, item, path, jinja, ignore_missing) - if isinstance(sub, JinjaStr) or sub != item: - return sub - elif isinstance(item, (core.Lambda, Extend, Remove)): - sub = _expand_substitutions( - substitutions, item.value, path, jinja, ignore_missing + item[idx] = sub + elif isinstance(item, dict): + replace_keys: list[tuple[str, Any]] = [] + for k, v in item.items(): + if path or k != CONF_SUBSTITUTIONS: + sub = _walk(k, path + [k], ctx) + if sub is not None: + replace_keys.append((k, sub)) + sub = _walk(v, path + [k], ctx) + if sub is not None: + item[k] = sub + for old, new in replace_keys: + if str(new) == str(old): + item[new] = item[old] + else: + item[new] = merge_config(item.get(new), item.get(old)) + del item[old] + elif isinstance(item, str): + sub = _expand_substitutions(item, path, ctx, strict_undefined, errors) + if not isinstance(sub, str) or sub != item: + return sub + elif isinstance(item, (core.Lambda, Extend, Remove)) and item.value: + sub = _expand_substitutions(item.value, path, ctx, strict_undefined, errors) + if sub != item.value: + item.value = sub + return None + + return _walk(item, path, parent_context) + + +def substitute_context_vars(node: Any, context_vars: dict[str, Any]) -> None: + """Eagerly substitute context vars into a config node in-place. + + Undefined variables are silently ignored — this is used before + the main substitution pass when not all variables are visible yet. + """ + _substitute_item(node, [], ContextVars(context_vars), strict_undefined=False) + + +def _warn_unresolved_variables(errors: ErrList) -> None: + """Log warnings for unresolved substitution variables, skipping password fields.""" + for err, path, expression in errors: + if "password" in path: + continue + location: str = "->".join(str(x) for x in path) + if isinstance(expression, ESPHomeDataBase) and expression.esp_range is not None: + location += f" in {str(expression.esp_range.start_mark)}" + + _LOGGER.warning( + "The string '%s' looks like an expression," + " but could not resolve all the variables: %s (see %s)", + expression, + err.message, + location, ) - if sub != item: - item.value = sub - return None def do_substitution_pass( - config: dict, command_line_substitutions: dict, ignore_missing: bool = False -) -> None: - if CONF_SUBSTITUTIONS not in config and not command_line_substitutions: - return + config: OrderedDict, command_line_substitutions: dict[str, Any] | None = None +) -> OrderedDict: + """Run the substitution pass over the entire config. - # Merge substitutions in config, overriding with substitutions coming from command line: + Extracts the ``substitutions:`` block, merges in any command-line + overrides, resolves inter-variable dependencies, then walks the + config tree replacing all ``$var`` / ``${expr}`` references. + Returns the (mutated) config dict with resolved substitutions + restored at the front. + """ + # Extract substitutions from config, overriding with substitutions coming from command line: # Use merge_dicts_ordered to preserve OrderedDict type for move_to_end() - substitutions = merge_dicts_ordered( - config.get(CONF_SUBSTITUTIONS, {}), command_line_substitutions or {} - ) - with cv.prepend_path("substitutions"): + substitutions = config.pop(CONF_SUBSTITUTIONS, {}) + with cv.prepend_path(CONF_SUBSTITUTIONS): if not isinstance(substitutions, dict): raise cv.Invalid( f"Substitutions must be a key to value mapping, got {type(substitutions)}" ) + substitutions = merge_dicts_ordered( + substitutions, command_line_substitutions or {} + ) - replace_keys = [] - for key, value in substitutions.items(): + replace_keys: list[tuple[str, str]] = [] + for key in substitutions: with cv.prepend_path(key): sub = validate_substitution_key(key) if sub != key: replace_keys.append((key, sub)) - substitutions[key] = value for old, new in replace_keys: substitutions[new] = substitutions[old] del substitutions[old] - config[CONF_SUBSTITUTIONS] = substitutions - # Move substitutions to the first place to replace substitutions in them correctly - config.move_to_end(CONF_SUBSTITUTIONS, False) + errors: ErrList = [] # Collect undefined errors during substitution + parent_context, substitutions = _push_context(substitutions, ContextVars(), errors) - # Create a Jinja environment that will consider substitutions in scope: - jinja = Jinja(substitutions) - _substitute_item(substitutions, config, [], jinja, ignore_missing) + _substitute_item(config, [], parent_context, False, errors) + + if errors: + _warn_unresolved_variables(errors) + + # Restore substitutions to front of dict for readability + if substitutions: + config[CONF_SUBSTITUTIONS] = substitutions + config.move_to_end(CONF_SUBSTITUTIONS, last=False) + return config diff --git a/esphome/components/substitutions/jinja.py b/esphome/components/substitutions/jinja.py index fb9f843da2..37e9fa4d2d 100644 --- a/esphome/components/substitutions/jinja.py +++ b/esphome/components/substitutions/jinja.py @@ -1,7 +1,6 @@ from ast import literal_eval -from collections.abc import Iterator +from collections.abc import Iterator, Mapping from itertools import chain, islice -import logging import math import re from types import GeneratorType @@ -9,16 +8,17 @@ from typing import Any import jinja2 as jinja from jinja2.nativetypes import NativeCodeGenerator, NativeTemplate - -from esphome.yaml_util import ESPLiteralValue +from jinja2.runtime import missing as Missing TemplateError = jinja.TemplateError TemplateSyntaxError = jinja.TemplateSyntaxError TemplateRuntimeError = jinja.TemplateRuntimeError UndefinedError = jinja.UndefinedError Undefined = jinja.Undefined +# Sentinel key for resolver callback in ContextVars. +# Dots are invalid in substitution names so this can never collide with user keys. +Resolver = ".resolver" -_LOGGER = logging.getLogger(__name__) DETECT_JINJA = r"(\$\{)" detect_jinja_re = re.compile( @@ -52,33 +52,6 @@ SAFE_GLOBALS = { } -class JinjaStr(str): - """ - Wraps a string containing an unresolved Jinja expression, - storing the variables visible to it when it failed to resolve. - For example, an expression inside a package, `${ A * B }` may fail - to resolve at package parsing time if `A` is a local package var - but `B` is a substitution defined in the root yaml. - Therefore, we store the value of `A` as an upvalue bound - to the original string so we may be able to resolve `${ A * B }` - later in the main substitutions pass. - """ - - Undefined = object() - - def __new__(cls, value: str, upvalues=None): - if isinstance(value, JinjaStr): - base = str(value) - merged = {**value.upvalues, **(upvalues or {})} - else: - base = value - merged = dict(upvalues or {}) - obj = super().__new__(cls, base) - obj.upvalues = merged - obj.result = JinjaStr.Undefined - return obj - - class JinjaError(Exception): def __init__(self, context_trace: dict, expr: str): self.context_trace = context_trace @@ -106,9 +79,13 @@ class JinjaError(Exception): class TrackerContext(jinja.runtime.Context): def resolve_or_missing(self, key): val = super().resolve_or_missing(key) - if isinstance(val, JinjaStr): - self.environment.context_trace[key] = val - val, _ = self.environment.expand(val) + if val is Missing: + # Variable not in the template context — check if a resolver callback + # was registered (by _push_context) to lazily resolve dependencies + # between substitution variables in the same block. + resolver = super().resolve_or_missing(Resolver) + if resolver is not Missing: + val = resolver(key) self.environment.context_trace[key] = val return val @@ -160,15 +137,13 @@ def _concat_nodes_override(values: Iterator[Any]) -> Any: class Jinja(jinja.Environment): - """ - Wraps a Jinja environment - """ + """Jinja environment configured for ESPHome substitution expressions.""" # jinja environment customization overrides code_generator_class = NativeCodeGenerator concat = staticmethod(_concat_nodes_override) - def __init__(self, context_vars: dict): + def __init__(self) -> None: super().__init__( trim_blocks=True, lstrip_blocks=True, @@ -183,49 +158,25 @@ class Jinja(jinja.Environment): self.context_class = TrackerContext self.add_extension("jinja2.ext.do") self.context_trace = {} - self.context_vars = {**context_vars} - for k, v in self.context_vars.items(): - if isinstance(v, ESPLiteralValue): - continue - if isinstance(v, str) and not isinstance(v, JinjaStr) and has_jinja(v): - self.context_vars[k] = JinjaStr(v, self.context_vars) - self.globals = { - **self.globals, - **self.context_vars, - **SAFE_GLOBALS, - } + self.globals = {**self.globals, **SAFE_GLOBALS} - def expand(self, content_str: str | JinjaStr) -> Any: + def expand(self, content_str: str, context_vars: Mapping[str, Any]) -> Any: """ Renders a string that may contain Jinja expressions or statements Returns the resulting value if all variables and expressions could be resolved. - Otherwise, it returns a tagged (JinjaStr) string that captures variables - in scope (upvalues), like a closure for later evaluation. """ result = None - override_vars = {} - if isinstance(content_str, JinjaStr): - if content_str.result is not JinjaStr.Undefined: - return content_str.result, None - # If `value` is already a JinjaStr, it means we are trying to evaluate it again - # in a parent pass. - # Hopefully, all required variables are visible now. - override_vars = content_str.upvalues old_trace = self.context_trace self.context_trace = {} try: template = self.from_string(content_str) - result = template.render(override_vars) + result = template.render(context_vars) if isinstance(result, Undefined): - print("" + result) # force a UndefinedError exception - except (TemplateSyntaxError, UndefinedError) as err: - # `content_str` contains a Jinja expression that refers to a variable that is undefined - # in this scope. Perhaps it refers to a root substitution that is not visible yet. - # Therefore, return `content_str` as a JinjaStr, which contains the variables - # that are actually visible to it at this point to postpone evaluation. - return JinjaStr(content_str, {**self.context_vars, **override_vars}), err + str(result) # force a UndefinedError exception + except UndefinedError as err: + raise err except JinjaError as err: err.context_trace = {**self.context_trace, **err.context_trace} err.eval_stack.append(content_str) @@ -242,10 +193,7 @@ class Jinja(jinja.Environment): finally: self.context_trace = old_trace - if isinstance(content_str, JinjaStr): - content_str.result = result - - return result, None + return result class JinjaTemplate(NativeTemplate): diff --git a/esphome/config.py b/esphome/config.py index 6f6ad4886b..b80aaf3700 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -12,7 +12,8 @@ from typing import Any import voluptuous as vol from esphome import core, loader, pins, yaml_util -from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered +from esphome.components.substitutions import do_substitution_pass +from esphome.config_helpers import Extend, Remove, merge_config import esphome.config_validation as cv from esphome.const import ( CONF_ESPHOME, @@ -974,7 +975,7 @@ class PinUseValidationCheck(ConfigValidationStep): def validate_config( config: dict[str, Any], - command_line_substitutions: dict[str, Any], + command_line_substitutions: dict[str, Any] | None, skip_external_update: bool = False, ) -> Config: result = Config() @@ -994,21 +995,15 @@ def validate_config( result.add_error(err) return result - CORE.raw_config = config - # 1. Load substitutions if CONF_SUBSTITUTIONS in config or command_line_substitutions: - from esphome.components import substitutions - - result[CONF_SUBSTITUTIONS] = merge_dicts_ordered( - config.get(CONF_SUBSTITUTIONS) or {}, command_line_substitutions - ) result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS) - try: - substitutions.do_substitution_pass(config, command_line_substitutions) - except vol.Invalid as err: - result.add_error(err) - return result + try: + config = do_substitution_pass(config, command_line_substitutions) + except vol.Invalid as err: + CORE.raw_config = config + result.add_error(err) + return result # 1.1. Merge packages if CONF_PACKAGES in config: @@ -1016,6 +1011,9 @@ def validate_config( config = merge_packages(config) + # Remove substitutions from config during validation to prevent + # re-substitution. Re-added to result at the end of this function. + substitutions = config.pop(CONF_SUBSTITUTIONS, None) CORE.raw_config = config # 1.2. Resolve !extend and !remove and check for REPLACEME @@ -1089,6 +1087,10 @@ def validate_config( result.run_validation_steps() + if substitutions is not None: + result[CONF_SUBSTITUTIONS] = substitutions + result.move_to_end(CONF_SUBSTITUTIONS, last=False) + return result diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index d0eab4e44e..e001316a22 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -325,9 +325,7 @@ class ESPHomeLoaderMixin: return val @_add_data_ref - def construct_include( - self, node: yaml.Node - ) -> dict[str, Any] | OrderedDict[str, Any]: + def construct_include(self, node: yaml.Node) -> Any: from esphome.const import CONF_VARS def extract_file_vars(node): @@ -344,9 +342,7 @@ class ESPHomeLoaderMixin: file, vars = node.value, None result = self.yaml_loader(self._rel_path(file)) - if not vars: - vars = {} - return substitute_vars(result, vars) + return add_context(result, vars) @_add_data_ref def construct_include_dir_list(self, node: yaml.Node) -> list[dict[str, Any]]: @@ -495,39 +491,6 @@ def parse_yaml( ) -def substitute_vars(config, vars): - from esphome.components import substitutions - from esphome.const import CONF_SUBSTITUTIONS - - org_subs = None - result = config - if not isinstance(config, dict): - # when the included yaml contains a list or a scalar - # wrap it into an OrderedDict because do_substitution_pass expects it - result = OrderedDict([("yaml", config)]) - elif CONF_SUBSTITUTIONS in result: - org_subs = result.pop(CONF_SUBSTITUTIONS) - - defaults = {} - if CONF_DEFAULTS in result: - defaults = result.pop(CONF_DEFAULTS) - - result[CONF_SUBSTITUTIONS] = vars - for k, v in defaults.items(): - if k not in result[CONF_SUBSTITUTIONS]: - result[CONF_SUBSTITUTIONS][k] = v - - # Ignore missing vars that refer to the top level substitutions - substitutions.do_substitution_pass(result, None, ignore_missing=True) - result.pop(CONF_SUBSTITUTIONS) - - if not isinstance(config, dict): - result = result["yaml"] # unwrap the result - elif org_subs: - result[CONF_SUBSTITUTIONS] = org_subs - return result - - def _load_yaml_internal_with_type( loader_type: type[ESPHomeLoader] | type[ESPHomePurePythonLoader], fname: Path, diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index 22fb2c4e32..60dc0dccda 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch import pytest from esphome.components.packages import CONFIG_SCHEMA, do_packages_pass, merge_packages +from esphome.components.substitutions import do_substitution_pass import esphome.config as config_module from esphome.config import resolve_extend_remove from esphome.config_helpers import Extend, Remove @@ -71,6 +72,7 @@ def fixture_basic_esphome(): def packages_pass(config): """Wrapper around packages_pass that also resolves Extend and Remove.""" config = do_packages_pass(config) + config = do_substitution_pass(config) config = merge_packages(config) resolve_extend_remove(config) return config diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml index 9ed9b99c49..87f0e3fa21 100644 --- a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml @@ -38,3 +38,20 @@ test_list: - '{ 79, 82 }' - a: 15 should be 15, overridden from command line b: 20 should stay as 20, not overridden + - aa: + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + bb: + - 7 + - 8 + - 9 + - aa: + x: 1 + y: 3 + z: 4 + bb: + w: 5 diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml index 64701c03dd..d70372f280 100644 --- a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml @@ -44,3 +44,13 @@ test_list: - '{ ${position.x}, ${position.y} }' - a: ${a} should be 15, overridden from command line b: ${b} should stay as 20, not overridden + + # Test merging lists when substituted keys resolve to an existing key + - ${ "aa" }: [1, 2, 3] + ${ "a" + "a" }: [4, 5, 6] + ${ "bb" }: [7, 8, 9] + + # Test merging dicts when substituted keys resolve to an existing key + - ${ "aa" }: {"x": 1, "y": 2} + ${ "a" + "a" }: {"y": 3, "z": 4} + ${ "bb" }: {"w": 5} diff --git a/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml b/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml index 1a51fc44cf..b8c76fbf52 100644 --- a/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml @@ -9,6 +9,11 @@ substitutions: numberOne: 1 var1: 79 double_width: 14 + double_height: 16 + y: ${x} + x: ${y} + b: 79 + c: 80 test_list: - The area is 56 - 56 @@ -27,3 +32,4 @@ test_list: - chr(97) = a - len([1,2,3]) = 3 - width = 7, double_width = 14 + - a = ${a} diff --git a/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml b/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml index 4612f581b5..9593867f49 100644 --- a/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml @@ -1,4 +1,7 @@ substitutions: + y: ${x} # Circular reference, expect to pass unresolved. + x: ${y} # Circular reference, expect to pass unresolved. + double_height: ${height * 2} width: 7 height: 8 enabled: true @@ -9,6 +12,8 @@ substitutions: numberOne: 1 var1: 79 double_width: ${width * 2} + c: ${b+1} + b: ${undefined_variable | default(79) } test_list: - "The area is ${width * height}" @@ -25,3 +30,4 @@ test_list: - chr(97) = ${ chr(97) } - len([1,2,3]) = ${ len([1,2,3]) } - width = ${width}, double_width = ${double_width} + - a = ${a} diff --git a/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml b/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml new file mode 100644 index 0000000000..867889b7bc --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml @@ -0,0 +1,46 @@ +fancy_component: &id001 + - id: component9 + value: 9 +some_component: + - id: component1 + value: 1 + - id: component2 + value: 2 + - id: component3 + value: 3 + - id: component4 + value: 4 + - id: component5 + value: 79 + power: 200 + - id: component6 + value: 6 + - id: component7 + value: 7 +switch: &id002 + - platform: gpio + id: switch1 + pin: 12 + - platform: gpio + id: switch2 + pin: 13 +display: + - platform: ili9xxx + dimensions: + width: 100 + height: 480 +substitutions: + extended_component: component5 + package_options: + alternative_package: + alternative_component: + - id: component8 + value: 8 + fancy_package: + substitutions: + fancy_subst: 42 + fancy_component: *id001 + pin: 12 + some_switches: *id002 + package_selection: fancy_package + fancy_subst: 42 diff --git a/tests/unit_tests/fixtures/substitutions/07-package_merging.input.yaml b/tests/unit_tests/fixtures/substitutions/07-package_merging.input.yaml new file mode 100644 index 0000000000..cc7b841aba --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/07-package_merging.input.yaml @@ -0,0 +1,63 @@ +substitutions: + package_options: + alternative_package: + alternative_component: + - id: component8 + value: 8 + fancy_package: + substitutions: + fancy_subst: 42 + fancy_component: + - id: component9 + value: 9 + + pin: 12 + some_switches: + - platform: gpio + id: switch1 + pin: ${pin} + - platform: gpio + id: switch2 + pin: ${pin+1} + + package_selection: fancy_package + +packages: + - ${ package_options[package_selection] } + - some_component: + - id: component1 + value: 1 + - some_component: + - id: component2 + value: 2 + - switch: ${ some_switches } + - packages: + package_with_defaults: !include + file: display.yaml + vars: + native_width: 100 + high_dpi: false + my_package: + packages: + - packages: + special_package: + substitutions: + extended_component: component5 + some_component: + - id: component3 + value: 3 + some_component: + - id: component4 + value: 4 + - id: !extend ${ extended_component } + power: 200 + value: 79 + some_component: + - id: component5 + value: 5 + +some_component: + - id: component6 + value: 6 + - id: component7 + value: 7 diff --git a/tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.approved.yaml b/tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.approved.yaml new file mode 100644 index 0000000000..4abaf4471d --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.approved.yaml @@ -0,0 +1,5 @@ +values: + - var1: $var1 + - a: 10 + - b: B-default + - c: The value of C is 79 diff --git a/tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.input.yaml b/tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.input.yaml new file mode 100644 index 0000000000..91eb0e9a3f --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.input.yaml @@ -0,0 +1,7 @@ +# Test that include_vars with vars works even when there are no substitutions key defined. +packages: + - !include + file: inc1.yaml + vars: + a: 10 + c: 79 diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index 1d8cb7631d..db46a27dfb 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -10,9 +10,10 @@ from esphome import config as config_module, yaml_util from esphome.components import substitutions from esphome.components.packages import do_packages_pass, merge_packages from esphome.config import resolve_extend_remove -from esphome.config_helpers import merge_config +from esphome.config_helpers import Extend, merge_config +import esphome.config_validation as cv from esphome.const import CONF_SUBSTITUTIONS -from esphome.core import CORE +from esphome.core import CORE, Lambda from esphome.util import OrderedDict _LOGGER = logging.getLogger(__name__) @@ -144,7 +145,7 @@ def test_substitutions_fixtures( config = do_packages_pass(config) - substitutions.do_substitution_pass(config, command_line_substitutions) + config = substitutions.do_substitution_pass(config, command_line_substitutions) config = merge_packages(config) @@ -206,7 +207,7 @@ def test_substitutions_with_command_line_maintains_ordered_dict() -> None: command_line_subs = {"var2": "override", "var3": "new_value"} # Call do_substitution_pass with command line substitutions - substitutions.do_substitution_pass(config, command_line_subs) + config = substitutions.do_substitution_pass(config, command_line_subs) # Verify that config is still an OrderedDict assert isinstance(config, OrderedDict), "Config should remain an OrderedDict" @@ -234,7 +235,7 @@ def test_substitutions_without_command_line_maintains_ordered_dict() -> None: config["other_key"] = "other_value" # Call without command line substitutions - substitutions.do_substitution_pass(config, None) + config = substitutions.do_substitution_pass(config, None) # Verify that config is still an OrderedDict assert isinstance(config, OrderedDict), "Config should remain an OrderedDict" @@ -268,7 +269,7 @@ def test_substitutions_after_merge_config_maintains_ordered_dict() -> None: ) # Now try to run substitution pass on the merged config - substitutions.do_substitution_pass(merged_config, None) + merged_config = substitutions.do_substitution_pass(merged_config, None) # Should not raise AttributeError assert isinstance(merged_config, OrderedDict), ( @@ -279,7 +280,7 @@ def test_substitutions_after_merge_config_maintains_ordered_dict() -> None: def test_validate_config_with_command_line_substitutions_maintains_ordered_dict( - tmp_path, + tmp_path: Path, ) -> None: """Test that validate_config preserves OrderedDict when merging command-line substitutions. @@ -288,7 +289,7 @@ def test_validate_config_with_command_line_substitutions_maintains_ordered_dict( """ # Create a minimal valid config test_config = OrderedDict() - test_config["esphome"] = {"name": "test_device", "platform": "ESP32"} + test_config["esphome"] = {"name": "test_device"} test_config[CONF_SUBSTITUTIONS] = OrderedDict({"var1": "value1", "var2": "value2"}) test_config["esp32"] = {"board": "esp32dev"} @@ -314,17 +315,11 @@ def test_validate_config_with_command_line_substitutions_maintains_ordered_dict( assert result[CONF_SUBSTITUTIONS]["var3"] == "new_value" -def test_validate_config_without_command_line_substitutions_maintains_ordered_dict( - tmp_path, -) -> None: - """Test that validate_config preserves OrderedDict without command-line substitutions. - - This tests the code path in config.py where result[CONF_SUBSTITUTIONS] is set - using merge_dicts_ordered() when command_line_substitutions is None. - """ +def _get_test_minimal_valid_config(tmp_path: Path) -> OrderedDict: + """Helper to create a minimal valid config for testing.""" # Create a minimal valid config test_config = OrderedDict() - test_config["esphome"] = {"name": "test_device", "platform": "ESP32"} + test_config["esphome"] = {"name": "test_device"} test_config[CONF_SUBSTITUTIONS] = OrderedDict({"var1": "value1", "var2": "value2"}) test_config["esp32"] = {"board": "esp32dev"} @@ -332,6 +327,19 @@ def test_validate_config_without_command_line_substitutions_maintains_ordered_di test_yaml = tmp_path / "test.yaml" test_yaml.write_text("# test config") CORE.config_path = test_yaml + return test_config + + +def test_validate_config_without_command_line_substitutions_maintains_ordered_dict( + tmp_path: Path, +) -> None: + """Test that validate_config preserves OrderedDict without command-line substitutions. + + This tests the code path in config.py where result[CONF_SUBSTITUTIONS] is set + using merge_dicts_ordered() when command_line_substitutions is None. + """ + + test_config = _get_test_minimal_valid_config(tmp_path) # Call validate_config without command line substitutions result = config_module.validate_config(test_config, None) @@ -384,3 +392,205 @@ def test_merge_config_preserves_ordered_dict() -> None: assert not isinstance(result, OrderedDict), ( "dict + dict should not return OrderedDict" ) + + +def test_substitution_pass_error_gets_captured( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """vol.Invalid from do_substitution_pass is captured by validate_config.""" + + # Patch the target: in config_module.do_substitution_pass (NOT where it's defined) + def fake_do_substitution_pass(*args, **kwargs): + raise cv.Invalid("Error in do_substitutions_pass!!") + + monkeypatch.setattr( + config_module, "do_substitution_pass", fake_do_substitution_pass + ) + + # Prepare minimal config + no CLI substitutions + config = _get_test_minimal_valid_config(tmp_path) + + # Call the function under test + result = config_module.validate_config(config, None) + + # Now assert that add_error was called with the vol.Invalid + + assert "Error in do_substitutions_pass!!" in str(result.get_error_for_path([])) + + +@pytest.mark.parametrize( + "value", ["", " ", "1foo", "9VAR", "0abc", "$1foo", "$9VAR", "$0abc"] +) +def test_validate_substitution_key_empty_raises(value: str) -> None: + """Empty (or all-whitespace) substitution keys are rejected.""" + with pytest.raises(cv.Invalid): + substitutions.validate_substitution_key(value) + + +@pytest.mark.parametrize( + "input_value, expected_output", + [ + ("$FOO_bar9", "FOO_bar9"), # Valid key with leading '$' + ("Foo_bar9", "Foo_bar9"), # Normal valid key + ], +) +def test_validate_substitution_key_valid( + input_value: str, expected_output: str +) -> None: + """Valid substitution keys are accepted with optional leading '$'.""" + result = substitutions.validate_substitution_key(input_value) + assert result == expected_output + + +def test_circular_dependency_warnings( + caplog: pytest.LogCaptureFixture, +) -> None: + """Circular substitution references produce warnings naming the cause.""" + config = OrderedDict( + { + CONF_SUBSTITUTIONS: OrderedDict({"x": "${y}", "y": "${x}"}), + "key": "value", + } + ) + with caplog.at_level(logging.WARNING): + substitutions.do_substitution_pass(config) + + assert "Could not resolve substitution variable 'x'" in caplog.text + assert "'y' is undefined" in caplog.text + assert "Could not resolve substitution variable 'y'" in caplog.text + assert "'x' is undefined" in caplog.text + # Verify path includes location + assert "substitutions->x" in caplog.text + assert "substitutions->y" in caplog.text + + +def test_missing_dependency_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """A substitution referencing an undefined variable warns with the cause.""" + config = OrderedDict( + { + CONF_SUBSTITUTIONS: OrderedDict({"a": "${missing}"}), + "key": "value", + } + ) + with caplog.at_level(logging.WARNING): + substitutions.do_substitution_pass(config) + + assert "Could not resolve substitution variable 'a'" in caplog.text + assert "'missing' is undefined" in caplog.text + assert "substitutions->a" in caplog.text + + +def test_undefined_variable_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """A reference to an undefined variable in config values produces a warning.""" + config = OrderedDict( + { + "key": "${undefined_var}", + } + ) + with caplog.at_level(logging.WARNING): + substitutions.do_substitution_pass(config) + + assert "'undefined_var' is undefined" in caplog.text + + +def test_password_field_warnings_suppressed( + caplog: pytest.LogCaptureFixture, +) -> None: + """Undefined variables in password fields should not produce warnings.""" + config = OrderedDict( + { + "password": "${undefined_var}", + } + ) + with caplog.at_level(logging.WARNING): + substitutions.do_substitution_pass(config) + + assert caplog.text == "" + + +def test_config_context_unresolvable_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """Unresolvable vars in a ConfigContext produce warnings via push_context.""" + inner = OrderedDict({"key": "${a}"}) + yaml_util.add_context(inner, {"a": "${undefined}"}) + config = OrderedDict({"items": [inner]}) + with caplog.at_level(logging.WARNING): + substitutions.do_substitution_pass(config) + + assert "Could not resolve substitution variable 'a'" in caplog.text + assert "'undefined' is undefined" in caplog.text + + +def test_non_string_substitution_value_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """Undefined vars in non-string contexts (e.g. dict keys) produce warnings.""" + config = OrderedDict( + { + "items": {"${undefined_key}": "value"}, + } + ) + with caplog.at_level(logging.WARNING): + substitutions.do_substitution_pass(config) + + assert "'undefined_key' is undefined" in caplog.text + + +def test_lambda_substitution() -> None: + """Substitution inside a Lambda value should be expanded.""" + lam = Lambda("return ${var};") + config = OrderedDict( + { + CONF_SUBSTITUTIONS: OrderedDict({"var": "42"}), + "lambda": lam, + } + ) + substitutions.do_substitution_pass(config) + assert lam.value == "return 42;" + + +def test_lambda_no_substitution_unchanged() -> None: + """A Lambda with no variable references should not be mutated.""" + lam = Lambda("return 1;") + original_value = lam.value + config = OrderedDict( + { + CONF_SUBSTITUTIONS: OrderedDict({"var": "42"}), + "lambda": lam, + } + ) + substitutions.do_substitution_pass(config) + assert lam.value is original_value + + +def test_extend_substitution() -> None: + """Substitution inside an Extend value should be expanded.""" + ext = Extend("${component_id}") + config = OrderedDict( + { + CONF_SUBSTITUTIONS: OrderedDict({"component_id": "my_sensor"}), + "sensor": ext, + } + ) + substitutions.do_substitution_pass(config) + assert ext.value == "my_sensor" + + +def test_do_substitution_pass_substitutions_must_be_mapping_from_config() -> None: + """Non-mapping substitutions raises cv.Invalid.""" + config = OrderedDict( + { + CONF_SUBSTITUTIONS: ["not", "a", "mapping"], + "other": "value", + } + ) + + with pytest.raises( + cv.Invalid, match="Substitutions must be a key to value mapping" + ): + substitutions.do_substitution_pass(config) diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index adb7658bfd..35a4bc3707 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -25,7 +25,7 @@ def test_include_with_vars(fixture_path: Path) -> None: yaml_file = fixture_path / "yaml_util" / "includetest.yaml" actual = yaml_util.load_yaml(yaml_file) - substitutions.do_substitution_pass(actual, None) + actual = substitutions.do_substitution_pass(actual, None) assert actual["esphome"]["name"] == "original" assert actual["esphome"]["libraries"][0] == "Wire" assert actual["esp8266"]["board"] == "nodemcu" From e6a73cab8f1e090244d63d4d63766a570a45ec62 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 24 Mar 2026 09:04:53 +1000 Subject: [PATCH 002/115] [number] Add sensor platform (#15125) --- esphome/components/number/sensor/__init__.py | 25 +++++++++++++++++++ .../number/sensor/number_sensor.cpp | 16 ++++++++++++ .../components/number/sensor/number_sensor.h | 19 ++++++++++++++ tests/components/number/common.yaml | 13 ++++++++++ tests/components/number/test.esp32-idf.yaml | 2 ++ tests/components/number/test.esp8266-ard.yaml | 2 ++ 6 files changed, 77 insertions(+) create mode 100644 esphome/components/number/sensor/__init__.py create mode 100644 esphome/components/number/sensor/number_sensor.cpp create mode 100644 esphome/components/number/sensor/number_sensor.h create mode 100644 tests/components/number/common.yaml create mode 100644 tests/components/number/test.esp32-idf.yaml create mode 100644 tests/components/number/test.esp8266-ard.yaml diff --git a/esphome/components/number/sensor/__init__.py b/esphome/components/number/sensor/__init__.py new file mode 100644 index 0000000000..0d4b580d7e --- /dev/null +++ b/esphome/components/number/sensor/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import CONF_SOURCE_ID + +from .. import Number, number_ns + +NumberSensor = number_ns.class_("NumberSensor", sensor.Sensor, cg.Component) + + +CONFIG_SCHEMA = ( + sensor.sensor_schema(NumberSensor) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(Number), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + source = await cg.get_variable(config[CONF_SOURCE_ID]) + var = await sensor.new_sensor(config, source) + await cg.register_component(var, config) diff --git a/esphome/components/number/sensor/number_sensor.cpp b/esphome/components/number/sensor/number_sensor.cpp new file mode 100644 index 0000000000..227202622a --- /dev/null +++ b/esphome/components/number/sensor/number_sensor.cpp @@ -0,0 +1,16 @@ +#include "number_sensor.h" +#include "esphome/core/log.h" + +namespace esphome::number { + +static const char *const TAG = "number.sensor"; + +void NumberSensor::setup() { + this->source_->add_on_state_callback([this](float value) { this->publish_state(value); }); + if (this->source_->has_state()) + this->publish_state(this->source_->state); +} + +void NumberSensor::dump_config() { LOG_SENSOR("", "Number Sensor", this); } + +} // namespace esphome::number diff --git a/esphome/components/number/sensor/number_sensor.h b/esphome/components/number/sensor/number_sensor.h new file mode 100644 index 0000000000..2d6825a298 --- /dev/null +++ b/esphome/components/number/sensor/number_sensor.h @@ -0,0 +1,19 @@ +#pragma once + +#include "../number.h" +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome::number { + +class NumberSensor : public sensor::Sensor, public Component { + public: + explicit NumberSensor(Number *source) : source_(source) {} + void setup() override; + void dump_config() override; + + protected: + Number *source_; +}; + +} // namespace esphome::number diff --git a/tests/components/number/common.yaml b/tests/components/number/common.yaml new file mode 100644 index 0000000000..c17c2dd5f8 --- /dev/null +++ b/tests/components/number/common.yaml @@ -0,0 +1,13 @@ +number: + - platform: template + name: "Test Number" + id: test_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + +sensor: + - platform: number + name: "Test Number Value" + source_id: test_number diff --git a/tests/components/number/test.esp32-idf.yaml b/tests/components/number/test.esp32-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/number/test.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/number/test.esp8266-ard.yaml b/tests/components/number/test.esp8266-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/number/test.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml From 0fb31726f69b304581caec41614a080774feda8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 13:39:29 -1000 Subject: [PATCH 003/115] [esp32] Add sram1_as_iram option and bootloader version detection (#14874) --- esphome/components/esp32/__init__.py | 19 ++++++++ esphome/core/application.cpp | 50 ++++++++++++++++++---- esphome/core/defines.h | 1 + tests/components/esp32/test.esp32-idf.yaml | 1 + 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index f85f13fe73..1ecc270fd1 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -97,6 +97,7 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision" CONF_RELEASE = "release" +CONF_SRAM1_AS_IRAM = "sram1_as_iram" CONF_SUBTYPE = "subtype" ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32" @@ -884,6 +885,13 @@ def final_validate(config): path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_MINIMUM_CHIP_REVISION], ) ) + if config[CONF_VARIANT] != VARIANT_ESP32 and advanced[CONF_SRAM1_AS_IRAM]: + errs.append( + cv.Invalid( + f"'{CONF_SRAM1_AS_IRAM}' is only supported on {VARIANT_ESP32}", + path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_SRAM1_AS_IRAM], + ) + ) if ( config[CONF_VARIANT] != VARIANT_ESP32P4 and config.get(CONF_ENGINEERING_SAMPLE) is not None @@ -1131,6 +1139,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_MINIMUM_CHIP_REVISION): cv.one_of( *ESP32_CHIP_REVISIONS ), + cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean, # DHCP server is needed for WiFi AP mode. When WiFi component is used, # it will handle disabling DHCP server when AP is not configured. # Default to false (disabled) when WiFi is not used. @@ -1655,6 +1664,16 @@ async def to_code(config): for rev, flag in ESP32_CHIP_REVISIONS.items(): add_idf_sdkconfig_option(flag, rev == min_rev) cg.add_define("USE_ESP32_MIN_CHIP_REVISION_SET") + + # Use SRAM1 region as IRAM on ESP32 (original) variant + # This provides an additional 40KB of IRAM by using SRAM1 memory that was previously + # reserved for bootloader DRAM. Requires a bootloader from ESP-IDF v5.1 or later. + # WARNING: If the device has an old bootloader (pre-v5.1), the app will fail to boot. + # A USB flash will update the bootloader automatically. OTA updates do not. + # See: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/performance/ram-usage.html + if variant == VARIANT_ESP32 and conf[CONF_ADVANCED][CONF_SRAM1_AS_IRAM]: + add_idf_sdkconfig_option("CONFIG_ESP_SYSTEM_ESP32_SRAM1_REGION_AS_IRAM", True) + cg.add_define("USE_ESP32_SRAM1_AS_IRAM") add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False) add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True) add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM_FILENAME", "partitions.csv") diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index c020a8ed58..ce15aed1e2 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -9,6 +9,8 @@ #endif #ifdef USE_ESP32 #include +#include +#include #endif #ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" @@ -167,19 +169,49 @@ void Application::process_dump_config_() { esp_chip_info(&chip_info); ESP_LOGI(TAG, "ESP32 Chip: %s rev%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100, chip_info.revision % 100, chip_info.cores); -#if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET) - // Suggest optimization for chips that don't need the PSRAM cache workaround - if (chip_info.revision >= 300) { -#ifdef USE_PSRAM - ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to save ~10KB IRAM", chip_info.revision / 100, - chip_info.revision % 100); -#else - ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to reduce binary size", chip_info.revision / 100, - chip_info.revision % 100); +#if defined(USE_ESP32_VARIANT_ESP32) && (!defined(USE_ESP32_MIN_CHIP_REVISION_SET) || !defined(USE_ESP32_SRAM1_AS_IRAM)) + static const char *const ESP32_ADVANCED_PATH = "under esp32 > framework > advanced"; #endif +#if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET) + { + // Suggest optimization for chips that don't need the PSRAM cache workaround + if (chip_info.revision >= 300) { +#ifdef USE_PSRAM + ESP_LOGW(TAG, "Chip rev >= 3.0 detected. Set minimum_chip_revision: \"%d.%d\" %s to save ~10KB IRAM", + chip_info.revision / 100, chip_info.revision % 100, ESP32_ADVANCED_PATH); +#else + ESP_LOGW(TAG, "Chip rev >= 3.0 detected. Set minimum_chip_revision: \"%d.%d\" %s to reduce binary size", + chip_info.revision / 100, chip_info.revision % 100, ESP32_ADVANCED_PATH); +#endif + } } #endif + { + // esp_bootloader_desc_t is available in ESP-IDF >= 5.2; if readable the bootloader is modern. + // + // Design decision: We intentionally do NOT mention sram1_as_iram when the bootloader is too old. + // Enabling sram1_as_iram with an old bootloader causes a hard brick (device fails to boot, + // requires USB reflash to recover). Users don't always read warnings carefully, so we only + // suggest the option once we've confirmed the bootloader can handle it. In practice this + // means a user with an old bootloader may need to flash twice: once via USB to update the + // bootloader (they'll see the suggestion on next boot), then OTA with sram1_as_iram: true. + // Two flashes is a better outcome than a bricked device. + esp_bootloader_desc_t boot_desc; + if (esp_ota_get_bootloader_description(nullptr, &boot_desc) != ESP_OK) { +#ifdef USE_ESP32_VARIANT_ESP32 + ESP_LOGW(TAG, "Bootloader too old for OTA rollback and SRAM1 as IRAM (+40KB). " + "Flash via USB once to update the bootloader"); +#else + ESP_LOGW(TAG, "Bootloader too old for OTA rollback. Flash via USB once to update the bootloader"); #endif + } +#if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_SRAM1_AS_IRAM) + else { + ESP_LOGW(TAG, "Bootloader supports SRAM1 as IRAM (+40KB). Set sram1_as_iram: true %s", ESP32_ADVANCED_PATH); + } +#endif + } +#endif // USE_ESP32 } this->components_[this->dump_config_at_]->call_dump_config_(); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index f437e30a95..996818c2e6 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -202,6 +202,7 @@ #define USE_ESPHOME_TASK_LOG_BUFFER #define USE_OTA_ROLLBACK #define USE_ESP32_MIN_CHIP_REVISION_SET +#define USE_ESP32_SRAM1_AS_IRAM #define USE_BLUETOOTH_PROXY #define BLUETOOTH_PROXY_MAX_CONNECTIONS 3 diff --git a/tests/components/esp32/test.esp32-idf.yaml b/tests/components/esp32/test.esp32-idf.yaml index da85aa3b0f..b999f23e1c 100644 --- a/tests/components/esp32/test.esp32-idf.yaml +++ b/tests/components/esp32/test.esp32-idf.yaml @@ -19,6 +19,7 @@ esp32: disable_mbedtls_pkcs7: true disable_regi2c_in_iram: true disable_fatfs: true + sram1_as_iram: true wifi: ssid: MySSID From a0d0516b22e7ea8cd29c718ef510e9d33d3de5a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 13:40:41 -1000 Subject: [PATCH 004/115] [benchmark] Add noise handshake benchmark (#15039) --- .../components/api/bench_noise_encrypt.cpp | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/tests/benchmarks/components/api/bench_noise_encrypt.cpp b/tests/benchmarks/components/api/bench_noise_encrypt.cpp index 223e6ada0d..9ef928192c 100644 --- a/tests/benchmarks/components/api/bench_noise_encrypt.cpp +++ b/tests/benchmarks/components/api/bench_noise_encrypt.cpp @@ -172,6 +172,135 @@ BENCHMARK(NoiseDecrypt_MediumMessage); static void NoiseDecrypt_LargeMessage(benchmark::State &state) { noise_decrypt_bench(state, 1024); } BENCHMARK(NoiseDecrypt_LargeMessage); +// --- Full Noise_NNpsk0 handshake benchmark --- +// Measures the complete handshake between initiator and responder: +// - Create handshake states for both sides +// - Set PSK and prologue +// - Exchange messages (initiator write -> responder read -> responder write -> initiator read) +// - Split to get cipher states +// This is dominated by Curve25519 DH operations (expensive on ESP8266). +// No inner iterations — each handshake is already expensive enough. + +static void NoiseHandshake_Full(benchmark::State &state) { + // Matching ESPHome's protocol: Noise_NNpsk0_25519_ChaChaPoly_SHA256 + NoiseProtocolId nid; + memset(&nid, 0, sizeof(nid)); + nid.pattern_id = NOISE_PATTERN_NN; + nid.cipher_id = NOISE_CIPHER_CHACHAPOLY; + nid.dh_id = NOISE_DH_CURVE25519; + nid.prefix_id = NOISE_PREFIX_STANDARD; + nid.hybrid_id = NOISE_DH_NONE; + nid.hash_id = NOISE_HASH_SHA256; + nid.modifier_ids[0] = NOISE_MODIFIER_PSK0; + + // Dummy PSK (32 bytes) and prologue matching production setup + static constexpr uint8_t PSK[32] = {0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, + 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, + 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB}; + static constexpr uint8_t PROLOGUE[] = "NoESPHome"; + + // Message buffer for handshake exchange (max handshake message ~96 bytes) + uint8_t msg_buf[128]; + + for (auto _ : state) { + NoiseHandshakeState *initiator = nullptr; + NoiseHandshakeState *responder = nullptr; + NoiseCipherState *init_send = nullptr, *init_recv = nullptr; + NoiseCipherState *resp_send = nullptr, *resp_recv = nullptr; + int err; + + // Create both handshake states + err = noise_handshakestate_new_by_id(&initiator, &nid, NOISE_ROLE_INITIATOR); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Failed to create initiator"); + return; + } + err = noise_handshakestate_new_by_id(&responder, &nid, NOISE_ROLE_RESPONDER); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Failed to create responder"); + noise_handshakestate_free(initiator); + return; + } + + // Set PSK and prologue on both sides + noise_handshakestate_set_pre_shared_key(initiator, PSK, sizeof(PSK)); + noise_handshakestate_set_pre_shared_key(responder, PSK, sizeof(PSK)); + noise_handshakestate_set_prologue(initiator, PROLOGUE, sizeof(PROLOGUE) - 1); + noise_handshakestate_set_prologue(responder, PROLOGUE, sizeof(PROLOGUE) - 1); + + noise_handshakestate_start(initiator); + noise_handshakestate_start(responder); + + // Message 1: Initiator -> Responder + NoiseBuffer write_buf, read_buf; + noise_buffer_set_output(write_buf, msg_buf, sizeof(msg_buf)); + err = noise_handshakestate_write_message(initiator, &write_buf, nullptr); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Initiator write_message failed"); + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + return; + } + + noise_buffer_set_input(read_buf, msg_buf, write_buf.size); + err = noise_handshakestate_read_message(responder, &read_buf, nullptr); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Responder read_message failed"); + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + return; + } + + // Message 2: Responder -> Initiator + noise_buffer_set_output(write_buf, msg_buf, sizeof(msg_buf)); + err = noise_handshakestate_write_message(responder, &write_buf, nullptr); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Responder write_message failed"); + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + return; + } + + noise_buffer_set_input(read_buf, msg_buf, write_buf.size); + err = noise_handshakestate_read_message(initiator, &read_buf, nullptr); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Initiator read_message failed"); + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + return; + } + + // Split to get cipher states + err = noise_handshakestate_split(initiator, &init_send, &init_recv); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Initiator split failed"); + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + return; + } + err = noise_handshakestate_split(responder, &resp_send, &resp_recv); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Responder split failed"); + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + noise_cipherstate_free(init_send); + noise_cipherstate_free(init_recv); + return; + } + + benchmark::DoNotOptimize(init_send); + + // Cleanup + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + noise_cipherstate_free(init_send); + noise_cipherstate_free(init_recv); + noise_cipherstate_free(resp_send); + noise_cipherstate_free(resp_recv); + } +} +BENCHMARK(NoiseHandshake_Full); + } // namespace esphome::api::benchmarks #endif // USE_API_NOISE From 382de7ca906b2b584b1c6188105a50523182f2f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 13:40:53 -1000 Subject: [PATCH 005/115] [api] Store dump strings in PROGMEM to save RAM on ESP8266 (#14982) --- esphome/components/api/api_pb2.h | 274 +-- esphome/components/api/api_pb2_dump.cpp | 2425 ++++++++++---------- esphome/components/api/api_pb2_service.cpp | 4 +- esphome/components/api/api_pb2_service.h | 2 +- esphome/components/api/proto.h | 20 +- script/api_protobuf/api_protobuf.py | 96 +- 6 files changed, 1443 insertions(+), 1378 deletions(-) diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 86289a28d6..16586e6e9a 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -388,7 +388,7 @@ class HelloRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 1; static constexpr uint8_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "hello_request"; } + const LogString *message_name() const override { return LOG_STR("hello_request"); } #endif StringRef client_info{}; uint32_t api_version_major{0}; @@ -406,7 +406,7 @@ class HelloResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 2; static constexpr uint8_t ESTIMATED_SIZE = 26; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "hello_response"; } + const LogString *message_name() const override { return LOG_STR("hello_response"); } #endif uint32_t api_version_major{0}; uint32_t api_version_minor{0}; @@ -425,7 +425,7 @@ class DisconnectRequest final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 5; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "disconnect_request"; } + const LogString *message_name() const override { return LOG_STR("disconnect_request"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -438,7 +438,7 @@ class DisconnectResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 6; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "disconnect_response"; } + const LogString *message_name() const override { return LOG_STR("disconnect_response"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -451,7 +451,7 @@ class PingRequest final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 7; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "ping_request"; } + const LogString *message_name() const override { return LOG_STR("ping_request"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -464,7 +464,7 @@ class PingResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 8; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "ping_response"; } + const LogString *message_name() const override { return LOG_STR("ping_response"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -520,7 +520,7 @@ class DeviceInfoResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 10; static constexpr uint16_t ESTIMATED_SIZE = 309; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "device_info_response"; } + const LogString *message_name() const override { return LOG_STR("device_info_response"); } #endif StringRef name{}; StringRef mac_address{}; @@ -587,7 +587,7 @@ class ListEntitiesDoneResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 19; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_done_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_done_response"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -601,7 +601,7 @@ class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 12; static constexpr uint8_t ESTIMATED_SIZE = 51; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_binary_sensor_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_binary_sensor_response"); } #endif StringRef device_class{}; bool is_status_binary_sensor{false}; @@ -618,7 +618,7 @@ class BinarySensorStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 21; static constexpr uint8_t ESTIMATED_SIZE = 13; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "binary_sensor_state_response"; } + const LogString *message_name() const override { return LOG_STR("binary_sensor_state_response"); } #endif bool state{false}; bool missing_state{false}; @@ -637,7 +637,7 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 13; static constexpr uint8_t ESTIMATED_SIZE = 57; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_cover_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_cover_response"); } #endif bool assumed_state{false}; bool supports_position{false}; @@ -657,7 +657,7 @@ class CoverStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 22; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "cover_state_response"; } + const LogString *message_name() const override { return LOG_STR("cover_state_response"); } #endif float position{0.0f}; float tilt{0.0f}; @@ -675,7 +675,7 @@ class CoverCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 30; static constexpr uint8_t ESTIMATED_SIZE = 25; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "cover_command_request"; } + const LogString *message_name() const override { return LOG_STR("cover_command_request"); } #endif bool has_position{false}; float position{0.0f}; @@ -697,7 +697,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 14; static constexpr uint8_t ESTIMATED_SIZE = 68; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_fan_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_fan_response"); } #endif bool supports_oscillation{false}; bool supports_speed{false}; @@ -717,7 +717,7 @@ class FanStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 23; static constexpr uint8_t ESTIMATED_SIZE = 28; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "fan_state_response"; } + const LogString *message_name() const override { return LOG_STR("fan_state_response"); } #endif bool state{false}; bool oscillating{false}; @@ -737,7 +737,7 @@ class FanCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 31; static constexpr uint8_t ESTIMATED_SIZE = 38; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "fan_command_request"; } + const LogString *message_name() const override { return LOG_STR("fan_command_request"); } #endif bool has_state{false}; bool state{false}; @@ -765,7 +765,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 15; static constexpr uint8_t ESTIMATED_SIZE = 73; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_light_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_light_response"); } #endif const light::ColorModeMask *supported_color_modes{}; float min_mireds{0.0f}; @@ -784,7 +784,7 @@ class LightStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 24; static constexpr uint8_t ESTIMATED_SIZE = 67; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "light_state_response"; } + const LogString *message_name() const override { return LOG_STR("light_state_response"); } #endif bool state{false}; float brightness{0.0f}; @@ -811,7 +811,7 @@ class LightCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 32; static constexpr uint8_t ESTIMATED_SIZE = 112; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "light_command_request"; } + const LogString *message_name() const override { return LOG_STR("light_command_request"); } #endif bool has_state{false}; bool state{false}; @@ -855,7 +855,7 @@ class ListEntitiesSensorResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 16; static constexpr uint8_t ESTIMATED_SIZE = 66; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_sensor_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_sensor_response"); } #endif StringRef unit_of_measurement{}; int32_t accuracy_decimals{0}; @@ -875,7 +875,7 @@ class SensorStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 25; static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "sensor_state_response"; } + const LogString *message_name() const override { return LOG_STR("sensor_state_response"); } #endif float state{0.0f}; bool missing_state{false}; @@ -894,7 +894,7 @@ class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 17; static constexpr uint8_t ESTIMATED_SIZE = 51; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_switch_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_switch_response"); } #endif bool assumed_state{false}; StringRef device_class{}; @@ -911,7 +911,7 @@ class SwitchStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 26; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "switch_state_response"; } + const LogString *message_name() const override { return LOG_STR("switch_state_response"); } #endif bool state{false}; void encode(ProtoWriteBuffer &buffer) const; @@ -927,7 +927,7 @@ class SwitchCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 33; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "switch_command_request"; } + const LogString *message_name() const override { return LOG_STR("switch_command_request"); } #endif bool state{false}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -945,7 +945,7 @@ class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 18; static constexpr uint8_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_text_sensor_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_text_sensor_response"); } #endif StringRef device_class{}; void encode(ProtoWriteBuffer &buffer) const; @@ -961,7 +961,7 @@ class TextSensorStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 27; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "text_sensor_state_response"; } + const LogString *message_name() const override { return LOG_STR("text_sensor_state_response"); } #endif StringRef state{}; bool missing_state{false}; @@ -979,7 +979,7 @@ class SubscribeLogsRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 28; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_logs_request"; } + const LogString *message_name() const override { return LOG_STR("subscribe_logs_request"); } #endif enums::LogLevel level{}; bool dump_config{false}; @@ -995,7 +995,7 @@ class SubscribeLogsResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 29; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_logs_response"; } + const LogString *message_name() const override { return LOG_STR("subscribe_logs_response"); } #endif enums::LogLevel level{}; const uint8_t *message_ptr_{nullptr}; @@ -1018,7 +1018,7 @@ class NoiseEncryptionSetKeyRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 124; static constexpr uint8_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "noise_encryption_set_key_request"; } + const LogString *message_name() const override { return LOG_STR("noise_encryption_set_key_request"); } #endif const uint8_t *key{nullptr}; uint16_t key_len{0}; @@ -1034,7 +1034,7 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 125; static constexpr uint8_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "noise_encryption_set_key_response"; } + const LogString *message_name() const override { return LOG_STR("noise_encryption_set_key_response"); } #endif bool success{false}; void encode(ProtoWriteBuffer &buffer) const; @@ -1064,7 +1064,7 @@ class HomeassistantActionRequest final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 35; static constexpr uint8_t ESTIMATED_SIZE = 128; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "homeassistant_action_request"; } + const LogString *message_name() const override { return LOG_STR("homeassistant_action_request"); } #endif StringRef service{}; FixedVector data{}; @@ -1095,7 +1095,7 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 130; static constexpr uint8_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "homeassistant_action_response"; } + const LogString *message_name() const override { return LOG_STR("homeassistant_action_response"); } #endif uint32_t call_id{0}; bool success{false}; @@ -1119,7 +1119,7 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 39; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_home_assistant_state_response"; } + const LogString *message_name() const override { return LOG_STR("subscribe_home_assistant_state_response"); } #endif StringRef entity_id{}; StringRef attribute{}; @@ -1137,7 +1137,7 @@ class HomeAssistantStateResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 40; static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "home_assistant_state_response"; } + const LogString *message_name() const override { return LOG_STR("home_assistant_state_response"); } #endif StringRef entity_id{}; StringRef state{}; @@ -1155,7 +1155,7 @@ class GetTimeRequest final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 36; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "get_time_request"; } + const LogString *message_name() const override { return LOG_STR("get_time_request"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1197,7 +1197,7 @@ class GetTimeResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 37; static constexpr uint8_t ESTIMATED_SIZE = 31; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "get_time_response"; } + const LogString *message_name() const override { return LOG_STR("get_time_response"); } #endif uint32_t epoch_seconds{0}; StringRef timezone{}; @@ -1228,7 +1228,7 @@ class ListEntitiesServicesResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 41; static constexpr uint8_t ESTIMATED_SIZE = 50; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_services_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_services_response"); } #endif StringRef name{}; uint32_t key{0}; @@ -1268,7 +1268,7 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 42; static constexpr uint8_t ESTIMATED_SIZE = 45; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "execute_service_request"; } + const LogString *message_name() const override { return LOG_STR("execute_service_request"); } #endif uint32_t key{0}; FixedVector args{}; @@ -1295,7 +1295,7 @@ class ExecuteServiceResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 131; static constexpr uint8_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "execute_service_response"; } + const LogString *message_name() const override { return LOG_STR("execute_service_response"); } #endif uint32_t call_id{0}; bool success{false}; @@ -1319,7 +1319,7 @@ class ListEntitiesCameraResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 43; static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_camera_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_camera_response"); } #endif void encode(ProtoWriteBuffer &buffer) const; uint32_t calculate_size() const; @@ -1334,7 +1334,7 @@ class CameraImageResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 44; static constexpr uint8_t ESTIMATED_SIZE = 30; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "camera_image_response"; } + const LogString *message_name() const override { return LOG_STR("camera_image_response"); } #endif const uint8_t *data_ptr_{nullptr}; size_t data_len_{0}; @@ -1356,7 +1356,7 @@ class CameraImageRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 45; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "camera_image_request"; } + const LogString *message_name() const override { return LOG_STR("camera_image_request"); } #endif bool single{false}; bool stream{false}; @@ -1374,7 +1374,7 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 46; static constexpr uint8_t ESTIMATED_SIZE = 150; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_climate_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_climate_response"); } #endif bool supports_current_temperature{false}; bool supports_two_point_target_temperature{false}; @@ -1407,7 +1407,7 @@ class ClimateStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 47; static constexpr uint8_t ESTIMATED_SIZE = 68; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "climate_state_response"; } + const LogString *message_name() const override { return LOG_STR("climate_state_response"); } #endif enums::ClimateMode mode{}; float current_temperature{0.0f}; @@ -1435,7 +1435,7 @@ class ClimateCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 48; static constexpr uint8_t ESTIMATED_SIZE = 84; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "climate_command_request"; } + const LogString *message_name() const override { return LOG_STR("climate_command_request"); } #endif bool has_mode{false}; enums::ClimateMode mode{}; @@ -1473,7 +1473,7 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 132; static constexpr uint8_t ESTIMATED_SIZE = 63; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_water_heater_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_water_heater_response"); } #endif float min_temperature{0.0f}; float max_temperature{0.0f}; @@ -1493,7 +1493,7 @@ class WaterHeaterStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 133; static constexpr uint8_t ESTIMATED_SIZE = 35; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "water_heater_state_response"; } + const LogString *message_name() const override { return LOG_STR("water_heater_state_response"); } #endif float current_temperature{0.0f}; float target_temperature{0.0f}; @@ -1514,7 +1514,7 @@ class WaterHeaterCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 134; static constexpr uint8_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "water_heater_command_request"; } + const LogString *message_name() const override { return LOG_STR("water_heater_command_request"); } #endif uint32_t has_fields{0}; enums::WaterHeaterMode mode{}; @@ -1537,7 +1537,7 @@ class ListEntitiesNumberResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 49; static constexpr uint8_t ESTIMATED_SIZE = 75; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_number_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_number_response"); } #endif float min_value{0.0f}; float max_value{0.0f}; @@ -1558,7 +1558,7 @@ class NumberStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 50; static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "number_state_response"; } + const LogString *message_name() const override { return LOG_STR("number_state_response"); } #endif float state{0.0f}; bool missing_state{false}; @@ -1575,7 +1575,7 @@ class NumberCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 51; static constexpr uint8_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "number_command_request"; } + const LogString *message_name() const override { return LOG_STR("number_command_request"); } #endif float state{0.0f}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1593,7 +1593,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 52; static constexpr uint8_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_select_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_select_response"); } #endif const FixedVector *options{}; void encode(ProtoWriteBuffer &buffer) const; @@ -1609,7 +1609,7 @@ class SelectStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 53; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "select_state_response"; } + const LogString *message_name() const override { return LOG_STR("select_state_response"); } #endif StringRef state{}; bool missing_state{false}; @@ -1626,7 +1626,7 @@ class SelectCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 54; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "select_command_request"; } + const LogString *message_name() const override { return LOG_STR("select_command_request"); } #endif StringRef state{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1645,7 +1645,7 @@ class ListEntitiesSirenResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 55; static constexpr uint8_t ESTIMATED_SIZE = 62; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_siren_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_siren_response"); } #endif const FixedVector *tones{}; bool supports_duration{false}; @@ -1663,7 +1663,7 @@ class SirenStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 56; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "siren_state_response"; } + const LogString *message_name() const override { return LOG_STR("siren_state_response"); } #endif bool state{false}; void encode(ProtoWriteBuffer &buffer) const; @@ -1679,7 +1679,7 @@ class SirenCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 57; static constexpr uint8_t ESTIMATED_SIZE = 37; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "siren_command_request"; } + const LogString *message_name() const override { return LOG_STR("siren_command_request"); } #endif bool has_state{false}; bool state{false}; @@ -1705,7 +1705,7 @@ class ListEntitiesLockResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 58; static constexpr uint8_t ESTIMATED_SIZE = 55; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_lock_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_lock_response"); } #endif bool assumed_state{false}; bool supports_open{false}; @@ -1724,7 +1724,7 @@ class LockStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 59; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "lock_state_response"; } + const LogString *message_name() const override { return LOG_STR("lock_state_response"); } #endif enums::LockState state{}; void encode(ProtoWriteBuffer &buffer) const; @@ -1740,7 +1740,7 @@ class LockCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 60; static constexpr uint8_t ESTIMATED_SIZE = 22; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "lock_command_request"; } + const LogString *message_name() const override { return LOG_STR("lock_command_request"); } #endif enums::LockCommand command{}; bool has_code{false}; @@ -1761,7 +1761,7 @@ class ListEntitiesButtonResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 61; static constexpr uint8_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_button_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_button_response"); } #endif StringRef device_class{}; void encode(ProtoWriteBuffer &buffer) const; @@ -1777,7 +1777,7 @@ class ButtonCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 62; static constexpr uint8_t ESTIMATED_SIZE = 9; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "button_command_request"; } + const LogString *message_name() const override { return LOG_STR("button_command_request"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1809,7 +1809,7 @@ class ListEntitiesMediaPlayerResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 63; static constexpr uint8_t ESTIMATED_SIZE = 80; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_media_player_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_media_player_response"); } #endif bool supports_pause{false}; std::vector supported_formats{}; @@ -1827,7 +1827,7 @@ class MediaPlayerStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 64; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "media_player_state_response"; } + const LogString *message_name() const override { return LOG_STR("media_player_state_response"); } #endif enums::MediaPlayerState state{}; float volume{0.0f}; @@ -1845,7 +1845,7 @@ class MediaPlayerCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 65; static constexpr uint8_t ESTIMATED_SIZE = 35; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "media_player_command_request"; } + const LogString *message_name() const override { return LOG_STR("media_player_command_request"); } #endif bool has_command{false}; enums::MediaPlayerCommand command{}; @@ -1871,7 +1871,7 @@ class SubscribeBluetoothLEAdvertisementsRequest final : public ProtoDecodableMes static constexpr uint8_t MESSAGE_TYPE = 66; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_bluetooth_le_advertisements_request"; } + const LogString *message_name() const override { return LOG_STR("subscribe_bluetooth_le_advertisements_request"); } #endif uint32_t flags{0}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1901,7 +1901,7 @@ class BluetoothLERawAdvertisementsResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 93; static constexpr uint8_t ESTIMATED_SIZE = 136; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_le_raw_advertisements_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_le_raw_advertisements_response"); } #endif std::array advertisements{}; uint16_t advertisements_len{0}; @@ -1918,7 +1918,7 @@ class BluetoothDeviceRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 68; static constexpr uint8_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_device_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_device_request"); } #endif uint64_t address{0}; enums::BluetoothDeviceRequestType request_type{}; @@ -1936,7 +1936,7 @@ class BluetoothDeviceConnectionResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 69; static constexpr uint8_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_device_connection_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_device_connection_response"); } #endif uint64_t address{0}; bool connected{false}; @@ -1955,7 +1955,7 @@ class BluetoothGATTGetServicesRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 70; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_get_services_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_get_services_request"); } #endif uint64_t address{0}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2012,7 +2012,7 @@ class BluetoothGATTGetServicesResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 71; static constexpr uint8_t ESTIMATED_SIZE = 38; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_get_services_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_get_services_response"); } #endif uint64_t address{0}; std::vector services{}; @@ -2029,7 +2029,7 @@ class BluetoothGATTGetServicesDoneResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 72; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_get_services_done_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_get_services_done_response"); } #endif uint64_t address{0}; void encode(ProtoWriteBuffer &buffer) const; @@ -2045,7 +2045,7 @@ class BluetoothGATTReadRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 73; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_read_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_read_request"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2061,7 +2061,7 @@ class BluetoothGATTReadResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 74; static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_read_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_read_response"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2084,7 +2084,7 @@ class BluetoothGATTWriteRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 75; static constexpr uint8_t ESTIMATED_SIZE = 29; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_write_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_write_request"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2104,7 +2104,7 @@ class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 76; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_read_descriptor_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_read_descriptor_request"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2120,7 +2120,7 @@ class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 77; static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_write_descriptor_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_write_descriptor_request"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2139,7 +2139,7 @@ class BluetoothGATTNotifyRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 78; static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_notify_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_notify_request"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2156,7 +2156,7 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 79; static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_notify_data_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_notify_data_response"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2179,7 +2179,7 @@ class BluetoothConnectionsFreeResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 81; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_connections_free_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_connections_free_response"); } #endif uint32_t free{0}; uint32_t limit{0}; @@ -2197,7 +2197,7 @@ class BluetoothGATTErrorResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 82; static constexpr uint8_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_error_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_error_response"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2215,7 +2215,7 @@ class BluetoothGATTWriteResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 83; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_write_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_write_response"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2232,7 +2232,7 @@ class BluetoothGATTNotifyResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 84; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_notify_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_notify_response"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2249,7 +2249,7 @@ class BluetoothDevicePairingResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 85; static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_device_pairing_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_device_pairing_response"); } #endif uint64_t address{0}; bool paired{false}; @@ -2267,7 +2267,7 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 86; static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_device_unpairing_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_device_unpairing_response"); } #endif uint64_t address{0}; bool success{false}; @@ -2285,7 +2285,7 @@ class BluetoothDeviceClearCacheResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 88; static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_device_clear_cache_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_device_clear_cache_response"); } #endif uint64_t address{0}; bool success{false}; @@ -2303,7 +2303,7 @@ class BluetoothScannerStateResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 126; static constexpr uint8_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_scanner_state_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_scanner_state_response"); } #endif enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; @@ -2321,7 +2321,7 @@ class BluetoothScannerSetModeRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 127; static constexpr uint8_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_scanner_set_mode_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_scanner_set_mode_request"); } #endif enums::BluetoothScannerMode mode{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2338,7 +2338,7 @@ class SubscribeVoiceAssistantRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 89; static constexpr uint8_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_voice_assistant_request"; } + const LogString *message_name() const override { return LOG_STR("subscribe_voice_assistant_request"); } #endif bool subscribe{false}; uint32_t flags{0}; @@ -2367,7 +2367,7 @@ class VoiceAssistantRequest final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 90; static constexpr uint8_t ESTIMATED_SIZE = 41; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_request"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_request"); } #endif bool start{false}; StringRef conversation_id{}; @@ -2387,7 +2387,7 @@ class VoiceAssistantResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 91; static constexpr uint8_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_response"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_response"); } #endif uint32_t port{0}; bool error{false}; @@ -2414,7 +2414,7 @@ class VoiceAssistantEventResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 92; static constexpr uint8_t ESTIMATED_SIZE = 36; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_event_response"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_event_response"); } #endif enums::VoiceAssistantEvent event_type{}; std::vector data{}; @@ -2431,7 +2431,7 @@ class VoiceAssistantAudio final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 106; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_audio"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_audio"); } #endif const uint8_t *data{nullptr}; uint16_t data_len{0}; @@ -2451,7 +2451,7 @@ class VoiceAssistantTimerEventResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 115; static constexpr uint8_t ESTIMATED_SIZE = 30; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_timer_event_response"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_timer_event_response"); } #endif enums::VoiceAssistantTimerEvent event_type{}; StringRef timer_id{}; @@ -2472,7 +2472,7 @@ class VoiceAssistantAnnounceRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 119; static constexpr uint8_t ESTIMATED_SIZE = 29; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_announce_request"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_announce_request"); } #endif StringRef media_id{}; StringRef text{}; @@ -2491,7 +2491,7 @@ class VoiceAssistantAnnounceFinished final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 120; static constexpr uint8_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_announce_finished"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_announce_finished"); } #endif bool success{false}; void encode(ProtoWriteBuffer &buffer) const; @@ -2537,7 +2537,7 @@ class VoiceAssistantConfigurationRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 121; static constexpr uint8_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_configuration_request"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_configuration_request"); } #endif std::vector external_wake_words{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2552,7 +2552,7 @@ class VoiceAssistantConfigurationResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 122; static constexpr uint8_t ESTIMATED_SIZE = 56; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_configuration_response"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_configuration_response"); } #endif std::vector available_wake_words{}; const std::vector *active_wake_words{}; @@ -2570,7 +2570,7 @@ class VoiceAssistantSetConfiguration final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 123; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_set_configuration"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_set_configuration"); } #endif std::vector active_wake_words{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2587,7 +2587,7 @@ class ListEntitiesAlarmControlPanelResponse final : public InfoResponseProtoMess static constexpr uint8_t MESSAGE_TYPE = 94; static constexpr uint8_t ESTIMATED_SIZE = 48; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_alarm_control_panel_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_alarm_control_panel_response"); } #endif uint32_t supported_features{0}; bool requires_code{false}; @@ -2605,7 +2605,7 @@ class AlarmControlPanelStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 95; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "alarm_control_panel_state_response"; } + const LogString *message_name() const override { return LOG_STR("alarm_control_panel_state_response"); } #endif enums::AlarmControlPanelState state{}; void encode(ProtoWriteBuffer &buffer) const; @@ -2621,7 +2621,7 @@ class AlarmControlPanelCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 96; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "alarm_control_panel_command_request"; } + const LogString *message_name() const override { return LOG_STR("alarm_control_panel_command_request"); } #endif enums::AlarmControlPanelStateCommand command{}; StringRef code{}; @@ -2641,7 +2641,7 @@ class ListEntitiesTextResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 97; static constexpr uint8_t ESTIMATED_SIZE = 59; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_text_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_text_response"); } #endif uint32_t min_length{0}; uint32_t max_length{0}; @@ -2660,7 +2660,7 @@ class TextStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 98; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "text_state_response"; } + const LogString *message_name() const override { return LOG_STR("text_state_response"); } #endif StringRef state{}; bool missing_state{false}; @@ -2677,7 +2677,7 @@ class TextCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 99; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "text_command_request"; } + const LogString *message_name() const override { return LOG_STR("text_command_request"); } #endif StringRef state{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2696,7 +2696,7 @@ class ListEntitiesDateResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 100; static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_date_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_date_response"); } #endif void encode(ProtoWriteBuffer &buffer) const; uint32_t calculate_size() const; @@ -2711,7 +2711,7 @@ class DateStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 101; static constexpr uint8_t ESTIMATED_SIZE = 23; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "date_state_response"; } + const LogString *message_name() const override { return LOG_STR("date_state_response"); } #endif bool missing_state{false}; uint32_t year{0}; @@ -2730,7 +2730,7 @@ class DateCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 102; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "date_command_request"; } + const LogString *message_name() const override { return LOG_STR("date_command_request"); } #endif uint32_t year{0}; uint32_t month{0}; @@ -2750,7 +2750,7 @@ class ListEntitiesTimeResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 103; static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_time_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_time_response"); } #endif void encode(ProtoWriteBuffer &buffer) const; uint32_t calculate_size() const; @@ -2765,7 +2765,7 @@ class TimeStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 104; static constexpr uint8_t ESTIMATED_SIZE = 23; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "time_state_response"; } + const LogString *message_name() const override { return LOG_STR("time_state_response"); } #endif bool missing_state{false}; uint32_t hour{0}; @@ -2784,7 +2784,7 @@ class TimeCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 105; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "time_command_request"; } + const LogString *message_name() const override { return LOG_STR("time_command_request"); } #endif uint32_t hour{0}; uint32_t minute{0}; @@ -2804,7 +2804,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 107; static constexpr uint8_t ESTIMATED_SIZE = 67; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_event_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_event_response"); } #endif StringRef device_class{}; const FixedVector *event_types{}; @@ -2821,7 +2821,7 @@ class EventResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 108; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "event_response"; } + const LogString *message_name() const override { return LOG_STR("event_response"); } #endif StringRef event_type{}; void encode(ProtoWriteBuffer &buffer) const; @@ -2839,7 +2839,7 @@ class ListEntitiesValveResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 109; static constexpr uint8_t ESTIMATED_SIZE = 55; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_valve_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_valve_response"); } #endif StringRef device_class{}; bool assumed_state{false}; @@ -2858,7 +2858,7 @@ class ValveStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 110; static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "valve_state_response"; } + const LogString *message_name() const override { return LOG_STR("valve_state_response"); } #endif float position{0.0f}; enums::ValveOperation current_operation{}; @@ -2875,7 +2875,7 @@ class ValveCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 111; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "valve_command_request"; } + const LogString *message_name() const override { return LOG_STR("valve_command_request"); } #endif bool has_position{false}; float position{0.0f}; @@ -2895,7 +2895,7 @@ class ListEntitiesDateTimeResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 112; static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_date_time_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_date_time_response"); } #endif void encode(ProtoWriteBuffer &buffer) const; uint32_t calculate_size() const; @@ -2910,7 +2910,7 @@ class DateTimeStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 113; static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "date_time_state_response"; } + const LogString *message_name() const override { return LOG_STR("date_time_state_response"); } #endif bool missing_state{false}; uint32_t epoch_seconds{0}; @@ -2927,7 +2927,7 @@ class DateTimeCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 114; static constexpr uint8_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "date_time_command_request"; } + const LogString *message_name() const override { return LOG_STR("date_time_command_request"); } #endif uint32_t epoch_seconds{0}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2945,7 +2945,7 @@ class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 116; static constexpr uint8_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_update_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_update_response"); } #endif StringRef device_class{}; void encode(ProtoWriteBuffer &buffer) const; @@ -2961,7 +2961,7 @@ class UpdateStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 117; static constexpr uint8_t ESTIMATED_SIZE = 65; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "update_state_response"; } + const LogString *message_name() const override { return LOG_STR("update_state_response"); } #endif bool missing_state{false}; bool in_progress{false}; @@ -2985,7 +2985,7 @@ class UpdateCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 118; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "update_command_request"; } + const LogString *message_name() const override { return LOG_STR("update_command_request"); } #endif enums::UpdateCommand command{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -3003,7 +3003,7 @@ class ZWaveProxyFrame final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 128; static constexpr uint8_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "z_wave_proxy_frame"; } + const LogString *message_name() const override { return LOG_STR("z_wave_proxy_frame"); } #endif const uint8_t *data{nullptr}; uint16_t data_len{0}; @@ -3021,7 +3021,7 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 129; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "z_wave_proxy_request"; } + const LogString *message_name() const override { return LOG_STR("z_wave_proxy_request"); } #endif enums::ZWaveProxyRequestType type{}; const uint8_t *data{nullptr}; @@ -3043,7 +3043,7 @@ class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 135; static constexpr uint8_t ESTIMATED_SIZE = 44; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_infrared_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_infrared_response"); } #endif uint32_t capabilities{0}; void encode(ProtoWriteBuffer &buffer) const; @@ -3061,7 +3061,7 @@ class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 136; static constexpr uint8_t ESTIMATED_SIZE = 220; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "infrared_rf_transmit_raw_timings_request"; } + const LogString *message_name() const override { return LOG_STR("infrared_rf_transmit_raw_timings_request"); } #endif #ifdef USE_DEVICES uint32_t device_id{0}; @@ -3086,7 +3086,7 @@ class InfraredRFReceiveEvent final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 137; static constexpr uint8_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "infrared_rf_receive_event"; } + const LogString *message_name() const override { return LOG_STR("infrared_rf_receive_event"); } #endif #ifdef USE_DEVICES uint32_t device_id{0}; @@ -3108,7 +3108,7 @@ class SerialProxyConfigureRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 138; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_configure_request"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_configure_request"); } #endif uint32_t instance{0}; uint32_t baudrate{0}; @@ -3128,7 +3128,7 @@ class SerialProxyDataReceived final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 139; static constexpr uint8_t ESTIMATED_SIZE = 23; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_data_received"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_data_received"); } #endif uint32_t instance{0}; const uint8_t *data_ptr_{nullptr}; @@ -3150,7 +3150,7 @@ class SerialProxyWriteRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 140; static constexpr uint8_t ESTIMATED_SIZE = 23; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_write_request"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_write_request"); } #endif uint32_t instance{0}; const uint8_t *data{nullptr}; @@ -3168,7 +3168,7 @@ class SerialProxySetModemPinsRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 141; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_set_modem_pins_request"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_set_modem_pins_request"); } #endif uint32_t instance{0}; uint32_t line_states{0}; @@ -3184,7 +3184,7 @@ class SerialProxyGetModemPinsRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 142; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_get_modem_pins_request"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_get_modem_pins_request"); } #endif uint32_t instance{0}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -3199,7 +3199,7 @@ class SerialProxyGetModemPinsResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 143; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_get_modem_pins_response"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_get_modem_pins_response"); } #endif uint32_t instance{0}; uint32_t line_states{0}; @@ -3216,7 +3216,7 @@ class SerialProxyRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 144; static constexpr uint8_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_request"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_request"); } #endif uint32_t instance{0}; enums::SerialProxyRequestType type{}; @@ -3232,7 +3232,7 @@ class SerialProxyRequestResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 147; static constexpr uint8_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_request_response"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_request_response"); } #endif uint32_t instance{0}; enums::SerialProxyRequestType type{}; @@ -3253,7 +3253,7 @@ class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 145; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_set_connection_params_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_set_connection_params_request"); } #endif uint64_t address{0}; uint32_t min_interval{0}; @@ -3272,7 +3272,7 @@ class BluetoothSetConnectionParamsResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 146; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_set_connection_params_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_set_connection_params_response"); } #endif uint64_t address{0}; int32_t error{0}; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 5a53f0281f..a11f3b231e 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -2,6 +2,7 @@ // See script/api_protobuf/api_protobuf.py #include "api_pb2.h" #include "esphome/core/helpers.h" +#include "esphome/core/progmem.h" #include @@ -9,6 +10,21 @@ namespace esphome::api { +#ifdef USE_ESP8266 +// Out-of-line to avoid inlining strlen_P/memcpy_P at every call site +void DumpBuffer::append_p_esp8266(const char *str) { + size_t len = strlen_P(str); + size_t space = CAPACITY - 1 - pos_; + if (len > space) + len = space; + if (len > 0) { + memcpy_P(buf_ + pos_, str, len); + pos_ += len; + buf_[pos_] = '\0'; + } +} +#endif + // Helper function to append a quoted string, handling empty StringRef static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) { out.append("'"); @@ -19,8 +35,9 @@ static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) { } // Common helpers for dump_field functions +// field_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) static inline void append_field_prefix(DumpBuffer &out, const char *field_name, int indent) { - out.append(indent, ' ').append(field_name).append(": "); + out.append(indent, ' ').append_p(field_name).append(": "); } static inline void append_uint(DumpBuffer &out, uint32_t value) { @@ -28,10 +45,11 @@ static inline void append_uint(DumpBuffer &out, uint32_t value) { } // RAII helper for message dump formatting +// message_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) class MessageDumpHelper { public: MessageDumpHelper(DumpBuffer &out, const char *message_name) : out_(out) { - out_.append(message_name); + out_.append_p(message_name); out_.append(" {\n"); } ~MessageDumpHelper() { out_.append(" }"); } @@ -41,6 +59,10 @@ class MessageDumpHelper { }; // Helper functions to reduce code duplication in dump methods +// field_name parameters are PROGMEM pointers (flash on ESP8266, regular pointers on other platforms) +// Not all overloads are used in every build (depends on enabled components) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" static void dump_field(DumpBuffer &out, const char *field_name, int32_t value, int indent = 2) { append_field_prefix(out, field_name, indent); out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRId32 "\n", value)); @@ -85,56 +107,59 @@ static void dump_field(DumpBuffer &out, const char *field_name, const char *valu out.append("\n"); } +// proto_enum_to_string returns PROGMEM pointers, so use append_p template static void dump_field(DumpBuffer &out, const char *field_name, T value, int indent = 2) { append_field_prefix(out, field_name, indent); - out.append(proto_enum_to_string(value)); + out.append_p(proto_enum_to_string(value)); out.append("\n"); } // Helper for bytes fields - uses stack buffer to avoid heap allocation // Buffer sized for 160 bytes of data (480 chars with separators) to fit typical log buffer +// field_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint8_t *data, size_t len, int indent = 2) { char hex_buf[format_hex_pretty_size(160)]; append_field_prefix(out, field_name, indent); format_hex_pretty_to(hex_buf, data, len); out.append(hex_buf).append("\n"); } +#pragma GCC diagnostic pop template<> const char *proto_enum_to_string(enums::SerialProxyPortType value) { switch (value) { case enums::SERIAL_PROXY_PORT_TYPE_TTL: - return "SERIAL_PROXY_PORT_TYPE_TTL"; + return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_TTL"); case enums::SERIAL_PROXY_PORT_TYPE_RS232: - return "SERIAL_PROXY_PORT_TYPE_RS232"; + return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_RS232"); case enums::SERIAL_PROXY_PORT_TYPE_RS485: - return "SERIAL_PROXY_PORT_TYPE_RS485"; + return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_RS485"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::EntityCategory value) { switch (value) { case enums::ENTITY_CATEGORY_NONE: - return "ENTITY_CATEGORY_NONE"; + return ESPHOME_PSTR("ENTITY_CATEGORY_NONE"); case enums::ENTITY_CATEGORY_CONFIG: - return "ENTITY_CATEGORY_CONFIG"; + return ESPHOME_PSTR("ENTITY_CATEGORY_CONFIG"); case enums::ENTITY_CATEGORY_DIAGNOSTIC: - return "ENTITY_CATEGORY_DIAGNOSTIC"; + return ESPHOME_PSTR("ENTITY_CATEGORY_DIAGNOSTIC"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #ifdef USE_COVER template<> const char *proto_enum_to_string(enums::CoverOperation value) { switch (value) { case enums::COVER_OPERATION_IDLE: - return "COVER_OPERATION_IDLE"; + return ESPHOME_PSTR("COVER_OPERATION_IDLE"); case enums::COVER_OPERATION_IS_OPENING: - return "COVER_OPERATION_IS_OPENING"; + return ESPHOME_PSTR("COVER_OPERATION_IS_OPENING"); case enums::COVER_OPERATION_IS_CLOSING: - return "COVER_OPERATION_IS_CLOSING"; + return ESPHOME_PSTR("COVER_OPERATION_IS_CLOSING"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -142,11 +167,11 @@ template<> const char *proto_enum_to_string(enums::CoverO template<> const char *proto_enum_to_string(enums::FanDirection value) { switch (value) { case enums::FAN_DIRECTION_FORWARD: - return "FAN_DIRECTION_FORWARD"; + return ESPHOME_PSTR("FAN_DIRECTION_FORWARD"); case enums::FAN_DIRECTION_REVERSE: - return "FAN_DIRECTION_REVERSE"; + return ESPHOME_PSTR("FAN_DIRECTION_REVERSE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -154,29 +179,29 @@ template<> const char *proto_enum_to_string(enums::FanDirec template<> const char *proto_enum_to_string(enums::ColorMode value) { switch (value) { case enums::COLOR_MODE_UNKNOWN: - return "COLOR_MODE_UNKNOWN"; + return ESPHOME_PSTR("COLOR_MODE_UNKNOWN"); case enums::COLOR_MODE_ON_OFF: - return "COLOR_MODE_ON_OFF"; + return ESPHOME_PSTR("COLOR_MODE_ON_OFF"); case enums::COLOR_MODE_LEGACY_BRIGHTNESS: - return "COLOR_MODE_LEGACY_BRIGHTNESS"; + return ESPHOME_PSTR("COLOR_MODE_LEGACY_BRIGHTNESS"); case enums::COLOR_MODE_BRIGHTNESS: - return "COLOR_MODE_BRIGHTNESS"; + return ESPHOME_PSTR("COLOR_MODE_BRIGHTNESS"); case enums::COLOR_MODE_WHITE: - return "COLOR_MODE_WHITE"; + return ESPHOME_PSTR("COLOR_MODE_WHITE"); case enums::COLOR_MODE_COLOR_TEMPERATURE: - return "COLOR_MODE_COLOR_TEMPERATURE"; + return ESPHOME_PSTR("COLOR_MODE_COLOR_TEMPERATURE"); case enums::COLOR_MODE_COLD_WARM_WHITE: - return "COLOR_MODE_COLD_WARM_WHITE"; + return ESPHOME_PSTR("COLOR_MODE_COLD_WARM_WHITE"); case enums::COLOR_MODE_RGB: - return "COLOR_MODE_RGB"; + return ESPHOME_PSTR("COLOR_MODE_RGB"); case enums::COLOR_MODE_RGB_WHITE: - return "COLOR_MODE_RGB_WHITE"; + return ESPHOME_PSTR("COLOR_MODE_RGB_WHITE"); case enums::COLOR_MODE_RGB_COLOR_TEMPERATURE: - return "COLOR_MODE_RGB_COLOR_TEMPERATURE"; + return ESPHOME_PSTR("COLOR_MODE_RGB_COLOR_TEMPERATURE"); case enums::COLOR_MODE_RGB_COLD_WARM_WHITE: - return "COLOR_MODE_RGB_COLD_WARM_WHITE"; + return ESPHOME_PSTR("COLOR_MODE_RGB_COLD_WARM_WHITE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -184,91 +209,91 @@ template<> const char *proto_enum_to_string(enums::ColorMode v template<> const char *proto_enum_to_string(enums::SensorStateClass value) { switch (value) { case enums::STATE_CLASS_NONE: - return "STATE_CLASS_NONE"; + return ESPHOME_PSTR("STATE_CLASS_NONE"); case enums::STATE_CLASS_MEASUREMENT: - return "STATE_CLASS_MEASUREMENT"; + return ESPHOME_PSTR("STATE_CLASS_MEASUREMENT"); case enums::STATE_CLASS_TOTAL_INCREASING: - return "STATE_CLASS_TOTAL_INCREASING"; + return ESPHOME_PSTR("STATE_CLASS_TOTAL_INCREASING"); case enums::STATE_CLASS_TOTAL: - return "STATE_CLASS_TOTAL"; + return ESPHOME_PSTR("STATE_CLASS_TOTAL"); case enums::STATE_CLASS_MEASUREMENT_ANGLE: - return "STATE_CLASS_MEASUREMENT_ANGLE"; + return ESPHOME_PSTR("STATE_CLASS_MEASUREMENT_ANGLE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif template<> const char *proto_enum_to_string(enums::LogLevel value) { switch (value) { case enums::LOG_LEVEL_NONE: - return "LOG_LEVEL_NONE"; + return ESPHOME_PSTR("LOG_LEVEL_NONE"); case enums::LOG_LEVEL_ERROR: - return "LOG_LEVEL_ERROR"; + return ESPHOME_PSTR("LOG_LEVEL_ERROR"); case enums::LOG_LEVEL_WARN: - return "LOG_LEVEL_WARN"; + return ESPHOME_PSTR("LOG_LEVEL_WARN"); case enums::LOG_LEVEL_INFO: - return "LOG_LEVEL_INFO"; + return ESPHOME_PSTR("LOG_LEVEL_INFO"); case enums::LOG_LEVEL_CONFIG: - return "LOG_LEVEL_CONFIG"; + return ESPHOME_PSTR("LOG_LEVEL_CONFIG"); case enums::LOG_LEVEL_DEBUG: - return "LOG_LEVEL_DEBUG"; + return ESPHOME_PSTR("LOG_LEVEL_DEBUG"); case enums::LOG_LEVEL_VERBOSE: - return "LOG_LEVEL_VERBOSE"; + return ESPHOME_PSTR("LOG_LEVEL_VERBOSE"); case enums::LOG_LEVEL_VERY_VERBOSE: - return "LOG_LEVEL_VERY_VERBOSE"; + return ESPHOME_PSTR("LOG_LEVEL_VERY_VERBOSE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::DSTRuleType value) { switch (value) { case enums::DST_RULE_TYPE_NONE: - return "DST_RULE_TYPE_NONE"; + return ESPHOME_PSTR("DST_RULE_TYPE_NONE"); case enums::DST_RULE_TYPE_MONTH_WEEK_DAY: - return "DST_RULE_TYPE_MONTH_WEEK_DAY"; + return ESPHOME_PSTR("DST_RULE_TYPE_MONTH_WEEK_DAY"); case enums::DST_RULE_TYPE_JULIAN_NO_LEAP: - return "DST_RULE_TYPE_JULIAN_NO_LEAP"; + return ESPHOME_PSTR("DST_RULE_TYPE_JULIAN_NO_LEAP"); case enums::DST_RULE_TYPE_DAY_OF_YEAR: - return "DST_RULE_TYPE_DAY_OF_YEAR"; + return ESPHOME_PSTR("DST_RULE_TYPE_DAY_OF_YEAR"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #ifdef USE_API_USER_DEFINED_ACTIONS template<> const char *proto_enum_to_string(enums::ServiceArgType value) { switch (value) { case enums::SERVICE_ARG_TYPE_BOOL: - return "SERVICE_ARG_TYPE_BOOL"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_BOOL"); case enums::SERVICE_ARG_TYPE_INT: - return "SERVICE_ARG_TYPE_INT"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_INT"); case enums::SERVICE_ARG_TYPE_FLOAT: - return "SERVICE_ARG_TYPE_FLOAT"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_FLOAT"); case enums::SERVICE_ARG_TYPE_STRING: - return "SERVICE_ARG_TYPE_STRING"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_STRING"); case enums::SERVICE_ARG_TYPE_BOOL_ARRAY: - return "SERVICE_ARG_TYPE_BOOL_ARRAY"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_BOOL_ARRAY"); case enums::SERVICE_ARG_TYPE_INT_ARRAY: - return "SERVICE_ARG_TYPE_INT_ARRAY"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_INT_ARRAY"); case enums::SERVICE_ARG_TYPE_FLOAT_ARRAY: - return "SERVICE_ARG_TYPE_FLOAT_ARRAY"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_FLOAT_ARRAY"); case enums::SERVICE_ARG_TYPE_STRING_ARRAY: - return "SERVICE_ARG_TYPE_STRING_ARRAY"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_STRING_ARRAY"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::SupportsResponseType value) { switch (value) { case enums::SUPPORTS_RESPONSE_NONE: - return "SUPPORTS_RESPONSE_NONE"; + return ESPHOME_PSTR("SUPPORTS_RESPONSE_NONE"); case enums::SUPPORTS_RESPONSE_OPTIONAL: - return "SUPPORTS_RESPONSE_OPTIONAL"; + return ESPHOME_PSTR("SUPPORTS_RESPONSE_OPTIONAL"); case enums::SUPPORTS_RESPONSE_ONLY: - return "SUPPORTS_RESPONSE_ONLY"; + return ESPHOME_PSTR("SUPPORTS_RESPONSE_ONLY"); case enums::SUPPORTS_RESPONSE_STATUS: - return "SUPPORTS_RESPONSE_STATUS"; + return ESPHOME_PSTR("SUPPORTS_RESPONSE_STATUS"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -276,103 +301,103 @@ template<> const char *proto_enum_to_string(enums:: template<> const char *proto_enum_to_string(enums::ClimateMode value) { switch (value) { case enums::CLIMATE_MODE_OFF: - return "CLIMATE_MODE_OFF"; + return ESPHOME_PSTR("CLIMATE_MODE_OFF"); case enums::CLIMATE_MODE_HEAT_COOL: - return "CLIMATE_MODE_HEAT_COOL"; + return ESPHOME_PSTR("CLIMATE_MODE_HEAT_COOL"); case enums::CLIMATE_MODE_COOL: - return "CLIMATE_MODE_COOL"; + return ESPHOME_PSTR("CLIMATE_MODE_COOL"); case enums::CLIMATE_MODE_HEAT: - return "CLIMATE_MODE_HEAT"; + return ESPHOME_PSTR("CLIMATE_MODE_HEAT"); case enums::CLIMATE_MODE_FAN_ONLY: - return "CLIMATE_MODE_FAN_ONLY"; + return ESPHOME_PSTR("CLIMATE_MODE_FAN_ONLY"); case enums::CLIMATE_MODE_DRY: - return "CLIMATE_MODE_DRY"; + return ESPHOME_PSTR("CLIMATE_MODE_DRY"); case enums::CLIMATE_MODE_AUTO: - return "CLIMATE_MODE_AUTO"; + return ESPHOME_PSTR("CLIMATE_MODE_AUTO"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::ClimateFanMode value) { switch (value) { case enums::CLIMATE_FAN_ON: - return "CLIMATE_FAN_ON"; + return ESPHOME_PSTR("CLIMATE_FAN_ON"); case enums::CLIMATE_FAN_OFF: - return "CLIMATE_FAN_OFF"; + return ESPHOME_PSTR("CLIMATE_FAN_OFF"); case enums::CLIMATE_FAN_AUTO: - return "CLIMATE_FAN_AUTO"; + return ESPHOME_PSTR("CLIMATE_FAN_AUTO"); case enums::CLIMATE_FAN_LOW: - return "CLIMATE_FAN_LOW"; + return ESPHOME_PSTR("CLIMATE_FAN_LOW"); case enums::CLIMATE_FAN_MEDIUM: - return "CLIMATE_FAN_MEDIUM"; + return ESPHOME_PSTR("CLIMATE_FAN_MEDIUM"); case enums::CLIMATE_FAN_HIGH: - return "CLIMATE_FAN_HIGH"; + return ESPHOME_PSTR("CLIMATE_FAN_HIGH"); case enums::CLIMATE_FAN_MIDDLE: - return "CLIMATE_FAN_MIDDLE"; + return ESPHOME_PSTR("CLIMATE_FAN_MIDDLE"); case enums::CLIMATE_FAN_FOCUS: - return "CLIMATE_FAN_FOCUS"; + return ESPHOME_PSTR("CLIMATE_FAN_FOCUS"); case enums::CLIMATE_FAN_DIFFUSE: - return "CLIMATE_FAN_DIFFUSE"; + return ESPHOME_PSTR("CLIMATE_FAN_DIFFUSE"); case enums::CLIMATE_FAN_QUIET: - return "CLIMATE_FAN_QUIET"; + return ESPHOME_PSTR("CLIMATE_FAN_QUIET"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::ClimateSwingMode value) { switch (value) { case enums::CLIMATE_SWING_OFF: - return "CLIMATE_SWING_OFF"; + return ESPHOME_PSTR("CLIMATE_SWING_OFF"); case enums::CLIMATE_SWING_BOTH: - return "CLIMATE_SWING_BOTH"; + return ESPHOME_PSTR("CLIMATE_SWING_BOTH"); case enums::CLIMATE_SWING_VERTICAL: - return "CLIMATE_SWING_VERTICAL"; + return ESPHOME_PSTR("CLIMATE_SWING_VERTICAL"); case enums::CLIMATE_SWING_HORIZONTAL: - return "CLIMATE_SWING_HORIZONTAL"; + return ESPHOME_PSTR("CLIMATE_SWING_HORIZONTAL"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::ClimateAction value) { switch (value) { case enums::CLIMATE_ACTION_OFF: - return "CLIMATE_ACTION_OFF"; + return ESPHOME_PSTR("CLIMATE_ACTION_OFF"); case enums::CLIMATE_ACTION_COOLING: - return "CLIMATE_ACTION_COOLING"; + return ESPHOME_PSTR("CLIMATE_ACTION_COOLING"); case enums::CLIMATE_ACTION_HEATING: - return "CLIMATE_ACTION_HEATING"; + return ESPHOME_PSTR("CLIMATE_ACTION_HEATING"); case enums::CLIMATE_ACTION_IDLE: - return "CLIMATE_ACTION_IDLE"; + return ESPHOME_PSTR("CLIMATE_ACTION_IDLE"); case enums::CLIMATE_ACTION_DRYING: - return "CLIMATE_ACTION_DRYING"; + return ESPHOME_PSTR("CLIMATE_ACTION_DRYING"); case enums::CLIMATE_ACTION_FAN: - return "CLIMATE_ACTION_FAN"; + return ESPHOME_PSTR("CLIMATE_ACTION_FAN"); case enums::CLIMATE_ACTION_DEFROSTING: - return "CLIMATE_ACTION_DEFROSTING"; + return ESPHOME_PSTR("CLIMATE_ACTION_DEFROSTING"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::ClimatePreset value) { switch (value) { case enums::CLIMATE_PRESET_NONE: - return "CLIMATE_PRESET_NONE"; + return ESPHOME_PSTR("CLIMATE_PRESET_NONE"); case enums::CLIMATE_PRESET_HOME: - return "CLIMATE_PRESET_HOME"; + return ESPHOME_PSTR("CLIMATE_PRESET_HOME"); case enums::CLIMATE_PRESET_AWAY: - return "CLIMATE_PRESET_AWAY"; + return ESPHOME_PSTR("CLIMATE_PRESET_AWAY"); case enums::CLIMATE_PRESET_BOOST: - return "CLIMATE_PRESET_BOOST"; + return ESPHOME_PSTR("CLIMATE_PRESET_BOOST"); case enums::CLIMATE_PRESET_COMFORT: - return "CLIMATE_PRESET_COMFORT"; + return ESPHOME_PSTR("CLIMATE_PRESET_COMFORT"); case enums::CLIMATE_PRESET_ECO: - return "CLIMATE_PRESET_ECO"; + return ESPHOME_PSTR("CLIMATE_PRESET_ECO"); case enums::CLIMATE_PRESET_SLEEP: - return "CLIMATE_PRESET_SLEEP"; + return ESPHOME_PSTR("CLIMATE_PRESET_SLEEP"); case enums::CLIMATE_PRESET_ACTIVITY: - return "CLIMATE_PRESET_ACTIVITY"; + return ESPHOME_PSTR("CLIMATE_PRESET_ACTIVITY"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -380,21 +405,21 @@ template<> const char *proto_enum_to_string(enums::Climate template<> const char *proto_enum_to_string(enums::WaterHeaterMode value) { switch (value) { case enums::WATER_HEATER_MODE_OFF: - return "WATER_HEATER_MODE_OFF"; + return ESPHOME_PSTR("WATER_HEATER_MODE_OFF"); case enums::WATER_HEATER_MODE_ECO: - return "WATER_HEATER_MODE_ECO"; + return ESPHOME_PSTR("WATER_HEATER_MODE_ECO"); case enums::WATER_HEATER_MODE_ELECTRIC: - return "WATER_HEATER_MODE_ELECTRIC"; + return ESPHOME_PSTR("WATER_HEATER_MODE_ELECTRIC"); case enums::WATER_HEATER_MODE_PERFORMANCE: - return "WATER_HEATER_MODE_PERFORMANCE"; + return ESPHOME_PSTR("WATER_HEATER_MODE_PERFORMANCE"); case enums::WATER_HEATER_MODE_HIGH_DEMAND: - return "WATER_HEATER_MODE_HIGH_DEMAND"; + return ESPHOME_PSTR("WATER_HEATER_MODE_HIGH_DEMAND"); case enums::WATER_HEATER_MODE_HEAT_PUMP: - return "WATER_HEATER_MODE_HEAT_PUMP"; + return ESPHOME_PSTR("WATER_HEATER_MODE_HEAT_PUMP"); case enums::WATER_HEATER_MODE_GAS: - return "WATER_HEATER_MODE_GAS"; + return ESPHOME_PSTR("WATER_HEATER_MODE_GAS"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -402,36 +427,36 @@ template<> const char *proto_enum_to_string(enums::WaterHeaterCommandHasField value) { switch (value) { case enums::WATER_HEATER_COMMAND_HAS_NONE: - return "WATER_HEATER_COMMAND_HAS_NONE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_NONE"); case enums::WATER_HEATER_COMMAND_HAS_MODE: - return "WATER_HEATER_COMMAND_HAS_MODE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_MODE"); case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE: - return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE"); case enums::WATER_HEATER_COMMAND_HAS_STATE: - return "WATER_HEATER_COMMAND_HAS_STATE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_STATE"); case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW: - return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW"); case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH: - return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH"); case enums::WATER_HEATER_COMMAND_HAS_ON_STATE: - return "WATER_HEATER_COMMAND_HAS_ON_STATE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_ON_STATE"); case enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE: - return "WATER_HEATER_COMMAND_HAS_AWAY_STATE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_AWAY_STATE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #ifdef USE_NUMBER template<> const char *proto_enum_to_string(enums::NumberMode value) { switch (value) { case enums::NUMBER_MODE_AUTO: - return "NUMBER_MODE_AUTO"; + return ESPHOME_PSTR("NUMBER_MODE_AUTO"); case enums::NUMBER_MODE_BOX: - return "NUMBER_MODE_BOX"; + return ESPHOME_PSTR("NUMBER_MODE_BOX"); case enums::NUMBER_MODE_SLIDER: - return "NUMBER_MODE_SLIDER"; + return ESPHOME_PSTR("NUMBER_MODE_SLIDER"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -439,31 +464,31 @@ template<> const char *proto_enum_to_string(enums::NumberMode template<> const char *proto_enum_to_string(enums::LockState value) { switch (value) { case enums::LOCK_STATE_NONE: - return "LOCK_STATE_NONE"; + return ESPHOME_PSTR("LOCK_STATE_NONE"); case enums::LOCK_STATE_LOCKED: - return "LOCK_STATE_LOCKED"; + return ESPHOME_PSTR("LOCK_STATE_LOCKED"); case enums::LOCK_STATE_UNLOCKED: - return "LOCK_STATE_UNLOCKED"; + return ESPHOME_PSTR("LOCK_STATE_UNLOCKED"); case enums::LOCK_STATE_JAMMED: - return "LOCK_STATE_JAMMED"; + return ESPHOME_PSTR("LOCK_STATE_JAMMED"); case enums::LOCK_STATE_LOCKING: - return "LOCK_STATE_LOCKING"; + return ESPHOME_PSTR("LOCK_STATE_LOCKING"); case enums::LOCK_STATE_UNLOCKING: - return "LOCK_STATE_UNLOCKING"; + return ESPHOME_PSTR("LOCK_STATE_UNLOCKING"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::LockCommand value) { switch (value) { case enums::LOCK_UNLOCK: - return "LOCK_UNLOCK"; + return ESPHOME_PSTR("LOCK_UNLOCK"); case enums::LOCK_LOCK: - return "LOCK_LOCK"; + return ESPHOME_PSTR("LOCK_LOCK"); case enums::LOCK_OPEN: - return "LOCK_OPEN"; + return ESPHOME_PSTR("LOCK_OPEN"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -471,65 +496,65 @@ template<> const char *proto_enum_to_string(enums::LockComma template<> const char *proto_enum_to_string(enums::MediaPlayerState value) { switch (value) { case enums::MEDIA_PLAYER_STATE_NONE: - return "MEDIA_PLAYER_STATE_NONE"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_NONE"); case enums::MEDIA_PLAYER_STATE_IDLE: - return "MEDIA_PLAYER_STATE_IDLE"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_IDLE"); case enums::MEDIA_PLAYER_STATE_PLAYING: - return "MEDIA_PLAYER_STATE_PLAYING"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_PLAYING"); case enums::MEDIA_PLAYER_STATE_PAUSED: - return "MEDIA_PLAYER_STATE_PAUSED"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_PAUSED"); case enums::MEDIA_PLAYER_STATE_ANNOUNCING: - return "MEDIA_PLAYER_STATE_ANNOUNCING"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_ANNOUNCING"); case enums::MEDIA_PLAYER_STATE_OFF: - return "MEDIA_PLAYER_STATE_OFF"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_OFF"); case enums::MEDIA_PLAYER_STATE_ON: - return "MEDIA_PLAYER_STATE_ON"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_ON"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::MediaPlayerCommand value) { switch (value) { case enums::MEDIA_PLAYER_COMMAND_PLAY: - return "MEDIA_PLAYER_COMMAND_PLAY"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_PLAY"); case enums::MEDIA_PLAYER_COMMAND_PAUSE: - return "MEDIA_PLAYER_COMMAND_PAUSE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_PAUSE"); case enums::MEDIA_PLAYER_COMMAND_STOP: - return "MEDIA_PLAYER_COMMAND_STOP"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_STOP"); case enums::MEDIA_PLAYER_COMMAND_MUTE: - return "MEDIA_PLAYER_COMMAND_MUTE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_MUTE"); case enums::MEDIA_PLAYER_COMMAND_UNMUTE: - return "MEDIA_PLAYER_COMMAND_UNMUTE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_UNMUTE"); case enums::MEDIA_PLAYER_COMMAND_TOGGLE: - return "MEDIA_PLAYER_COMMAND_TOGGLE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_TOGGLE"); case enums::MEDIA_PLAYER_COMMAND_VOLUME_UP: - return "MEDIA_PLAYER_COMMAND_VOLUME_UP"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_VOLUME_UP"); case enums::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: - return "MEDIA_PLAYER_COMMAND_VOLUME_DOWN"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_VOLUME_DOWN"); case enums::MEDIA_PLAYER_COMMAND_ENQUEUE: - return "MEDIA_PLAYER_COMMAND_ENQUEUE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_ENQUEUE"); case enums::MEDIA_PLAYER_COMMAND_REPEAT_ONE: - return "MEDIA_PLAYER_COMMAND_REPEAT_ONE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_REPEAT_ONE"); case enums::MEDIA_PLAYER_COMMAND_REPEAT_OFF: - return "MEDIA_PLAYER_COMMAND_REPEAT_OFF"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_REPEAT_OFF"); case enums::MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST: - return "MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST"); case enums::MEDIA_PLAYER_COMMAND_TURN_ON: - return "MEDIA_PLAYER_COMMAND_TURN_ON"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_TURN_ON"); case enums::MEDIA_PLAYER_COMMAND_TURN_OFF: - return "MEDIA_PLAYER_COMMAND_TURN_OFF"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_TURN_OFF"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::MediaPlayerFormatPurpose value) { switch (value) { case enums::MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT: - return "MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT"; + return ESPHOME_PSTR("MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT"); case enums::MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT: - return "MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT"; + return ESPHOME_PSTR("MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -538,49 +563,49 @@ template<> const char *proto_enum_to_string(enums::BluetoothDeviceRequestType value) { switch (value) { case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::BluetoothScannerState value) { switch (value) { case enums::BLUETOOTH_SCANNER_STATE_IDLE: - return "BLUETOOTH_SCANNER_STATE_IDLE"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_IDLE"); case enums::BLUETOOTH_SCANNER_STATE_STARTING: - return "BLUETOOTH_SCANNER_STATE_STARTING"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_STARTING"); case enums::BLUETOOTH_SCANNER_STATE_RUNNING: - return "BLUETOOTH_SCANNER_STATE_RUNNING"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_RUNNING"); case enums::BLUETOOTH_SCANNER_STATE_FAILED: - return "BLUETOOTH_SCANNER_STATE_FAILED"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_FAILED"); case enums::BLUETOOTH_SCANNER_STATE_STOPPING: - return "BLUETOOTH_SCANNER_STATE_STOPPING"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_STOPPING"); case enums::BLUETOOTH_SCANNER_STATE_STOPPED: - return "BLUETOOTH_SCANNER_STATE_STOPPED"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_STOPPED"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::BluetoothScannerMode value) { switch (value) { case enums::BLUETOOTH_SCANNER_MODE_PASSIVE: - return "BLUETOOTH_SCANNER_MODE_PASSIVE"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_MODE_PASSIVE"); case enums::BLUETOOTH_SCANNER_MODE_ACTIVE: - return "BLUETOOTH_SCANNER_MODE_ACTIVE"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_MODE_ACTIVE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -588,76 +613,76 @@ template<> const char *proto_enum_to_string(enums::VoiceAssistantSubscribeFlag value) { switch (value) { case enums::VOICE_ASSISTANT_SUBSCRIBE_NONE: - return "VOICE_ASSISTANT_SUBSCRIBE_NONE"; + return ESPHOME_PSTR("VOICE_ASSISTANT_SUBSCRIBE_NONE"); case enums::VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO: - return "VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO"; + return ESPHOME_PSTR("VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::VoiceAssistantRequestFlag value) { switch (value) { case enums::VOICE_ASSISTANT_REQUEST_NONE: - return "VOICE_ASSISTANT_REQUEST_NONE"; + return ESPHOME_PSTR("VOICE_ASSISTANT_REQUEST_NONE"); case enums::VOICE_ASSISTANT_REQUEST_USE_VAD: - return "VOICE_ASSISTANT_REQUEST_USE_VAD"; + return ESPHOME_PSTR("VOICE_ASSISTANT_REQUEST_USE_VAD"); case enums::VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD: - return "VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD"; + return ESPHOME_PSTR("VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #ifdef USE_VOICE_ASSISTANT template<> const char *proto_enum_to_string(enums::VoiceAssistantEvent value) { switch (value) { case enums::VOICE_ASSISTANT_ERROR: - return "VOICE_ASSISTANT_ERROR"; + return ESPHOME_PSTR("VOICE_ASSISTANT_ERROR"); case enums::VOICE_ASSISTANT_RUN_START: - return "VOICE_ASSISTANT_RUN_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_RUN_START"); case enums::VOICE_ASSISTANT_RUN_END: - return "VOICE_ASSISTANT_RUN_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_RUN_END"); case enums::VOICE_ASSISTANT_STT_START: - return "VOICE_ASSISTANT_STT_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_STT_START"); case enums::VOICE_ASSISTANT_STT_END: - return "VOICE_ASSISTANT_STT_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_STT_END"); case enums::VOICE_ASSISTANT_INTENT_START: - return "VOICE_ASSISTANT_INTENT_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_INTENT_START"); case enums::VOICE_ASSISTANT_INTENT_END: - return "VOICE_ASSISTANT_INTENT_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_INTENT_END"); case enums::VOICE_ASSISTANT_TTS_START: - return "VOICE_ASSISTANT_TTS_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TTS_START"); case enums::VOICE_ASSISTANT_TTS_END: - return "VOICE_ASSISTANT_TTS_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TTS_END"); case enums::VOICE_ASSISTANT_WAKE_WORD_START: - return "VOICE_ASSISTANT_WAKE_WORD_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_WAKE_WORD_START"); case enums::VOICE_ASSISTANT_WAKE_WORD_END: - return "VOICE_ASSISTANT_WAKE_WORD_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_WAKE_WORD_END"); case enums::VOICE_ASSISTANT_STT_VAD_START: - return "VOICE_ASSISTANT_STT_VAD_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_STT_VAD_START"); case enums::VOICE_ASSISTANT_STT_VAD_END: - return "VOICE_ASSISTANT_STT_VAD_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_STT_VAD_END"); case enums::VOICE_ASSISTANT_TTS_STREAM_START: - return "VOICE_ASSISTANT_TTS_STREAM_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TTS_STREAM_START"); case enums::VOICE_ASSISTANT_TTS_STREAM_END: - return "VOICE_ASSISTANT_TTS_STREAM_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TTS_STREAM_END"); case enums::VOICE_ASSISTANT_INTENT_PROGRESS: - return "VOICE_ASSISTANT_INTENT_PROGRESS"; + return ESPHOME_PSTR("VOICE_ASSISTANT_INTENT_PROGRESS"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::VoiceAssistantTimerEvent value) { switch (value) { case enums::VOICE_ASSISTANT_TIMER_STARTED: - return "VOICE_ASSISTANT_TIMER_STARTED"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TIMER_STARTED"); case enums::VOICE_ASSISTANT_TIMER_UPDATED: - return "VOICE_ASSISTANT_TIMER_UPDATED"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TIMER_UPDATED"); case enums::VOICE_ASSISTANT_TIMER_CANCELLED: - return "VOICE_ASSISTANT_TIMER_CANCELLED"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TIMER_CANCELLED"); case enums::VOICE_ASSISTANT_TIMER_FINISHED: - return "VOICE_ASSISTANT_TIMER_FINISHED"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TIMER_FINISHED"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -665,48 +690,48 @@ template<> const char *proto_enum_to_string(enu template<> const char *proto_enum_to_string(enums::AlarmControlPanelState value) { switch (value) { case enums::ALARM_STATE_DISARMED: - return "ALARM_STATE_DISARMED"; + return ESPHOME_PSTR("ALARM_STATE_DISARMED"); case enums::ALARM_STATE_ARMED_HOME: - return "ALARM_STATE_ARMED_HOME"; + return ESPHOME_PSTR("ALARM_STATE_ARMED_HOME"); case enums::ALARM_STATE_ARMED_AWAY: - return "ALARM_STATE_ARMED_AWAY"; + return ESPHOME_PSTR("ALARM_STATE_ARMED_AWAY"); case enums::ALARM_STATE_ARMED_NIGHT: - return "ALARM_STATE_ARMED_NIGHT"; + return ESPHOME_PSTR("ALARM_STATE_ARMED_NIGHT"); case enums::ALARM_STATE_ARMED_VACATION: - return "ALARM_STATE_ARMED_VACATION"; + return ESPHOME_PSTR("ALARM_STATE_ARMED_VACATION"); case enums::ALARM_STATE_ARMED_CUSTOM_BYPASS: - return "ALARM_STATE_ARMED_CUSTOM_BYPASS"; + return ESPHOME_PSTR("ALARM_STATE_ARMED_CUSTOM_BYPASS"); case enums::ALARM_STATE_PENDING: - return "ALARM_STATE_PENDING"; + return ESPHOME_PSTR("ALARM_STATE_PENDING"); case enums::ALARM_STATE_ARMING: - return "ALARM_STATE_ARMING"; + return ESPHOME_PSTR("ALARM_STATE_ARMING"); case enums::ALARM_STATE_DISARMING: - return "ALARM_STATE_DISARMING"; + return ESPHOME_PSTR("ALARM_STATE_DISARMING"); case enums::ALARM_STATE_TRIGGERED: - return "ALARM_STATE_TRIGGERED"; + return ESPHOME_PSTR("ALARM_STATE_TRIGGERED"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::AlarmControlPanelStateCommand value) { switch (value) { case enums::ALARM_CONTROL_PANEL_DISARM: - return "ALARM_CONTROL_PANEL_DISARM"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_DISARM"); case enums::ALARM_CONTROL_PANEL_ARM_AWAY: - return "ALARM_CONTROL_PANEL_ARM_AWAY"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_ARM_AWAY"); case enums::ALARM_CONTROL_PANEL_ARM_HOME: - return "ALARM_CONTROL_PANEL_ARM_HOME"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_ARM_HOME"); case enums::ALARM_CONTROL_PANEL_ARM_NIGHT: - return "ALARM_CONTROL_PANEL_ARM_NIGHT"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_ARM_NIGHT"); case enums::ALARM_CONTROL_PANEL_ARM_VACATION: - return "ALARM_CONTROL_PANEL_ARM_VACATION"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_ARM_VACATION"); case enums::ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS: - return "ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS"); case enums::ALARM_CONTROL_PANEL_TRIGGER: - return "ALARM_CONTROL_PANEL_TRIGGER"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_TRIGGER"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -714,11 +739,11 @@ const char *proto_enum_to_string(enums::Al template<> const char *proto_enum_to_string(enums::TextMode value) { switch (value) { case enums::TEXT_MODE_TEXT: - return "TEXT_MODE_TEXT"; + return ESPHOME_PSTR("TEXT_MODE_TEXT"); case enums::TEXT_MODE_PASSWORD: - return "TEXT_MODE_PASSWORD"; + return ESPHOME_PSTR("TEXT_MODE_PASSWORD"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -726,13 +751,13 @@ template<> const char *proto_enum_to_string(enums::TextMode val template<> const char *proto_enum_to_string(enums::ValveOperation value) { switch (value) { case enums::VALVE_OPERATION_IDLE: - return "VALVE_OPERATION_IDLE"; + return ESPHOME_PSTR("VALVE_OPERATION_IDLE"); case enums::VALVE_OPERATION_IS_OPENING: - return "VALVE_OPERATION_IS_OPENING"; + return ESPHOME_PSTR("VALVE_OPERATION_IS_OPENING"); case enums::VALVE_OPERATION_IS_CLOSING: - return "VALVE_OPERATION_IS_CLOSING"; + return ESPHOME_PSTR("VALVE_OPERATION_IS_CLOSING"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -740,13 +765,13 @@ template<> const char *proto_enum_to_string(enums::ValveO template<> const char *proto_enum_to_string(enums::UpdateCommand value) { switch (value) { case enums::UPDATE_COMMAND_NONE: - return "UPDATE_COMMAND_NONE"; + return ESPHOME_PSTR("UPDATE_COMMAND_NONE"); case enums::UPDATE_COMMAND_UPDATE: - return "UPDATE_COMMAND_UPDATE"; + return ESPHOME_PSTR("UPDATE_COMMAND_UPDATE"); case enums::UPDATE_COMMAND_CHECK: - return "UPDATE_COMMAND_CHECK"; + return ESPHOME_PSTR("UPDATE_COMMAND_CHECK"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -754,13 +779,13 @@ template<> const char *proto_enum_to_string(enums::UpdateC template<> const char *proto_enum_to_string(enums::ZWaveProxyRequestType value) { switch (value) { case enums::ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE: - return "ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE"; + return ESPHOME_PSTR("ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE"); case enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE: - return "ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE"; + return ESPHOME_PSTR("ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE"); case enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE: - return "ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE"; + return ESPHOME_PSTR("ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -768,165 +793,165 @@ template<> const char *proto_enum_to_string(enums: template<> const char *proto_enum_to_string(enums::SerialProxyParity value) { switch (value) { case enums::SERIAL_PROXY_PARITY_NONE: - return "SERIAL_PROXY_PARITY_NONE"; + return ESPHOME_PSTR("SERIAL_PROXY_PARITY_NONE"); case enums::SERIAL_PROXY_PARITY_EVEN: - return "SERIAL_PROXY_PARITY_EVEN"; + return ESPHOME_PSTR("SERIAL_PROXY_PARITY_EVEN"); case enums::SERIAL_PROXY_PARITY_ODD: - return "SERIAL_PROXY_PARITY_ODD"; + return ESPHOME_PSTR("SERIAL_PROXY_PARITY_ODD"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::SerialProxyRequestType value) { switch (value) { case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE: - return "SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE"; + return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE"); case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: - return "SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE"; + return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE"); case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: - return "SERIAL_PROXY_REQUEST_TYPE_FLUSH"; + return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_FLUSH"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::SerialProxyStatus value) { switch (value) { case enums::SERIAL_PROXY_STATUS_OK: - return "SERIAL_PROXY_STATUS_OK"; + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_OK"); case enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS: - return "SERIAL_PROXY_STATUS_ASSUMED_SUCCESS"; + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_ASSUMED_SUCCESS"); case enums::SERIAL_PROXY_STATUS_ERROR: - return "SERIAL_PROXY_STATUS_ERROR"; + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_ERROR"); case enums::SERIAL_PROXY_STATUS_TIMEOUT: - return "SERIAL_PROXY_STATUS_TIMEOUT"; + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_TIMEOUT"); case enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED: - return "SERIAL_PROXY_STATUS_NOT_SUPPORTED"; + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_NOT_SUPPORTED"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif const char *HelloRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HelloRequest"); - dump_field(out, "client_info", this->client_info); - dump_field(out, "api_version_major", this->api_version_major); - dump_field(out, "api_version_minor", this->api_version_minor); + MessageDumpHelper helper(out, ESPHOME_PSTR("HelloRequest")); + dump_field(out, ESPHOME_PSTR("client_info"), this->client_info); + dump_field(out, ESPHOME_PSTR("api_version_major"), this->api_version_major); + dump_field(out, ESPHOME_PSTR("api_version_minor"), this->api_version_minor); return out.c_str(); } const char *HelloResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HelloResponse"); - dump_field(out, "api_version_major", this->api_version_major); - dump_field(out, "api_version_minor", this->api_version_minor); - dump_field(out, "server_info", this->server_info); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("HelloResponse")); + dump_field(out, ESPHOME_PSTR("api_version_major"), this->api_version_major); + dump_field(out, ESPHOME_PSTR("api_version_minor"), this->api_version_minor); + dump_field(out, ESPHOME_PSTR("server_info"), this->server_info); + dump_field(out, ESPHOME_PSTR("name"), this->name); return out.c_str(); } const char *DisconnectRequest::dump_to(DumpBuffer &out) const { - out.append("DisconnectRequest {}"); + out.append_p(ESPHOME_PSTR("DisconnectRequest {}")); return out.c_str(); } const char *DisconnectResponse::dump_to(DumpBuffer &out) const { - out.append("DisconnectResponse {}"); + out.append_p(ESPHOME_PSTR("DisconnectResponse {}")); return out.c_str(); } const char *PingRequest::dump_to(DumpBuffer &out) const { - out.append("PingRequest {}"); + out.append_p(ESPHOME_PSTR("PingRequest {}")); return out.c_str(); } const char *PingResponse::dump_to(DumpBuffer &out) const { - out.append("PingResponse {}"); + out.append_p(ESPHOME_PSTR("PingResponse {}")); return out.c_str(); } #ifdef USE_AREAS const char *AreaInfo::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "AreaInfo"); - dump_field(out, "area_id", this->area_id); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("AreaInfo")); + dump_field(out, ESPHOME_PSTR("area_id"), this->area_id); + dump_field(out, ESPHOME_PSTR("name"), this->name); return out.c_str(); } #endif #ifdef USE_DEVICES const char *DeviceInfo::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DeviceInfo"); - dump_field(out, "device_id", this->device_id); - dump_field(out, "name", this->name); - dump_field(out, "area_id", this->area_id); + MessageDumpHelper helper(out, ESPHOME_PSTR("DeviceInfo")); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("area_id"), this->area_id); return out.c_str(); } #endif #ifdef USE_SERIAL_PROXY const char *SerialProxyInfo::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyInfo"); - dump_field(out, "name", this->name); - dump_field(out, "port_type", static_cast(this->port_type)); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyInfo")); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("port_type"), static_cast(this->port_type)); return out.c_str(); } #endif const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DeviceInfoResponse"); - dump_field(out, "name", this->name); - dump_field(out, "mac_address", this->mac_address); - dump_field(out, "esphome_version", this->esphome_version); - dump_field(out, "compilation_time", this->compilation_time); - dump_field(out, "model", this->model); + MessageDumpHelper helper(out, ESPHOME_PSTR("DeviceInfoResponse")); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("mac_address"), this->mac_address); + dump_field(out, ESPHOME_PSTR("esphome_version"), this->esphome_version); + dump_field(out, ESPHOME_PSTR("compilation_time"), this->compilation_time); + dump_field(out, ESPHOME_PSTR("model"), this->model); #ifdef USE_DEEP_SLEEP - dump_field(out, "has_deep_sleep", this->has_deep_sleep); + dump_field(out, ESPHOME_PSTR("has_deep_sleep"), this->has_deep_sleep); #endif #ifdef ESPHOME_PROJECT_NAME - dump_field(out, "project_name", this->project_name); + dump_field(out, ESPHOME_PSTR("project_name"), this->project_name); #endif #ifdef ESPHOME_PROJECT_NAME - dump_field(out, "project_version", this->project_version); + dump_field(out, ESPHOME_PSTR("project_version"), this->project_version); #endif #ifdef USE_WEBSERVER - dump_field(out, "webserver_port", this->webserver_port); + dump_field(out, ESPHOME_PSTR("webserver_port"), this->webserver_port); #endif #ifdef USE_BLUETOOTH_PROXY - dump_field(out, "bluetooth_proxy_feature_flags", this->bluetooth_proxy_feature_flags); + dump_field(out, ESPHOME_PSTR("bluetooth_proxy_feature_flags"), this->bluetooth_proxy_feature_flags); #endif - dump_field(out, "manufacturer", this->manufacturer); - dump_field(out, "friendly_name", this->friendly_name); + dump_field(out, ESPHOME_PSTR("manufacturer"), this->manufacturer); + dump_field(out, ESPHOME_PSTR("friendly_name"), this->friendly_name); #ifdef USE_VOICE_ASSISTANT - dump_field(out, "voice_assistant_feature_flags", this->voice_assistant_feature_flags); + dump_field(out, ESPHOME_PSTR("voice_assistant_feature_flags"), this->voice_assistant_feature_flags); #endif #ifdef USE_AREAS - dump_field(out, "suggested_area", this->suggested_area); + dump_field(out, ESPHOME_PSTR("suggested_area"), this->suggested_area); #endif #ifdef USE_BLUETOOTH_PROXY - dump_field(out, "bluetooth_mac_address", this->bluetooth_mac_address); + dump_field(out, ESPHOME_PSTR("bluetooth_mac_address"), this->bluetooth_mac_address); #endif #ifdef USE_API_NOISE - dump_field(out, "api_encryption_supported", this->api_encryption_supported); + dump_field(out, ESPHOME_PSTR("api_encryption_supported"), this->api_encryption_supported); #endif #ifdef USE_DEVICES for (const auto &it : this->devices) { - out.append(" devices: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("devices")).append(": "); it.dump_to(out); out.append("\n"); } #endif #ifdef USE_AREAS for (const auto &it : this->areas) { - out.append(" areas: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("areas")).append(": "); it.dump_to(out); out.append("\n"); } #endif #ifdef USE_AREAS - out.append(" area: "); + out.append(2, ' ').append_p(ESPHOME_PSTR("area")).append(": "); this->area.dump_to(out); out.append("\n"); #endif #ifdef USE_ZWAVE_PROXY - dump_field(out, "zwave_proxy_feature_flags", this->zwave_proxy_feature_flags); + dump_field(out, ESPHOME_PSTR("zwave_proxy_feature_flags"), this->zwave_proxy_feature_flags); #endif #ifdef USE_ZWAVE_PROXY - dump_field(out, "zwave_home_id", this->zwave_home_id); + dump_field(out, ESPHOME_PSTR("zwave_home_id"), this->zwave_home_id); #endif #ifdef USE_SERIAL_PROXY for (const auto &it : this->serial_proxies) { - out.append(" serial_proxies: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("serial_proxies")).append(": "); it.dump_to(out); out.append("\n"); } @@ -934,1720 +959,1720 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const { return out.c_str(); } const char *ListEntitiesDoneResponse::dump_to(DumpBuffer &out) const { - out.append("ListEntitiesDoneResponse {}"); + out.append_p(ESPHOME_PSTR("ListEntitiesDoneResponse {}")); return out.c_str(); } #ifdef USE_BINARY_SENSOR const char *ListEntitiesBinarySensorResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesBinarySensorResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); - dump_field(out, "device_class", this->device_class); - dump_field(out, "is_status_binary_sensor", this->is_status_binary_sensor); - dump_field(out, "disabled_by_default", this->disabled_by_default); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesBinarySensorResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); + dump_field(out, ESPHOME_PSTR("is_status_binary_sensor"), this->is_status_binary_sensor); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *BinarySensorStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BinarySensorStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("BinarySensorStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_COVER const char *ListEntitiesCoverResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesCoverResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); - dump_field(out, "assumed_state", this->assumed_state); - dump_field(out, "supports_position", this->supports_position); - dump_field(out, "supports_tilt", this->supports_tilt); - dump_field(out, "device_class", this->device_class); - dump_field(out, "disabled_by_default", this->disabled_by_default); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesCoverResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("assumed_state"), this->assumed_state); + dump_field(out, ESPHOME_PSTR("supports_position"), this->supports_position); + dump_field(out, ESPHOME_PSTR("supports_tilt"), this->supports_tilt); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "supports_stop", this->supports_stop); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("supports_stop"), this->supports_stop); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *CoverStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "CoverStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "position", this->position); - dump_field(out, "tilt", this->tilt); - dump_field(out, "current_operation", static_cast(this->current_operation)); + MessageDumpHelper helper(out, ESPHOME_PSTR("CoverStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("position"), this->position); + dump_field(out, ESPHOME_PSTR("tilt"), this->tilt); + dump_field(out, ESPHOME_PSTR("current_operation"), static_cast(this->current_operation)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *CoverCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "CoverCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_position", this->has_position); - dump_field(out, "position", this->position); - dump_field(out, "has_tilt", this->has_tilt); - dump_field(out, "tilt", this->tilt); - dump_field(out, "stop", this->stop); + MessageDumpHelper helper(out, ESPHOME_PSTR("CoverCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_position"), this->has_position); + dump_field(out, ESPHOME_PSTR("position"), this->position); + dump_field(out, ESPHOME_PSTR("has_tilt"), this->has_tilt); + dump_field(out, ESPHOME_PSTR("tilt"), this->tilt); + dump_field(out, ESPHOME_PSTR("stop"), this->stop); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_FAN const char *ListEntitiesFanResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesFanResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); - dump_field(out, "supports_oscillation", this->supports_oscillation); - dump_field(out, "supports_speed", this->supports_speed); - dump_field(out, "supports_direction", this->supports_direction); - dump_field(out, "supported_speed_count", this->supported_speed_count); - dump_field(out, "disabled_by_default", this->disabled_by_default); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesFanResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("supports_oscillation"), this->supports_oscillation); + dump_field(out, ESPHOME_PSTR("supports_speed"), this->supports_speed); + dump_field(out, ESPHOME_PSTR("supports_direction"), this->supports_direction); + dump_field(out, ESPHOME_PSTR("supported_speed_count"), this->supported_speed_count); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); for (const auto &it : *this->supported_preset_modes) { - dump_field(out, "supported_preset_modes", it, 4); + dump_field(out, ESPHOME_PSTR("supported_preset_modes"), it, 4); } #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *FanStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "FanStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "oscillating", this->oscillating); - dump_field(out, "direction", static_cast(this->direction)); - dump_field(out, "speed_level", this->speed_level); - dump_field(out, "preset_mode", this->preset_mode); + MessageDumpHelper helper(out, ESPHOME_PSTR("FanStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("oscillating"), this->oscillating); + dump_field(out, ESPHOME_PSTR("direction"), static_cast(this->direction)); + dump_field(out, ESPHOME_PSTR("speed_level"), this->speed_level); + dump_field(out, ESPHOME_PSTR("preset_mode"), this->preset_mode); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *FanCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "FanCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_state", this->has_state); - dump_field(out, "state", this->state); - dump_field(out, "has_oscillating", this->has_oscillating); - dump_field(out, "oscillating", this->oscillating); - dump_field(out, "has_direction", this->has_direction); - dump_field(out, "direction", static_cast(this->direction)); - dump_field(out, "has_speed_level", this->has_speed_level); - dump_field(out, "speed_level", this->speed_level); - dump_field(out, "has_preset_mode", this->has_preset_mode); - dump_field(out, "preset_mode", this->preset_mode); + MessageDumpHelper helper(out, ESPHOME_PSTR("FanCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_state"), this->has_state); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("has_oscillating"), this->has_oscillating); + dump_field(out, ESPHOME_PSTR("oscillating"), this->oscillating); + dump_field(out, ESPHOME_PSTR("has_direction"), this->has_direction); + dump_field(out, ESPHOME_PSTR("direction"), static_cast(this->direction)); + dump_field(out, ESPHOME_PSTR("has_speed_level"), this->has_speed_level); + dump_field(out, ESPHOME_PSTR("speed_level"), this->speed_level); + dump_field(out, ESPHOME_PSTR("has_preset_mode"), this->has_preset_mode); + dump_field(out, ESPHOME_PSTR("preset_mode"), this->preset_mode); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_LIGHT const char *ListEntitiesLightResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesLightResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesLightResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); for (const auto &it : *this->supported_color_modes) { - dump_field(out, "supported_color_modes", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("supported_color_modes"), static_cast(it), 4); } - dump_field(out, "min_mireds", this->min_mireds); - dump_field(out, "max_mireds", this->max_mireds); + dump_field(out, ESPHOME_PSTR("min_mireds"), this->min_mireds); + dump_field(out, ESPHOME_PSTR("max_mireds"), this->max_mireds); for (const auto &it : *this->effects) { - dump_field(out, "effects", it, 4); + dump_field(out, ESPHOME_PSTR("effects"), it, 4); } - dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *LightStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "LightStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "brightness", this->brightness); - dump_field(out, "color_mode", static_cast(this->color_mode)); - dump_field(out, "color_brightness", this->color_brightness); - dump_field(out, "red", this->red); - dump_field(out, "green", this->green); - dump_field(out, "blue", this->blue); - dump_field(out, "white", this->white); - dump_field(out, "color_temperature", this->color_temperature); - dump_field(out, "cold_white", this->cold_white); - dump_field(out, "warm_white", this->warm_white); - dump_field(out, "effect", this->effect); + MessageDumpHelper helper(out, ESPHOME_PSTR("LightStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("brightness"), this->brightness); + dump_field(out, ESPHOME_PSTR("color_mode"), static_cast(this->color_mode)); + dump_field(out, ESPHOME_PSTR("color_brightness"), this->color_brightness); + dump_field(out, ESPHOME_PSTR("red"), this->red); + dump_field(out, ESPHOME_PSTR("green"), this->green); + dump_field(out, ESPHOME_PSTR("blue"), this->blue); + dump_field(out, ESPHOME_PSTR("white"), this->white); + dump_field(out, ESPHOME_PSTR("color_temperature"), this->color_temperature); + dump_field(out, ESPHOME_PSTR("cold_white"), this->cold_white); + dump_field(out, ESPHOME_PSTR("warm_white"), this->warm_white); + dump_field(out, ESPHOME_PSTR("effect"), this->effect); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *LightCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "LightCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_state", this->has_state); - dump_field(out, "state", this->state); - dump_field(out, "has_brightness", this->has_brightness); - dump_field(out, "brightness", this->brightness); - dump_field(out, "has_color_mode", this->has_color_mode); - dump_field(out, "color_mode", static_cast(this->color_mode)); - dump_field(out, "has_color_brightness", this->has_color_brightness); - dump_field(out, "color_brightness", this->color_brightness); - dump_field(out, "has_rgb", this->has_rgb); - dump_field(out, "red", this->red); - dump_field(out, "green", this->green); - dump_field(out, "blue", this->blue); - dump_field(out, "has_white", this->has_white); - dump_field(out, "white", this->white); - dump_field(out, "has_color_temperature", this->has_color_temperature); - dump_field(out, "color_temperature", this->color_temperature); - dump_field(out, "has_cold_white", this->has_cold_white); - dump_field(out, "cold_white", this->cold_white); - dump_field(out, "has_warm_white", this->has_warm_white); - dump_field(out, "warm_white", this->warm_white); - dump_field(out, "has_transition_length", this->has_transition_length); - dump_field(out, "transition_length", this->transition_length); - dump_field(out, "has_flash_length", this->has_flash_length); - dump_field(out, "flash_length", this->flash_length); - dump_field(out, "has_effect", this->has_effect); - dump_field(out, "effect", this->effect); + MessageDumpHelper helper(out, ESPHOME_PSTR("LightCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_state"), this->has_state); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("has_brightness"), this->has_brightness); + dump_field(out, ESPHOME_PSTR("brightness"), this->brightness); + dump_field(out, ESPHOME_PSTR("has_color_mode"), this->has_color_mode); + dump_field(out, ESPHOME_PSTR("color_mode"), static_cast(this->color_mode)); + dump_field(out, ESPHOME_PSTR("has_color_brightness"), this->has_color_brightness); + dump_field(out, ESPHOME_PSTR("color_brightness"), this->color_brightness); + dump_field(out, ESPHOME_PSTR("has_rgb"), this->has_rgb); + dump_field(out, ESPHOME_PSTR("red"), this->red); + dump_field(out, ESPHOME_PSTR("green"), this->green); + dump_field(out, ESPHOME_PSTR("blue"), this->blue); + dump_field(out, ESPHOME_PSTR("has_white"), this->has_white); + dump_field(out, ESPHOME_PSTR("white"), this->white); + dump_field(out, ESPHOME_PSTR("has_color_temperature"), this->has_color_temperature); + dump_field(out, ESPHOME_PSTR("color_temperature"), this->color_temperature); + dump_field(out, ESPHOME_PSTR("has_cold_white"), this->has_cold_white); + dump_field(out, ESPHOME_PSTR("cold_white"), this->cold_white); + dump_field(out, ESPHOME_PSTR("has_warm_white"), this->has_warm_white); + dump_field(out, ESPHOME_PSTR("warm_white"), this->warm_white); + dump_field(out, ESPHOME_PSTR("has_transition_length"), this->has_transition_length); + dump_field(out, ESPHOME_PSTR("transition_length"), this->transition_length); + dump_field(out, ESPHOME_PSTR("has_flash_length"), this->has_flash_length); + dump_field(out, ESPHOME_PSTR("flash_length"), this->flash_length); + dump_field(out, ESPHOME_PSTR("has_effect"), this->has_effect); + dump_field(out, ESPHOME_PSTR("effect"), this->effect); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_SENSOR const char *ListEntitiesSensorResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesSensorResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesSensorResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "unit_of_measurement", this->unit_of_measurement); - dump_field(out, "accuracy_decimals", this->accuracy_decimals); - dump_field(out, "force_update", this->force_update); - dump_field(out, "device_class", this->device_class); - dump_field(out, "state_class", static_cast(this->state_class)); - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("unit_of_measurement"), this->unit_of_measurement); + dump_field(out, ESPHOME_PSTR("accuracy_decimals"), this->accuracy_decimals); + dump_field(out, ESPHOME_PSTR("force_update"), this->force_update); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); + dump_field(out, ESPHOME_PSTR("state_class"), static_cast(this->state_class)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SensorStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SensorStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SensorStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_SWITCH const char *ListEntitiesSwitchResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesSwitchResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesSwitchResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "assumed_state", this->assumed_state); - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("assumed_state"), this->assumed_state); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SwitchStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SwitchStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SwitchStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SwitchCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SwitchCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SwitchCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_TEXT_SENSOR const char *ListEntitiesTextSensorResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesTextSensorResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesTextSensorResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *TextSensorStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "TextSensorStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("TextSensorStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif const char *SubscribeLogsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SubscribeLogsRequest"); - dump_field(out, "level", static_cast(this->level)); - dump_field(out, "dump_config", this->dump_config); + MessageDumpHelper helper(out, ESPHOME_PSTR("SubscribeLogsRequest")); + dump_field(out, ESPHOME_PSTR("level"), static_cast(this->level)); + dump_field(out, ESPHOME_PSTR("dump_config"), this->dump_config); return out.c_str(); } const char *SubscribeLogsResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SubscribeLogsResponse"); - dump_field(out, "level", static_cast(this->level)); - dump_bytes_field(out, "message", this->message_ptr_, this->message_len_); + MessageDumpHelper helper(out, ESPHOME_PSTR("SubscribeLogsResponse")); + dump_field(out, ESPHOME_PSTR("level"), static_cast(this->level)); + dump_bytes_field(out, ESPHOME_PSTR("message"), this->message_ptr_, this->message_len_); return out.c_str(); } #ifdef USE_API_NOISE const char *NoiseEncryptionSetKeyRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "NoiseEncryptionSetKeyRequest"); - dump_bytes_field(out, "key", this->key, this->key_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("NoiseEncryptionSetKeyRequest")); + dump_bytes_field(out, ESPHOME_PSTR("key"), this->key, this->key_len); return out.c_str(); } const char *NoiseEncryptionSetKeyResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "NoiseEncryptionSetKeyResponse"); - dump_field(out, "success", this->success); + MessageDumpHelper helper(out, ESPHOME_PSTR("NoiseEncryptionSetKeyResponse")); + dump_field(out, ESPHOME_PSTR("success"), this->success); return out.c_str(); } #endif #ifdef USE_API_HOMEASSISTANT_SERVICES const char *HomeassistantServiceMap::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HomeassistantServiceMap"); - dump_field(out, "key", this->key); - dump_field(out, "value", this->value); + MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantServiceMap")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("value"), this->value); return out.c_str(); } const char *HomeassistantActionRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HomeassistantActionRequest"); - dump_field(out, "service", this->service); + MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantActionRequest")); + dump_field(out, ESPHOME_PSTR("service"), this->service); for (const auto &it : this->data) { - out.append(" data: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("data")).append(": "); it.dump_to(out); out.append("\n"); } for (const auto &it : this->data_template) { - out.append(" data_template: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("data_template")).append(": "); it.dump_to(out); out.append("\n"); } for (const auto &it : this->variables) { - out.append(" variables: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("variables")).append(": "); it.dump_to(out); out.append("\n"); } - dump_field(out, "is_event", this->is_event); + dump_field(out, ESPHOME_PSTR("is_event"), this->is_event); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES - dump_field(out, "call_id", this->call_id); + dump_field(out, ESPHOME_PSTR("call_id"), this->call_id); #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - dump_field(out, "wants_response", this->wants_response); + dump_field(out, ESPHOME_PSTR("wants_response"), this->wants_response); #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - dump_field(out, "response_template", this->response_template); + dump_field(out, ESPHOME_PSTR("response_template"), this->response_template); #endif return out.c_str(); } #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES const char *HomeassistantActionResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HomeassistantActionResponse"); - dump_field(out, "call_id", this->call_id); - dump_field(out, "success", this->success); - dump_field(out, "error_message", this->error_message); + MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantActionResponse")); + dump_field(out, ESPHOME_PSTR("call_id"), this->call_id); + dump_field(out, ESPHOME_PSTR("success"), this->success); + dump_field(out, ESPHOME_PSTR("error_message"), this->error_message); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - dump_bytes_field(out, "response_data", this->response_data, this->response_data_len); + dump_bytes_field(out, ESPHOME_PSTR("response_data"), this->response_data, this->response_data_len); #endif return out.c_str(); } #endif #ifdef USE_API_HOMEASSISTANT_STATES const char *SubscribeHomeAssistantStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SubscribeHomeAssistantStateResponse"); - dump_field(out, "entity_id", this->entity_id); - dump_field(out, "attribute", this->attribute); - dump_field(out, "once", this->once); + MessageDumpHelper helper(out, ESPHOME_PSTR("SubscribeHomeAssistantStateResponse")); + dump_field(out, ESPHOME_PSTR("entity_id"), this->entity_id); + dump_field(out, ESPHOME_PSTR("attribute"), this->attribute); + dump_field(out, ESPHOME_PSTR("once"), this->once); return out.c_str(); } const char *HomeAssistantStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HomeAssistantStateResponse"); - dump_field(out, "entity_id", this->entity_id); - dump_field(out, "state", this->state); - dump_field(out, "attribute", this->attribute); + MessageDumpHelper helper(out, ESPHOME_PSTR("HomeAssistantStateResponse")); + dump_field(out, ESPHOME_PSTR("entity_id"), this->entity_id); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("attribute"), this->attribute); return out.c_str(); } #endif const char *GetTimeRequest::dump_to(DumpBuffer &out) const { - out.append("GetTimeRequest {}"); + out.append_p(ESPHOME_PSTR("GetTimeRequest {}")); return out.c_str(); } const char *DSTRule::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DSTRule"); - dump_field(out, "time_seconds", this->time_seconds); - dump_field(out, "day", this->day); - dump_field(out, "type", static_cast(this->type)); - dump_field(out, "month", this->month); - dump_field(out, "week", this->week); - dump_field(out, "day_of_week", this->day_of_week); + MessageDumpHelper helper(out, ESPHOME_PSTR("DSTRule")); + dump_field(out, ESPHOME_PSTR("time_seconds"), this->time_seconds); + dump_field(out, ESPHOME_PSTR("day"), this->day); + dump_field(out, ESPHOME_PSTR("type"), static_cast(this->type)); + dump_field(out, ESPHOME_PSTR("month"), this->month); + dump_field(out, ESPHOME_PSTR("week"), this->week); + dump_field(out, ESPHOME_PSTR("day_of_week"), this->day_of_week); return out.c_str(); } const char *ParsedTimezone::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ParsedTimezone"); - dump_field(out, "std_offset_seconds", this->std_offset_seconds); - dump_field(out, "dst_offset_seconds", this->dst_offset_seconds); - out.append(" dst_start: "); + MessageDumpHelper helper(out, ESPHOME_PSTR("ParsedTimezone")); + dump_field(out, ESPHOME_PSTR("std_offset_seconds"), this->std_offset_seconds); + dump_field(out, ESPHOME_PSTR("dst_offset_seconds"), this->dst_offset_seconds); + out.append(2, ' ').append_p(ESPHOME_PSTR("dst_start")).append(": "); this->dst_start.dump_to(out); out.append("\n"); - out.append(" dst_end: "); + out.append(2, ' ').append_p(ESPHOME_PSTR("dst_end")).append(": "); this->dst_end.dump_to(out); out.append("\n"); return out.c_str(); } const char *GetTimeResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "GetTimeResponse"); - dump_field(out, "epoch_seconds", this->epoch_seconds); - dump_field(out, "timezone", this->timezone); - out.append(" parsed_timezone: "); + MessageDumpHelper helper(out, ESPHOME_PSTR("GetTimeResponse")); + dump_field(out, ESPHOME_PSTR("epoch_seconds"), this->epoch_seconds); + dump_field(out, ESPHOME_PSTR("timezone"), this->timezone); + out.append(2, ' ').append_p(ESPHOME_PSTR("parsed_timezone")).append(": "); this->parsed_timezone.dump_to(out); out.append("\n"); return out.c_str(); } #ifdef USE_API_USER_DEFINED_ACTIONS const char *ListEntitiesServicesArgument::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesServicesArgument"); - dump_field(out, "name", this->name); - dump_field(out, "type", static_cast(this->type)); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesArgument")); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("type"), static_cast(this->type)); return out.c_str(); } const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesServicesResponse"); - dump_field(out, "name", this->name); - dump_field(out, "key", this->key); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesResponse")); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("key"), this->key); for (const auto &it : this->args) { - out.append(" args: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("args")).append(": "); it.dump_to(out); out.append("\n"); } - dump_field(out, "supports_response", static_cast(this->supports_response)); + dump_field(out, ESPHOME_PSTR("supports_response"), static_cast(this->supports_response)); return out.c_str(); } const char *ExecuteServiceArgument::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ExecuteServiceArgument"); - dump_field(out, "bool_", this->bool_); - dump_field(out, "legacy_int", this->legacy_int); - dump_field(out, "float_", this->float_); - dump_field(out, "string_", this->string_); - dump_field(out, "int_", this->int_); + MessageDumpHelper helper(out, ESPHOME_PSTR("ExecuteServiceArgument")); + dump_field(out, ESPHOME_PSTR("bool_"), this->bool_); + dump_field(out, ESPHOME_PSTR("legacy_int"), this->legacy_int); + dump_field(out, ESPHOME_PSTR("float_"), this->float_); + dump_field(out, ESPHOME_PSTR("string_"), this->string_); + dump_field(out, ESPHOME_PSTR("int_"), this->int_); for (const auto it : this->bool_array) { - dump_field(out, "bool_array", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("bool_array"), static_cast(it), 4); } for (const auto &it : this->int_array) { - dump_field(out, "int_array", it, 4); + dump_field(out, ESPHOME_PSTR("int_array"), it, 4); } for (const auto &it : this->float_array) { - dump_field(out, "float_array", it, 4); + dump_field(out, ESPHOME_PSTR("float_array"), it, 4); } for (const auto &it : this->string_array) { - dump_field(out, "string_array", it, 4); + dump_field(out, ESPHOME_PSTR("string_array"), it, 4); } return out.c_str(); } const char *ExecuteServiceRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ExecuteServiceRequest"); - dump_field(out, "key", this->key); + MessageDumpHelper helper(out, ESPHOME_PSTR("ExecuteServiceRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); for (const auto &it : this->args) { - out.append(" args: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("args")).append(": "); it.dump_to(out); out.append("\n"); } #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES - dump_field(out, "call_id", this->call_id); + dump_field(out, ESPHOME_PSTR("call_id"), this->call_id); #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES - dump_field(out, "return_response", this->return_response); + dump_field(out, ESPHOME_PSTR("return_response"), this->return_response); #endif return out.c_str(); } #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES const char *ExecuteServiceResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ExecuteServiceResponse"); - dump_field(out, "call_id", this->call_id); - dump_field(out, "success", this->success); - dump_field(out, "error_message", this->error_message); + MessageDumpHelper helper(out, ESPHOME_PSTR("ExecuteServiceResponse")); + dump_field(out, ESPHOME_PSTR("call_id"), this->call_id); + dump_field(out, ESPHOME_PSTR("success"), this->success); + dump_field(out, ESPHOME_PSTR("error_message"), this->error_message); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON - dump_bytes_field(out, "response_data", this->response_data, this->response_data_len); + dump_bytes_field(out, ESPHOME_PSTR("response_data"), this->response_data, this->response_data_len); #endif return out.c_str(); } #endif #ifdef USE_CAMERA const char *ListEntitiesCameraResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesCameraResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); - dump_field(out, "disabled_by_default", this->disabled_by_default); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesCameraResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *CameraImageResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "CameraImageResponse"); - dump_field(out, "key", this->key); - dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); - dump_field(out, "done", this->done); + MessageDumpHelper helper(out, ESPHOME_PSTR("CameraImageResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data_ptr_, this->data_len_); + dump_field(out, ESPHOME_PSTR("done"), this->done); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *CameraImageRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "CameraImageRequest"); - dump_field(out, "single", this->single); - dump_field(out, "stream", this->stream); + MessageDumpHelper helper(out, ESPHOME_PSTR("CameraImageRequest")); + dump_field(out, ESPHOME_PSTR("single"), this->single); + dump_field(out, ESPHOME_PSTR("stream"), this->stream); return out.c_str(); } #endif #ifdef USE_CLIMATE const char *ListEntitiesClimateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesClimateResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); - dump_field(out, "supports_current_temperature", this->supports_current_temperature); - dump_field(out, "supports_two_point_target_temperature", this->supports_two_point_target_temperature); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesClimateResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("supports_current_temperature"), this->supports_current_temperature); + dump_field(out, ESPHOME_PSTR("supports_two_point_target_temperature"), this->supports_two_point_target_temperature); for (const auto &it : *this->supported_modes) { - dump_field(out, "supported_modes", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("supported_modes"), static_cast(it), 4); } - dump_field(out, "visual_min_temperature", this->visual_min_temperature); - dump_field(out, "visual_max_temperature", this->visual_max_temperature); - dump_field(out, "visual_target_temperature_step", this->visual_target_temperature_step); - dump_field(out, "supports_action", this->supports_action); + dump_field(out, ESPHOME_PSTR("visual_min_temperature"), this->visual_min_temperature); + dump_field(out, ESPHOME_PSTR("visual_max_temperature"), this->visual_max_temperature); + dump_field(out, ESPHOME_PSTR("visual_target_temperature_step"), this->visual_target_temperature_step); + dump_field(out, ESPHOME_PSTR("supports_action"), this->supports_action); for (const auto &it : *this->supported_fan_modes) { - dump_field(out, "supported_fan_modes", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("supported_fan_modes"), static_cast(it), 4); } for (const auto &it : *this->supported_swing_modes) { - dump_field(out, "supported_swing_modes", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("supported_swing_modes"), static_cast(it), 4); } for (const auto &it : *this->supported_custom_fan_modes) { - dump_field(out, "supported_custom_fan_modes", it, 4); + dump_field(out, ESPHOME_PSTR("supported_custom_fan_modes"), it, 4); } for (const auto &it : *this->supported_presets) { - dump_field(out, "supported_presets", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("supported_presets"), static_cast(it), 4); } for (const auto &it : *this->supported_custom_presets) { - dump_field(out, "supported_custom_presets", it, 4); + dump_field(out, ESPHOME_PSTR("supported_custom_presets"), it, 4); } - dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "visual_current_temperature_step", this->visual_current_temperature_step); - dump_field(out, "supports_current_humidity", this->supports_current_humidity); - dump_field(out, "supports_target_humidity", this->supports_target_humidity); - dump_field(out, "visual_min_humidity", this->visual_min_humidity); - dump_field(out, "visual_max_humidity", this->visual_max_humidity); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("visual_current_temperature_step"), this->visual_current_temperature_step); + dump_field(out, ESPHOME_PSTR("supports_current_humidity"), this->supports_current_humidity); + dump_field(out, ESPHOME_PSTR("supports_target_humidity"), this->supports_target_humidity); + dump_field(out, ESPHOME_PSTR("visual_min_humidity"), this->visual_min_humidity); + dump_field(out, ESPHOME_PSTR("visual_max_humidity"), this->visual_max_humidity); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "feature_flags", this->feature_flags); + dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags); return out.c_str(); } const char *ClimateStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ClimateStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "mode", static_cast(this->mode)); - dump_field(out, "current_temperature", this->current_temperature); - dump_field(out, "target_temperature", this->target_temperature); - dump_field(out, "target_temperature_low", this->target_temperature_low); - dump_field(out, "target_temperature_high", this->target_temperature_high); - dump_field(out, "action", static_cast(this->action)); - dump_field(out, "fan_mode", static_cast(this->fan_mode)); - dump_field(out, "swing_mode", static_cast(this->swing_mode)); - dump_field(out, "custom_fan_mode", this->custom_fan_mode); - dump_field(out, "preset", static_cast(this->preset)); - dump_field(out, "custom_preset", this->custom_preset); - dump_field(out, "current_humidity", this->current_humidity); - dump_field(out, "target_humidity", this->target_humidity); + MessageDumpHelper helper(out, ESPHOME_PSTR("ClimateStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); + dump_field(out, ESPHOME_PSTR("current_temperature"), this->current_temperature); + dump_field(out, ESPHOME_PSTR("target_temperature"), this->target_temperature); + dump_field(out, ESPHOME_PSTR("target_temperature_low"), this->target_temperature_low); + dump_field(out, ESPHOME_PSTR("target_temperature_high"), this->target_temperature_high); + dump_field(out, ESPHOME_PSTR("action"), static_cast(this->action)); + dump_field(out, ESPHOME_PSTR("fan_mode"), static_cast(this->fan_mode)); + dump_field(out, ESPHOME_PSTR("swing_mode"), static_cast(this->swing_mode)); + dump_field(out, ESPHOME_PSTR("custom_fan_mode"), this->custom_fan_mode); + dump_field(out, ESPHOME_PSTR("preset"), static_cast(this->preset)); + dump_field(out, ESPHOME_PSTR("custom_preset"), this->custom_preset); + dump_field(out, ESPHOME_PSTR("current_humidity"), this->current_humidity); + dump_field(out, ESPHOME_PSTR("target_humidity"), this->target_humidity); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *ClimateCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ClimateCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_mode", this->has_mode); - dump_field(out, "mode", static_cast(this->mode)); - dump_field(out, "has_target_temperature", this->has_target_temperature); - dump_field(out, "target_temperature", this->target_temperature); - dump_field(out, "has_target_temperature_low", this->has_target_temperature_low); - dump_field(out, "target_temperature_low", this->target_temperature_low); - dump_field(out, "has_target_temperature_high", this->has_target_temperature_high); - dump_field(out, "target_temperature_high", this->target_temperature_high); - dump_field(out, "has_fan_mode", this->has_fan_mode); - dump_field(out, "fan_mode", static_cast(this->fan_mode)); - dump_field(out, "has_swing_mode", this->has_swing_mode); - dump_field(out, "swing_mode", static_cast(this->swing_mode)); - dump_field(out, "has_custom_fan_mode", this->has_custom_fan_mode); - dump_field(out, "custom_fan_mode", this->custom_fan_mode); - dump_field(out, "has_preset", this->has_preset); - dump_field(out, "preset", static_cast(this->preset)); - dump_field(out, "has_custom_preset", this->has_custom_preset); - dump_field(out, "custom_preset", this->custom_preset); - dump_field(out, "has_target_humidity", this->has_target_humidity); - dump_field(out, "target_humidity", this->target_humidity); + MessageDumpHelper helper(out, ESPHOME_PSTR("ClimateCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_mode"), this->has_mode); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); + dump_field(out, ESPHOME_PSTR("has_target_temperature"), this->has_target_temperature); + dump_field(out, ESPHOME_PSTR("target_temperature"), this->target_temperature); + dump_field(out, ESPHOME_PSTR("has_target_temperature_low"), this->has_target_temperature_low); + dump_field(out, ESPHOME_PSTR("target_temperature_low"), this->target_temperature_low); + dump_field(out, ESPHOME_PSTR("has_target_temperature_high"), this->has_target_temperature_high); + dump_field(out, ESPHOME_PSTR("target_temperature_high"), this->target_temperature_high); + dump_field(out, ESPHOME_PSTR("has_fan_mode"), this->has_fan_mode); + dump_field(out, ESPHOME_PSTR("fan_mode"), static_cast(this->fan_mode)); + dump_field(out, ESPHOME_PSTR("has_swing_mode"), this->has_swing_mode); + dump_field(out, ESPHOME_PSTR("swing_mode"), static_cast(this->swing_mode)); + dump_field(out, ESPHOME_PSTR("has_custom_fan_mode"), this->has_custom_fan_mode); + dump_field(out, ESPHOME_PSTR("custom_fan_mode"), this->custom_fan_mode); + dump_field(out, ESPHOME_PSTR("has_preset"), this->has_preset); + dump_field(out, ESPHOME_PSTR("preset"), static_cast(this->preset)); + dump_field(out, ESPHOME_PSTR("has_custom_preset"), this->has_custom_preset); + dump_field(out, ESPHOME_PSTR("custom_preset"), this->custom_preset); + dump_field(out, ESPHOME_PSTR("has_target_humidity"), this->has_target_humidity); + dump_field(out, ESPHOME_PSTR("target_humidity"), this->target_humidity); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_WATER_HEATER const char *ListEntitiesWaterHeaterResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesWaterHeaterResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesWaterHeaterResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "min_temperature", this->min_temperature); - dump_field(out, "max_temperature", this->max_temperature); - dump_field(out, "target_temperature_step", this->target_temperature_step); + dump_field(out, ESPHOME_PSTR("min_temperature"), this->min_temperature); + dump_field(out, ESPHOME_PSTR("max_temperature"), this->max_temperature); + dump_field(out, ESPHOME_PSTR("target_temperature_step"), this->target_temperature_step); for (const auto &it : *this->supported_modes) { - dump_field(out, "supported_modes", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("supported_modes"), static_cast(it), 4); } - dump_field(out, "supported_features", this->supported_features); + dump_field(out, ESPHOME_PSTR("supported_features"), this->supported_features); return out.c_str(); } const char *WaterHeaterStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "WaterHeaterStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "current_temperature", this->current_temperature); - dump_field(out, "target_temperature", this->target_temperature); - dump_field(out, "mode", static_cast(this->mode)); + MessageDumpHelper helper(out, ESPHOME_PSTR("WaterHeaterStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("current_temperature"), this->current_temperature); + dump_field(out, ESPHOME_PSTR("target_temperature"), this->target_temperature); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "state", this->state); - dump_field(out, "target_temperature_low", this->target_temperature_low); - dump_field(out, "target_temperature_high", this->target_temperature_high); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("target_temperature_low"), this->target_temperature_low); + dump_field(out, ESPHOME_PSTR("target_temperature_high"), this->target_temperature_high); return out.c_str(); } const char *WaterHeaterCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "WaterHeaterCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_fields", this->has_fields); - dump_field(out, "mode", static_cast(this->mode)); - dump_field(out, "target_temperature", this->target_temperature); + MessageDumpHelper helper(out, ESPHOME_PSTR("WaterHeaterCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_fields"), this->has_fields); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); + dump_field(out, ESPHOME_PSTR("target_temperature"), this->target_temperature); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "state", this->state); - dump_field(out, "target_temperature_low", this->target_temperature_low); - dump_field(out, "target_temperature_high", this->target_temperature_high); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("target_temperature_low"), this->target_temperature_low); + dump_field(out, ESPHOME_PSTR("target_temperature_high"), this->target_temperature_high); return out.c_str(); } #endif #ifdef USE_NUMBER const char *ListEntitiesNumberResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesNumberResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesNumberResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "min_value", this->min_value); - dump_field(out, "max_value", this->max_value); - dump_field(out, "step", this->step); - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "unit_of_measurement", this->unit_of_measurement); - dump_field(out, "mode", static_cast(this->mode)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("min_value"), this->min_value); + dump_field(out, ESPHOME_PSTR("max_value"), this->max_value); + dump_field(out, ESPHOME_PSTR("step"), this->step); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("unit_of_measurement"), this->unit_of_measurement); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *NumberStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "NumberStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("NumberStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *NumberCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "NumberCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("NumberCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_SELECT const char *ListEntitiesSelectResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesSelectResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesSelectResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif for (const auto &it : *this->options) { - dump_field(out, "options", it, 4); + dump_field(out, ESPHOME_PSTR("options"), it, 4); } - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SelectStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SelectStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SelectStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SelectCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SelectCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SelectCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_SIREN const char *ListEntitiesSirenResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesSirenResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesSirenResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); for (const auto &it : *this->tones) { - dump_field(out, "tones", it, 4); + dump_field(out, ESPHOME_PSTR("tones"), it, 4); } - dump_field(out, "supports_duration", this->supports_duration); - dump_field(out, "supports_volume", this->supports_volume); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("supports_duration"), this->supports_duration); + dump_field(out, ESPHOME_PSTR("supports_volume"), this->supports_volume); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SirenStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SirenStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SirenStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SirenCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SirenCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_state", this->has_state); - dump_field(out, "state", this->state); - dump_field(out, "has_tone", this->has_tone); - dump_field(out, "tone", this->tone); - dump_field(out, "has_duration", this->has_duration); - dump_field(out, "duration", this->duration); - dump_field(out, "has_volume", this->has_volume); - dump_field(out, "volume", this->volume); + MessageDumpHelper helper(out, ESPHOME_PSTR("SirenCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_state"), this->has_state); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("has_tone"), this->has_tone); + dump_field(out, ESPHOME_PSTR("tone"), this->tone); + dump_field(out, ESPHOME_PSTR("has_duration"), this->has_duration); + dump_field(out, ESPHOME_PSTR("duration"), this->duration); + dump_field(out, ESPHOME_PSTR("has_volume"), this->has_volume); + dump_field(out, ESPHOME_PSTR("volume"), this->volume); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_LOCK const char *ListEntitiesLockResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesLockResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesLockResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "assumed_state", this->assumed_state); - dump_field(out, "supports_open", this->supports_open); - dump_field(out, "requires_code", this->requires_code); - dump_field(out, "code_format", this->code_format); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("assumed_state"), this->assumed_state); + dump_field(out, ESPHOME_PSTR("supports_open"), this->supports_open); + dump_field(out, ESPHOME_PSTR("requires_code"), this->requires_code); + dump_field(out, ESPHOME_PSTR("code_format"), this->code_format); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *LockStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "LockStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", static_cast(this->state)); + MessageDumpHelper helper(out, ESPHOME_PSTR("LockStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), static_cast(this->state)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *LockCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "LockCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "command", static_cast(this->command)); - dump_field(out, "has_code", this->has_code); - dump_field(out, "code", this->code); + MessageDumpHelper helper(out, ESPHOME_PSTR("LockCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("command"), static_cast(this->command)); + dump_field(out, ESPHOME_PSTR("has_code"), this->has_code); + dump_field(out, ESPHOME_PSTR("code"), this->code); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_BUTTON const char *ListEntitiesButtonResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesButtonResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesButtonResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *ButtonCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ButtonCommandRequest"); - dump_field(out, "key", this->key); + MessageDumpHelper helper(out, ESPHOME_PSTR("ButtonCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_MEDIA_PLAYER const char *MediaPlayerSupportedFormat::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "MediaPlayerSupportedFormat"); - dump_field(out, "format", this->format); - dump_field(out, "sample_rate", this->sample_rate); - dump_field(out, "num_channels", this->num_channels); - dump_field(out, "purpose", static_cast(this->purpose)); - dump_field(out, "sample_bytes", this->sample_bytes); + MessageDumpHelper helper(out, ESPHOME_PSTR("MediaPlayerSupportedFormat")); + dump_field(out, ESPHOME_PSTR("format"), this->format); + dump_field(out, ESPHOME_PSTR("sample_rate"), this->sample_rate); + dump_field(out, ESPHOME_PSTR("num_channels"), this->num_channels); + dump_field(out, ESPHOME_PSTR("purpose"), static_cast(this->purpose)); + dump_field(out, ESPHOME_PSTR("sample_bytes"), this->sample_bytes); return out.c_str(); } const char *ListEntitiesMediaPlayerResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesMediaPlayerResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesMediaPlayerResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "supports_pause", this->supports_pause); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("supports_pause"), this->supports_pause); for (const auto &it : this->supported_formats) { - out.append(" supported_formats: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("supported_formats")).append(": "); it.dump_to(out); out.append("\n"); } #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "feature_flags", this->feature_flags); + dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags); return out.c_str(); } const char *MediaPlayerStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "MediaPlayerStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", static_cast(this->state)); - dump_field(out, "volume", this->volume); - dump_field(out, "muted", this->muted); + MessageDumpHelper helper(out, ESPHOME_PSTR("MediaPlayerStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), static_cast(this->state)); + dump_field(out, ESPHOME_PSTR("volume"), this->volume); + dump_field(out, ESPHOME_PSTR("muted"), this->muted); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *MediaPlayerCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "MediaPlayerCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_command", this->has_command); - dump_field(out, "command", static_cast(this->command)); - dump_field(out, "has_volume", this->has_volume); - dump_field(out, "volume", this->volume); - dump_field(out, "has_media_url", this->has_media_url); - dump_field(out, "media_url", this->media_url); - dump_field(out, "has_announcement", this->has_announcement); - dump_field(out, "announcement", this->announcement); + MessageDumpHelper helper(out, ESPHOME_PSTR("MediaPlayerCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_command"), this->has_command); + dump_field(out, ESPHOME_PSTR("command"), static_cast(this->command)); + dump_field(out, ESPHOME_PSTR("has_volume"), this->has_volume); + dump_field(out, ESPHOME_PSTR("volume"), this->volume); + dump_field(out, ESPHOME_PSTR("has_media_url"), this->has_media_url); + dump_field(out, ESPHOME_PSTR("media_url"), this->media_url); + dump_field(out, ESPHOME_PSTR("has_announcement"), this->has_announcement); + dump_field(out, ESPHOME_PSTR("announcement"), this->announcement); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_BLUETOOTH_PROXY const char *SubscribeBluetoothLEAdvertisementsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SubscribeBluetoothLEAdvertisementsRequest"); - dump_field(out, "flags", this->flags); + MessageDumpHelper helper(out, ESPHOME_PSTR("SubscribeBluetoothLEAdvertisementsRequest")); + dump_field(out, ESPHOME_PSTR("flags"), this->flags); return out.c_str(); } const char *BluetoothLERawAdvertisement::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothLERawAdvertisement"); - dump_field(out, "address", this->address); - dump_field(out, "rssi", this->rssi); - dump_field(out, "address_type", this->address_type); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothLERawAdvertisement")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("rssi"), this->rssi); + dump_field(out, ESPHOME_PSTR("address_type"), this->address_type); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } const char *BluetoothLERawAdvertisementsResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothLERawAdvertisementsResponse"); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothLERawAdvertisementsResponse")); for (uint16_t i = 0; i < this->advertisements_len; i++) { - out.append(" advertisements: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("advertisements")).append(": "); this->advertisements[i].dump_to(out); out.append("\n"); } return out.c_str(); } const char *BluetoothDeviceRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothDeviceRequest"); - dump_field(out, "address", this->address); - dump_field(out, "request_type", static_cast(this->request_type)); - dump_field(out, "has_address_type", this->has_address_type); - dump_field(out, "address_type", this->address_type); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothDeviceRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("request_type"), static_cast(this->request_type)); + dump_field(out, ESPHOME_PSTR("has_address_type"), this->has_address_type); + dump_field(out, ESPHOME_PSTR("address_type"), this->address_type); return out.c_str(); } const char *BluetoothDeviceConnectionResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothDeviceConnectionResponse"); - dump_field(out, "address", this->address); - dump_field(out, "connected", this->connected); - dump_field(out, "mtu", this->mtu); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothDeviceConnectionResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("connected"), this->connected); + dump_field(out, ESPHOME_PSTR("mtu"), this->mtu); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *BluetoothGATTGetServicesRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTGetServicesRequest"); - dump_field(out, "address", this->address); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTGetServicesRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); return out.c_str(); } const char *BluetoothGATTDescriptor::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTDescriptor"); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTDescriptor")); for (const auto &it : this->uuid) { - dump_field(out, "uuid", it, 4); + dump_field(out, ESPHOME_PSTR("uuid"), it, 4); } - dump_field(out, "handle", this->handle); - dump_field(out, "short_uuid", this->short_uuid); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_field(out, ESPHOME_PSTR("short_uuid"), this->short_uuid); return out.c_str(); } const char *BluetoothGATTCharacteristic::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTCharacteristic"); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTCharacteristic")); for (const auto &it : this->uuid) { - dump_field(out, "uuid", it, 4); + dump_field(out, ESPHOME_PSTR("uuid"), it, 4); } - dump_field(out, "handle", this->handle); - dump_field(out, "properties", this->properties); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_field(out, ESPHOME_PSTR("properties"), this->properties); for (const auto &it : this->descriptors) { - out.append(" descriptors: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("descriptors")).append(": "); it.dump_to(out); out.append("\n"); } - dump_field(out, "short_uuid", this->short_uuid); + dump_field(out, ESPHOME_PSTR("short_uuid"), this->short_uuid); return out.c_str(); } const char *BluetoothGATTService::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTService"); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTService")); for (const auto &it : this->uuid) { - dump_field(out, "uuid", it, 4); + dump_field(out, ESPHOME_PSTR("uuid"), it, 4); } - dump_field(out, "handle", this->handle); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); for (const auto &it : this->characteristics) { - out.append(" characteristics: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("characteristics")).append(": "); it.dump_to(out); out.append("\n"); } - dump_field(out, "short_uuid", this->short_uuid); + dump_field(out, ESPHOME_PSTR("short_uuid"), this->short_uuid); return out.c_str(); } const char *BluetoothGATTGetServicesResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTGetServicesResponse"); - dump_field(out, "address", this->address); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTGetServicesResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); for (const auto &it : this->services) { - out.append(" services: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("services")).append(": "); it.dump_to(out); out.append("\n"); } return out.c_str(); } const char *BluetoothGATTGetServicesDoneResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTGetServicesDoneResponse"); - dump_field(out, "address", this->address); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTGetServicesDoneResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); return out.c_str(); } const char *BluetoothGATTReadRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTReadRequest"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTReadRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); return out.c_str(); } const char *BluetoothGATTReadResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTReadResponse"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTReadResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data_ptr_, this->data_len_); return out.c_str(); } const char *BluetoothGATTWriteRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTWriteRequest"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_field(out, "response", this->response); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTWriteRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_field(out, ESPHOME_PSTR("response"), this->response); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } const char *BluetoothGATTReadDescriptorRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTReadDescriptorRequest"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTReadDescriptorRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); return out.c_str(); } const char *BluetoothGATTWriteDescriptorRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTWriteDescriptorRequest"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTWriteDescriptorRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } const char *BluetoothGATTNotifyRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTNotifyRequest"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_field(out, "enable", this->enable); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTNotifyRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_field(out, ESPHOME_PSTR("enable"), this->enable); return out.c_str(); } const char *BluetoothGATTNotifyDataResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTNotifyDataResponse"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTNotifyDataResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data_ptr_, this->data_len_); return out.c_str(); } const char *BluetoothConnectionsFreeResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothConnectionsFreeResponse"); - dump_field(out, "free", this->free); - dump_field(out, "limit", this->limit); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothConnectionsFreeResponse")); + dump_field(out, ESPHOME_PSTR("free"), this->free); + dump_field(out, ESPHOME_PSTR("limit"), this->limit); for (const auto &it : this->allocated) { - dump_field(out, "allocated", it, 4); + dump_field(out, ESPHOME_PSTR("allocated"), it, 4); } return out.c_str(); } const char *BluetoothGATTErrorResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTErrorResponse"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTErrorResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *BluetoothGATTWriteResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTWriteResponse"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTWriteResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); return out.c_str(); } const char *BluetoothGATTNotifyResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTNotifyResponse"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTNotifyResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); return out.c_str(); } const char *BluetoothDevicePairingResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothDevicePairingResponse"); - dump_field(out, "address", this->address); - dump_field(out, "paired", this->paired); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothDevicePairingResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("paired"), this->paired); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *BluetoothDeviceUnpairingResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothDeviceUnpairingResponse"); - dump_field(out, "address", this->address); - dump_field(out, "success", this->success); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothDeviceUnpairingResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("success"), this->success); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *BluetoothDeviceClearCacheResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothDeviceClearCacheResponse"); - dump_field(out, "address", this->address); - dump_field(out, "success", this->success); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothDeviceClearCacheResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("success"), this->success); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *BluetoothScannerStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothScannerStateResponse"); - dump_field(out, "state", static_cast(this->state)); - dump_field(out, "mode", static_cast(this->mode)); - dump_field(out, "configured_mode", static_cast(this->configured_mode)); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothScannerStateResponse")); + dump_field(out, ESPHOME_PSTR("state"), static_cast(this->state)); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); + dump_field(out, ESPHOME_PSTR("configured_mode"), static_cast(this->configured_mode)); return out.c_str(); } const char *BluetoothScannerSetModeRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothScannerSetModeRequest"); - dump_field(out, "mode", static_cast(this->mode)); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothScannerSetModeRequest")); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); return out.c_str(); } #endif #ifdef USE_VOICE_ASSISTANT const char *SubscribeVoiceAssistantRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SubscribeVoiceAssistantRequest"); - dump_field(out, "subscribe", this->subscribe); - dump_field(out, "flags", this->flags); + MessageDumpHelper helper(out, ESPHOME_PSTR("SubscribeVoiceAssistantRequest")); + dump_field(out, ESPHOME_PSTR("subscribe"), this->subscribe); + dump_field(out, ESPHOME_PSTR("flags"), this->flags); return out.c_str(); } const char *VoiceAssistantAudioSettings::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantAudioSettings"); - dump_field(out, "noise_suppression_level", this->noise_suppression_level); - dump_field(out, "auto_gain", this->auto_gain); - dump_field(out, "volume_multiplier", this->volume_multiplier); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantAudioSettings")); + dump_field(out, ESPHOME_PSTR("noise_suppression_level"), this->noise_suppression_level); + dump_field(out, ESPHOME_PSTR("auto_gain"), this->auto_gain); + dump_field(out, ESPHOME_PSTR("volume_multiplier"), this->volume_multiplier); return out.c_str(); } const char *VoiceAssistantRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantRequest"); - dump_field(out, "start", this->start); - dump_field(out, "conversation_id", this->conversation_id); - dump_field(out, "flags", this->flags); - out.append(" audio_settings: "); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantRequest")); + dump_field(out, ESPHOME_PSTR("start"), this->start); + dump_field(out, ESPHOME_PSTR("conversation_id"), this->conversation_id); + dump_field(out, ESPHOME_PSTR("flags"), this->flags); + out.append(2, ' ').append_p(ESPHOME_PSTR("audio_settings")).append(": "); this->audio_settings.dump_to(out); out.append("\n"); - dump_field(out, "wake_word_phrase", this->wake_word_phrase); + dump_field(out, ESPHOME_PSTR("wake_word_phrase"), this->wake_word_phrase); return out.c_str(); } const char *VoiceAssistantResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantResponse"); - dump_field(out, "port", this->port); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantResponse")); + dump_field(out, ESPHOME_PSTR("port"), this->port); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *VoiceAssistantEventData::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantEventData"); - dump_field(out, "name", this->name); - dump_field(out, "value", this->value); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantEventData")); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("value"), this->value); return out.c_str(); } const char *VoiceAssistantEventResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantEventResponse"); - dump_field(out, "event_type", static_cast(this->event_type)); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantEventResponse")); + dump_field(out, ESPHOME_PSTR("event_type"), static_cast(this->event_type)); for (const auto &it : this->data) { - out.append(" data: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("data")).append(": "); it.dump_to(out); out.append("\n"); } return out.c_str(); } const char *VoiceAssistantAudio::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantAudio"); - dump_bytes_field(out, "data", this->data, this->data_len); - dump_field(out, "end", this->end); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantAudio")); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); + dump_field(out, ESPHOME_PSTR("end"), this->end); return out.c_str(); } const char *VoiceAssistantTimerEventResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantTimerEventResponse"); - dump_field(out, "event_type", static_cast(this->event_type)); - dump_field(out, "timer_id", this->timer_id); - dump_field(out, "name", this->name); - dump_field(out, "total_seconds", this->total_seconds); - dump_field(out, "seconds_left", this->seconds_left); - dump_field(out, "is_active", this->is_active); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantTimerEventResponse")); + dump_field(out, ESPHOME_PSTR("event_type"), static_cast(this->event_type)); + dump_field(out, ESPHOME_PSTR("timer_id"), this->timer_id); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("total_seconds"), this->total_seconds); + dump_field(out, ESPHOME_PSTR("seconds_left"), this->seconds_left); + dump_field(out, ESPHOME_PSTR("is_active"), this->is_active); return out.c_str(); } const char *VoiceAssistantAnnounceRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantAnnounceRequest"); - dump_field(out, "media_id", this->media_id); - dump_field(out, "text", this->text); - dump_field(out, "preannounce_media_id", this->preannounce_media_id); - dump_field(out, "start_conversation", this->start_conversation); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantAnnounceRequest")); + dump_field(out, ESPHOME_PSTR("media_id"), this->media_id); + dump_field(out, ESPHOME_PSTR("text"), this->text); + dump_field(out, ESPHOME_PSTR("preannounce_media_id"), this->preannounce_media_id); + dump_field(out, ESPHOME_PSTR("start_conversation"), this->start_conversation); return out.c_str(); } const char *VoiceAssistantAnnounceFinished::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantAnnounceFinished"); - dump_field(out, "success", this->success); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantAnnounceFinished")); + dump_field(out, ESPHOME_PSTR("success"), this->success); return out.c_str(); } const char *VoiceAssistantWakeWord::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantWakeWord"); - dump_field(out, "id", this->id); - dump_field(out, "wake_word", this->wake_word); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantWakeWord")); + dump_field(out, ESPHOME_PSTR("id"), this->id); + dump_field(out, ESPHOME_PSTR("wake_word"), this->wake_word); for (const auto &it : this->trained_languages) { - dump_field(out, "trained_languages", it, 4); + dump_field(out, ESPHOME_PSTR("trained_languages"), it, 4); } return out.c_str(); } const char *VoiceAssistantExternalWakeWord::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantExternalWakeWord"); - dump_field(out, "id", this->id); - dump_field(out, "wake_word", this->wake_word); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantExternalWakeWord")); + dump_field(out, ESPHOME_PSTR("id"), this->id); + dump_field(out, ESPHOME_PSTR("wake_word"), this->wake_word); for (const auto &it : this->trained_languages) { - dump_field(out, "trained_languages", it, 4); + dump_field(out, ESPHOME_PSTR("trained_languages"), it, 4); } - dump_field(out, "model_type", this->model_type); - dump_field(out, "model_size", this->model_size); - dump_field(out, "model_hash", this->model_hash); - dump_field(out, "url", this->url); + dump_field(out, ESPHOME_PSTR("model_type"), this->model_type); + dump_field(out, ESPHOME_PSTR("model_size"), this->model_size); + dump_field(out, ESPHOME_PSTR("model_hash"), this->model_hash); + dump_field(out, ESPHOME_PSTR("url"), this->url); return out.c_str(); } const char *VoiceAssistantConfigurationRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantConfigurationRequest"); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantConfigurationRequest")); for (const auto &it : this->external_wake_words) { - out.append(" external_wake_words: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("external_wake_words")).append(": "); it.dump_to(out); out.append("\n"); } return out.c_str(); } const char *VoiceAssistantConfigurationResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantConfigurationResponse"); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantConfigurationResponse")); for (const auto &it : this->available_wake_words) { - out.append(" available_wake_words: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("available_wake_words")).append(": "); it.dump_to(out); out.append("\n"); } for (const auto &it : *this->active_wake_words) { - dump_field(out, "active_wake_words", it, 4); + dump_field(out, ESPHOME_PSTR("active_wake_words"), it, 4); } - dump_field(out, "max_active_wake_words", this->max_active_wake_words); + dump_field(out, ESPHOME_PSTR("max_active_wake_words"), this->max_active_wake_words); return out.c_str(); } const char *VoiceAssistantSetConfiguration::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantSetConfiguration"); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantSetConfiguration")); for (const auto &it : this->active_wake_words) { - dump_field(out, "active_wake_words", it, 4); + dump_field(out, ESPHOME_PSTR("active_wake_words"), it, 4); } return out.c_str(); } #endif #ifdef USE_ALARM_CONTROL_PANEL const char *ListEntitiesAlarmControlPanelResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesAlarmControlPanelResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesAlarmControlPanelResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "supported_features", this->supported_features); - dump_field(out, "requires_code", this->requires_code); - dump_field(out, "requires_code_to_arm", this->requires_code_to_arm); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("supported_features"), this->supported_features); + dump_field(out, ESPHOME_PSTR("requires_code"), this->requires_code); + dump_field(out, ESPHOME_PSTR("requires_code_to_arm"), this->requires_code_to_arm); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *AlarmControlPanelStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "AlarmControlPanelStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", static_cast(this->state)); + MessageDumpHelper helper(out, ESPHOME_PSTR("AlarmControlPanelStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), static_cast(this->state)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *AlarmControlPanelCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "AlarmControlPanelCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "command", static_cast(this->command)); - dump_field(out, "code", this->code); + MessageDumpHelper helper(out, ESPHOME_PSTR("AlarmControlPanelCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("command"), static_cast(this->command)); + dump_field(out, ESPHOME_PSTR("code"), this->code); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_TEXT const char *ListEntitiesTextResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesTextResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesTextResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "min_length", this->min_length); - dump_field(out, "max_length", this->max_length); - dump_field(out, "pattern", this->pattern); - dump_field(out, "mode", static_cast(this->mode)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("min_length"), this->min_length); + dump_field(out, ESPHOME_PSTR("max_length"), this->max_length); + dump_field(out, ESPHOME_PSTR("pattern"), this->pattern); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *TextStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "TextStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("TextStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *TextCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "TextCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("TextCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_DATETIME_DATE const char *ListEntitiesDateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesDateResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesDateResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *DateStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DateStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "missing_state", this->missing_state); - dump_field(out, "year", this->year); - dump_field(out, "month", this->month); - dump_field(out, "day", this->day); + MessageDumpHelper helper(out, ESPHOME_PSTR("DateStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); + dump_field(out, ESPHOME_PSTR("year"), this->year); + dump_field(out, ESPHOME_PSTR("month"), this->month); + dump_field(out, ESPHOME_PSTR("day"), this->day); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *DateCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DateCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "year", this->year); - dump_field(out, "month", this->month); - dump_field(out, "day", this->day); + MessageDumpHelper helper(out, ESPHOME_PSTR("DateCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("year"), this->year); + dump_field(out, ESPHOME_PSTR("month"), this->month); + dump_field(out, ESPHOME_PSTR("day"), this->day); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_DATETIME_TIME const char *ListEntitiesTimeResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesTimeResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesTimeResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *TimeStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "TimeStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "missing_state", this->missing_state); - dump_field(out, "hour", this->hour); - dump_field(out, "minute", this->minute); - dump_field(out, "second", this->second); + MessageDumpHelper helper(out, ESPHOME_PSTR("TimeStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); + dump_field(out, ESPHOME_PSTR("hour"), this->hour); + dump_field(out, ESPHOME_PSTR("minute"), this->minute); + dump_field(out, ESPHOME_PSTR("second"), this->second); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *TimeCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "TimeCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "hour", this->hour); - dump_field(out, "minute", this->minute); - dump_field(out, "second", this->second); + MessageDumpHelper helper(out, ESPHOME_PSTR("TimeCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("hour"), this->hour); + dump_field(out, ESPHOME_PSTR("minute"), this->minute); + dump_field(out, ESPHOME_PSTR("second"), this->second); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_EVENT const char *ListEntitiesEventResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesEventResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesEventResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); for (const auto &it : *this->event_types) { - dump_field(out, "event_types", it, 4); + dump_field(out, ESPHOME_PSTR("event_types"), it, 4); } #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *EventResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "EventResponse"); - dump_field(out, "key", this->key); - dump_field(out, "event_type", this->event_type); + MessageDumpHelper helper(out, ESPHOME_PSTR("EventResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("event_type"), this->event_type); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_VALVE const char *ListEntitiesValveResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesValveResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesValveResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class); - dump_field(out, "assumed_state", this->assumed_state); - dump_field(out, "supports_position", this->supports_position); - dump_field(out, "supports_stop", this->supports_stop); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); + dump_field(out, ESPHOME_PSTR("assumed_state"), this->assumed_state); + dump_field(out, ESPHOME_PSTR("supports_position"), this->supports_position); + dump_field(out, ESPHOME_PSTR("supports_stop"), this->supports_stop); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *ValveStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ValveStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "position", this->position); - dump_field(out, "current_operation", static_cast(this->current_operation)); + MessageDumpHelper helper(out, ESPHOME_PSTR("ValveStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("position"), this->position); + dump_field(out, ESPHOME_PSTR("current_operation"), static_cast(this->current_operation)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *ValveCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ValveCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_position", this->has_position); - dump_field(out, "position", this->position); - dump_field(out, "stop", this->stop); + MessageDumpHelper helper(out, ESPHOME_PSTR("ValveCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_position"), this->has_position); + dump_field(out, ESPHOME_PSTR("position"), this->position); + dump_field(out, ESPHOME_PSTR("stop"), this->stop); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_DATETIME_DATETIME const char *ListEntitiesDateTimeResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesDateTimeResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesDateTimeResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *DateTimeStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DateTimeStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "missing_state", this->missing_state); - dump_field(out, "epoch_seconds", this->epoch_seconds); + MessageDumpHelper helper(out, ESPHOME_PSTR("DateTimeStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); + dump_field(out, ESPHOME_PSTR("epoch_seconds"), this->epoch_seconds); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *DateTimeCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DateTimeCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "epoch_seconds", this->epoch_seconds); + MessageDumpHelper helper(out, ESPHOME_PSTR("DateTimeCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("epoch_seconds"), this->epoch_seconds); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_UPDATE const char *ListEntitiesUpdateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesUpdateResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesUpdateResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *UpdateStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "UpdateStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "missing_state", this->missing_state); - dump_field(out, "in_progress", this->in_progress); - dump_field(out, "has_progress", this->has_progress); - dump_field(out, "progress", this->progress); - dump_field(out, "current_version", this->current_version); - dump_field(out, "latest_version", this->latest_version); - dump_field(out, "title", this->title); - dump_field(out, "release_summary", this->release_summary); - dump_field(out, "release_url", this->release_url); + MessageDumpHelper helper(out, ESPHOME_PSTR("UpdateStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); + dump_field(out, ESPHOME_PSTR("in_progress"), this->in_progress); + dump_field(out, ESPHOME_PSTR("has_progress"), this->has_progress); + dump_field(out, ESPHOME_PSTR("progress"), this->progress); + dump_field(out, ESPHOME_PSTR("current_version"), this->current_version); + dump_field(out, ESPHOME_PSTR("latest_version"), this->latest_version); + dump_field(out, ESPHOME_PSTR("title"), this->title); + dump_field(out, ESPHOME_PSTR("release_summary"), this->release_summary); + dump_field(out, ESPHOME_PSTR("release_url"), this->release_url); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *UpdateCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "UpdateCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "command", static_cast(this->command)); + MessageDumpHelper helper(out, ESPHOME_PSTR("UpdateCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("command"), static_cast(this->command)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_ZWAVE_PROXY const char *ZWaveProxyFrame::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ZWaveProxyFrame"); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("ZWaveProxyFrame")); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } const char *ZWaveProxyRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ZWaveProxyRequest"); - dump_field(out, "type", static_cast(this->type)); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("ZWaveProxyRequest")); + dump_field(out, ESPHOME_PSTR("type"), static_cast(this->type)); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } #endif #ifdef USE_INFRARED const char *ListEntitiesInfraredResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesInfraredResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesInfraredResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "capabilities", this->capabilities); + dump_field(out, ESPHOME_PSTR("capabilities"), this->capabilities); return out.c_str(); } #endif #ifdef USE_IR_RF const char *InfraredRFTransmitRawTimingsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "InfraredRFTransmitRawTimingsRequest"); + MessageDumpHelper helper(out, ESPHOME_PSTR("InfraredRFTransmitRawTimingsRequest")); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "key", this->key); - dump_field(out, "carrier_frequency", this->carrier_frequency); - dump_field(out, "repeat_count", this->repeat_count); - out.append(" timings: "); - out.append("packed buffer ["); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("carrier_frequency"), this->carrier_frequency); + dump_field(out, ESPHOME_PSTR("repeat_count"), this->repeat_count); + out.append(2, ' ').append_p(ESPHOME_PSTR("timings")).append(": "); + out.append_p(ESPHOME_PSTR("packed buffer [")); append_uint(out, this->timings_count_); - out.append(" values, "); + out.append_p(ESPHOME_PSTR(" values, ")); append_uint(out, this->timings_length_); - out.append(" bytes]\n"); + out.append_p(ESPHOME_PSTR(" bytes]\n")); return out.c_str(); } const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "InfraredRFReceiveEvent"); + MessageDumpHelper helper(out, ESPHOME_PSTR("InfraredRFReceiveEvent")); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "key", this->key); + dump_field(out, ESPHOME_PSTR("key"), this->key); for (const auto &it : *this->timings) { - dump_field(out, "timings", it, 4); + dump_field(out, ESPHOME_PSTR("timings"), it, 4); } return out.c_str(); } #endif #ifdef USE_SERIAL_PROXY const char *SerialProxyConfigureRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyConfigureRequest"); - dump_field(out, "instance", this->instance); - dump_field(out, "baudrate", this->baudrate); - dump_field(out, "flow_control", this->flow_control); - dump_field(out, "parity", static_cast(this->parity)); - dump_field(out, "stop_bits", this->stop_bits); - dump_field(out, "data_size", this->data_size); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyConfigureRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("baudrate"), this->baudrate); + dump_field(out, ESPHOME_PSTR("flow_control"), this->flow_control); + dump_field(out, ESPHOME_PSTR("parity"), static_cast(this->parity)); + dump_field(out, ESPHOME_PSTR("stop_bits"), this->stop_bits); + dump_field(out, ESPHOME_PSTR("data_size"), this->data_size); return out.c_str(); } const char *SerialProxyDataReceived::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyDataReceived"); - dump_field(out, "instance", this->instance); - dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyDataReceived")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data_ptr_, this->data_len_); return out.c_str(); } const char *SerialProxyWriteRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyWriteRequest"); - dump_field(out, "instance", this->instance); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyWriteRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } const char *SerialProxySetModemPinsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxySetModemPinsRequest"); - dump_field(out, "instance", this->instance); - dump_field(out, "line_states", this->line_states); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxySetModemPinsRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("line_states"), this->line_states); return out.c_str(); } const char *SerialProxyGetModemPinsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyGetModemPinsRequest"); - dump_field(out, "instance", this->instance); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetModemPinsRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); return out.c_str(); } const char *SerialProxyGetModemPinsResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyGetModemPinsResponse"); - dump_field(out, "instance", this->instance); - dump_field(out, "line_states", this->line_states); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetModemPinsResponse")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("line_states"), this->line_states); return out.c_str(); } const char *SerialProxyRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyRequest"); - dump_field(out, "instance", this->instance); - dump_field(out, "type", static_cast(this->type)); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("type"), static_cast(this->type)); return out.c_str(); } const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyRequestResponse"); - dump_field(out, "instance", this->instance); - dump_field(out, "type", static_cast(this->type)); - dump_field(out, "status", static_cast(this->status)); - dump_field(out, "error_message", this->error_message); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyRequestResponse")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("type"), static_cast(this->type)); + dump_field(out, ESPHOME_PSTR("status"), static_cast(this->status)); + dump_field(out, ESPHOME_PSTR("error_message"), this->error_message); return out.c_str(); } #endif #ifdef USE_BLUETOOTH_PROXY const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothSetConnectionParamsRequest"); - dump_field(out, "address", this->address); - dump_field(out, "min_interval", this->min_interval); - dump_field(out, "max_interval", this->max_interval); - dump_field(out, "latency", this->latency); - dump_field(out, "timeout", this->timeout); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothSetConnectionParamsRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("min_interval"), this->min_interval); + dump_field(out, ESPHOME_PSTR("max_interval"), this->max_interval); + dump_field(out, ESPHOME_PSTR("latency"), this->latency); + dump_field(out, ESPHOME_PSTR("timeout"), this->timeout); return out.c_str(); } const char *BluetoothSetConnectionParamsResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothSetConnectionParamsResponse"); - dump_field(out, "address", this->address); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothSetConnectionParamsResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } #endif diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index d86cf912db..b41233eddd 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -9,8 +9,8 @@ namespace esphome::api { static const char *const TAG = "api.service"; #ifdef HAS_PROTO_MESSAGE_DUMP -void APIServerConnectionBase::log_send_message_(const char *name, const char *dump) { - ESP_LOGVV(TAG, "send_message %s: %s", name, dump); +void APIServerConnectionBase::log_send_message_(const LogString *name, const char *dump) { + ESP_LOGVV(TAG, "send_message %s: %s", LOG_STR_ARG(name), dump); } void APIServerConnectionBase::log_receive_message_(const LogString *name, const ProtoMessage &msg) { DumpBuffer dump_buf; diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 4925a6497a..6ff988902f 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -12,7 +12,7 @@ class APIServerConnectionBase { public: #ifdef HAS_PROTO_MESSAGE_DUMP protected: - void log_send_message_(const char *name, const char *dump); + void log_send_message_(const LogString *name, const char *dump); void log_receive_message_(const LogString *name, const ProtoMessage &msg); void log_receive_message_(const LogString *name); diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index d6f9c947d7..95a79105a3 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -5,6 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "esphome/core/string_ref.h" #include @@ -400,6 +401,23 @@ class DumpBuffer { return *this; } + /// Append a PROGMEM string (flash-safe on ESP8266, regular append on other platforms) + DumpBuffer &append_p(const char *str) { + if (str) { +#ifdef USE_ESP8266 + append_p_esp8266(str); +#else + append_impl_(str, strlen(str)); +#endif + } + return *this; + } + +#ifdef USE_ESP8266 + /// Out-of-line ESP8266 PROGMEM append to avoid inlining strlen_P/memcpy_P at every call site + void append_p_esp8266(const char *str); +#endif + const char *c_str() const { return buf_; } size_t size() const { return pos_; } @@ -445,7 +463,7 @@ class ProtoMessage { uint32_t calculate_size() const { return 0; } #ifdef HAS_PROTO_MESSAGE_DUMP virtual const char *dump_to(DumpBuffer &out) const = 0; - virtual const char *message_name() const { return "unknown"; } + virtual const LogString *message_name() const { return LOG_STR("unknown"); } #endif #ifndef USE_HOST diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 0bb569fdb5..81ea93caf4 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -248,7 +248,7 @@ class TypeInfo(ABC): @property def dump_content(self) -> str: # Default implementation - subclasses can override if they need special handling - return f'dump_field(out, "{self.name}", {self.dump_field_value(f"this->{self.field_name}")});' + return f'dump_field(out, ESPHOME_PSTR("{self.name}"), {self.dump_field_value(f"this->{self.field_name}")});' @abstractmethod def dump(self, name: str) -> str: @@ -665,14 +665,14 @@ class StringType(TypeInfo): def dump_content(self) -> str: # For SOURCE_CLIENT only, use std::string if not self._needs_encode: - return f'dump_field(out, "{self.name}", this->{self.field_name});' + return f'dump_field(out, ESPHOME_PSTR("{self.name}"), this->{self.field_name});' # For SOURCE_SERVER, use StringRef with _ref_ suffix if not self._needs_decode: - return f'dump_field(out, "{self.name}", this->{self.field_name}_ref_);' + return f'dump_field(out, ESPHOME_PSTR("{self.name}"), this->{self.field_name}_ref_);' # For SOURCE_BOTH, we need custom logic - o = f'out.append(" {self.name}: ");\n' + o = f'out.append(2, \' \').append_p(ESPHOME_PSTR("{self.name}")).append(": ");\n' o += self.dump(f"this->{self.field_name}") + "\n" o += 'out.append("\\n");' return o @@ -745,7 +745,7 @@ class MessageType(TypeInfo): @property def dump_content(self) -> str: - o = f'out.append(" {self.name}: ");\n' + o = f'out.append(2, \' \').append_p(ESPHOME_PSTR("{self.name}")).append(": ");\n' o += f"this->{self.field_name}.dump_to(out);\n" o += 'out.append("\\n");' return o @@ -831,7 +831,7 @@ class BytesType(TypeInfo): # For SOURCE_CLIENT only, always use std::string if not self._needs_encode: return ( - f'dump_bytes_field(out, "{self.name}", ' + f'dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), ' f"reinterpret_cast(this->{self.field_name}.data()), " f"this->{self.field_name}.size());" ) @@ -839,17 +839,17 @@ class BytesType(TypeInfo): # For SOURCE_SERVER, always use pointer/length if not self._needs_decode: return ( - f'dump_bytes_field(out, "{self.name}", ' + f'dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), ' f"this->{self.field_name}_ptr_, this->{self.field_name}_len_);" ) # For SOURCE_BOTH, check if pointer is set (sending) or use string (received) return ( f"if (this->{self.field_name}_ptr_ != nullptr) {{\n" - f' dump_bytes_field(out, "{self.name}", ' + f' dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), ' f"this->{self.field_name}_ptr_, this->{self.field_name}_len_);\n" f"}} else {{\n" - f' dump_bytes_field(out, "{self.name}", ' + f' dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), ' f"reinterpret_cast(this->{self.field_name}.data()), " f"this->{self.field_name}.size());\n" f"}}" @@ -928,7 +928,7 @@ class PointerToBytesBufferType(PointerToBufferTypeBase): @property def dump_content(self) -> str: return ( - f'dump_bytes_field(out, "{self.name}", ' + f'dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), ' f"this->{self.field_name}, this->{self.field_name}_len);" ) @@ -976,7 +976,7 @@ class PointerToStringBufferType(PointerToBufferTypeBase): @property def dump_content(self) -> str: - return f'dump_field(out, "{self.name}", this->{self.field_name});' + return f'dump_field(out, ESPHOME_PSTR("{self.name}"), this->{self.field_name});' def get_size_calculation(self, name: str, force: bool = False) -> str: return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}.size());" @@ -1036,12 +1036,12 @@ class PackedBufferTypeInfo(TypeInfo): def dump_content(self) -> str: """Dump shows buffer info but not decoded values.""" return ( - f'out.append(" {self.name}: ");\n' - + 'out.append("packed buffer [");\n' + f'out.append(2, \' \').append_p(ESPHOME_PSTR("{self.name}")).append(": ");\n' + + 'out.append_p(ESPHOME_PSTR("packed buffer ["));\n' + f"append_uint(out, this->{self.field_name}_count_);\n" - + 'out.append(" values, ");\n' + + 'out.append_p(ESPHOME_PSTR(" values, "));\n' + f"append_uint(out, this->{self.field_name}_length_);\n" - + 'out.append(" bytes]\\n");' + + 'out.append_p(ESPHOME_PSTR(" bytes]\\n"));' ) def dump(self, name: str) -> str: @@ -1134,7 +1134,7 @@ class FixedArrayBytesType(TypeInfo): @property def dump_content(self) -> str: return ( - f'dump_bytes_field(out, "{self.name}", ' + f'dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), ' f"this->{self.field_name}, this->{self.field_name}_len);" ) @@ -1204,7 +1204,7 @@ class EnumType(TypeInfo): return f"buffer.{self.encode_func}({self.number}, static_cast(this->{self.field_name}));" def dump(self, name: str) -> str: - return f"out.append(proto_enum_to_string<{self.cpp_type}>({name}));" + return f"out.append_p(proto_enum_to_string<{self.cpp_type}>({name}));" def dump_field_value(self, value: str) -> str: # Enums need explicit cast for the template @@ -1326,15 +1326,15 @@ def _generate_array_dump_content( # Check if underlying type can use dump_field if is_const_char_ptr: # Special case for const char* - use it directly - o += f' dump_field(out, "{name}", it, 4);\n' + o += f' dump_field(out, ESPHOME_PSTR("{name}"), it, 4);\n' elif ti.can_use_dump_field(): # For types that have dump_field overloads, use them with extra indent # std::vector iterators return proxy objects, need explicit cast value_expr = "static_cast(it)" if is_bool else ti.dump_field_value("it") - o += f' dump_field(out, "{name}", {value_expr}, 4);\n' + o += f' dump_field(out, ESPHOME_PSTR("{name}"), {value_expr}, 4);\n' else: # For complex types (messages, bytes), use the old pattern - o += f' out.append(" {name}: ");\n' + o += f' out.append(4, \' \').append_p(ESPHOME_PSTR("{name}")).append(": ");\n' o += indent(ti.dump("it")) + "\n" o += ' out.append("\\n");\n' o += "}" @@ -1543,9 +1543,9 @@ class FixedArrayWithLengthRepeatedType(FixedArrayRepeatedType): o = f"for (uint16_t i = 0; i < this->{self.field_name}_len; i++) {{\n" # Check if underlying type can use dump_field if self._ti.can_use_dump_field(): - o += f' dump_field(out, "{self.name}", {self._ti.dump_field_value(f"this->{self.field_name}[i]")}, 4);\n' + o += f' dump_field(out, ESPHOME_PSTR("{self.name}"), {self._ti.dump_field_value(f"this->{self.field_name}[i]")}, 4);\n' else: - o += f' out.append(" {self.name}: ");\n' + o += f' out.append(4, \' \').append_p(ESPHOME_PSTR("{self.name}")).append(": ");\n' o += indent(self._ti.dump(f"this->{self.field_name}[i]")) + "\n" o += ' out.append("\\n");\n' o += "}" @@ -2023,9 +2023,9 @@ def build_enum_type(desc, enum_ifdef_map) -> tuple[str, str, str]: dump_cpp += " switch (value) {\n" for v in desc.value: dump_cpp += f" case enums::{v.name}:\n" - dump_cpp += f' return "{v.name}";\n' + dump_cpp += f' return ESPHOME_PSTR("{v.name}");\n' dump_cpp += " default:\n" - dump_cpp += ' return "UNKNOWN";\n' + dump_cpp += ' return ESPHOME_PSTR("UNKNOWN");\n' dump_cpp += " }\n" dump_cpp += "}\n" @@ -2107,7 +2107,7 @@ def build_message_type( public_content.append("#ifdef HAS_PROTO_MESSAGE_DUMP") snake_name = camel_to_snake(desc.name) public_content.append( - f'const char *message_name() const override {{ return "{snake_name}"; }}' + f'const LogString *message_name() const override {{ return LOG_STR("{snake_name}"); }}' ) public_content.append("#endif") @@ -2315,12 +2315,12 @@ def build_message_type( if dump: # Always use MessageDumpHelper for consistent output formatting dump_impl += "\n" - dump_impl += f' MessageDumpHelper helper(out, "{desc.name}");\n' + dump_impl += f' MessageDumpHelper helper(out, ESPHOME_PSTR("{desc.name}"));\n' dump_impl += indent("\n".join(dump)) + "\n" dump_impl += " return out.c_str();\n" else: dump_impl += "\n" - dump_impl += f' out.append("{desc.name} {{}}");\n' + dump_impl += f' out.append_p(ESPHOME_PSTR("{desc.name} {{}}"));\n' dump_impl += " return out.c_str();\n" dump_impl += "}\n" @@ -2707,6 +2707,7 @@ namespace esphome::api { dump_cpp += """\ #include "api_pb2.h" #include "esphome/core/helpers.h" +#include "esphome/core/progmem.h" #include @@ -2714,6 +2715,21 @@ namespace esphome::api { namespace esphome::api { +#ifdef USE_ESP8266 +// Out-of-line to avoid inlining strlen_P/memcpy_P at every call site +void DumpBuffer::append_p_esp8266(const char *str) { + size_t len = strlen_P(str); + size_t space = CAPACITY - 1 - pos_; + if (len > space) + len = space; + if (len > 0) { + memcpy_P(buf_ + pos_, str, len); + pos_ += len; + buf_[pos_] = '\\0'; + } +} +#endif + // Helper function to append a quoted string, handling empty StringRef static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) { out.append("'"); @@ -2724,8 +2740,9 @@ static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) { } // Common helpers for dump_field functions +// field_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) static inline void append_field_prefix(DumpBuffer &out, const char *field_name, int indent) { - out.append(indent, ' ').append(field_name).append(": "); + out.append(indent, ' ').append_p(field_name).append(": "); } static inline void append_uint(DumpBuffer &out, uint32_t value) { @@ -2733,10 +2750,11 @@ static inline void append_uint(DumpBuffer &out, uint32_t value) { } // RAII helper for message dump formatting +// message_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) class MessageDumpHelper { public: MessageDumpHelper(DumpBuffer &out, const char *message_name) : out_(out) { - out_.append(message_name); + out_.append_p(message_name); out_.append(" {\\n"); } ~MessageDumpHelper() { out_.append(" }"); } @@ -2746,6 +2764,10 @@ class MessageDumpHelper { }; // Helper functions to reduce code duplication in dump methods +// field_name parameters are PROGMEM pointers (flash on ESP8266, regular pointers on other platforms) +// Not all overloads are used in every build (depends on enabled components) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" static void dump_field(DumpBuffer &out, const char *field_name, int32_t value, int indent = 2) { append_field_prefix(out, field_name, indent); out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRId32 "\\n", value)); @@ -2790,21 +2812,23 @@ static void dump_field(DumpBuffer &out, const char *field_name, const char *valu out.append("\\n"); } -template -static void dump_field(DumpBuffer &out, const char *field_name, T value, int indent = 2) { +// proto_enum_to_string returns PROGMEM pointers, so use append_p +template static void dump_field(DumpBuffer &out, const char *field_name, T value, int indent = 2) { append_field_prefix(out, field_name, indent); - out.append(proto_enum_to_string(value)); + out.append_p(proto_enum_to_string(value)); out.append("\\n"); } // Helper for bytes fields - uses stack buffer to avoid heap allocation // Buffer sized for 160 bytes of data (480 chars with separators) to fit typical log buffer +// field_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint8_t *data, size_t len, int indent = 2) { char hex_buf[format_hex_pretty_size(160)]; append_field_prefix(out, field_name, indent); format_hex_pretty_to(hex_buf, data, len); out.append(hex_buf).append("\\n"); } +#pragma GCC diagnostic pop """ @@ -2977,7 +3001,7 @@ static const char *const TAG = "api.service"; # Add logging helper method declarations hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" hpp += " protected:\n" - hpp += " void log_send_message_(const char *name, const char *dump);\n" + hpp += " void log_send_message_(const LogString *name, const char *dump);\n" hpp += ( " void log_receive_message_(const LogString *name, const ProtoMessage &msg);\n" ) @@ -2990,10 +3014,8 @@ static const char *const TAG = "api.service"; # Add logging helper method implementations to cpp cpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" - cpp += ( - f"void {class_name}::log_send_message_(const char *name, const char *dump) {{\n" - ) - cpp += ' ESP_LOGVV(TAG, "send_message %s: %s", name, dump);\n' + cpp += f"void {class_name}::log_send_message_(const LogString *name, const char *dump) {{\n" + cpp += ' ESP_LOGVV(TAG, "send_message %s: %s", LOG_STR_ARG(name), dump);\n' cpp += "}\n" cpp += f"void {class_name}::log_receive_message_(const LogString *name, const ProtoMessage &msg) {{\n" cpp += " DumpBuffer dump_buf;\n" From 13d3968d9b9220fd777b953aac8efbbba1819013 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 13:41:09 -1000 Subject: [PATCH 006/115] [api] Avoid heap allocation in PSK update timeout lambda (#14921) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/api_server.cpp | 28 ++++++++++++++++++--------- esphome/components/api/api_server.h | 4 +++- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 17d69405ad..1151bc5983 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -46,10 +46,8 @@ void APIServer::setup() { #ifndef USE_API_NOISE_PSK_FROM_YAML // Only load saved PSK if not set from YAML - SavedNoisePsk noise_pref_saved{}; - if (this->noise_pref_.load(&noise_pref_saved)) { + if (this->load_and_apply_noise_psk_()) { ESP_LOGD(TAG, "Loaded saved Noise PSK"); - this->set_noise_psk(noise_pref_saved.psk); } #endif #endif @@ -514,7 +512,7 @@ void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeo #ifdef USE_API_NOISE bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, - const LogString *fail_log_msg, const psk_t &active_psk, bool make_active) { + const LogString *fail_log_msg, bool make_active) { if (!this->noise_pref_.save(&new_psk)) { ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg)); return false; @@ -526,9 +524,14 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString } ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg)); if (make_active) { - this->set_timeout(100, [this, active_psk]() { + this->set_timeout(100, [this]() { + // Re-read the PSK from preferences rather than capturing the 32-byte array + // in the lambda (which would exceed std::function SBO and heap-allocate). + if (!this->load_and_apply_noise_psk_()) { + ESP_LOGW(TAG, "Failed to load saved PSK for activation"); + return; + } ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); - this->set_noise_psk(active_psk); for (auto &c : this->clients_) { DisconnectRequest req; c->send_message(req); @@ -538,6 +541,14 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString return true; } +bool APIServer::load_and_apply_noise_psk_() { + SavedNoisePsk saved{}; + if (!this->noise_pref_.load(&saved)) + return false; + this->set_noise_psk(saved.psk); + return true; +} + bool APIServer::save_noise_psk(psk_t psk, bool make_active) { #ifdef USE_API_NOISE_PSK_FROM_YAML // When PSK is set from YAML, this function should never be called @@ -552,7 +563,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { } SavedNoisePsk new_saved_psk{psk}; - return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), psk, + return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), make_active); #endif } @@ -564,8 +575,7 @@ bool APIServer::clear_noise_psk(bool make_active) { return false; #else SavedNoisePsk empty_psk{}; - psk_t empty{}; - return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), empty, + return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), make_active); #endif } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index ccba6deb00..65076879a2 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -239,7 +239,9 @@ class APIServer final : public Component, #ifdef USE_API_NOISE bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, - const psk_t &active_psk, bool make_active); + bool make_active); + // Load saved PSK from preferences and apply it. Returns true on success. + bool load_and_apply_noise_psk_(); #endif // USE_API_NOISE #ifdef USE_API_HOMEASSISTANT_STATES // Helper methods to reduce code duplication From a3d9854704a7c448908847f95ea5179e54094547 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 13:56:36 -1000 Subject: [PATCH 007/115] [gpio] Remove redundant last_state_ and pack GPIOBinarySensor fields (#15113) --- .../gpio/binary_sensor/gpio_binary_sensor.cpp | 26 +++++++++---------- .../gpio/binary_sensor/gpio_binary_sensor.h | 20 +++++++------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp index 38ebbc90e4..39b1a2f713 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp @@ -23,9 +23,8 @@ static const LogString *gpio_mode_to_string(bool use_interrupt) { void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) { bool new_state = arg->isr_pin_.digital_read(); - if (new_state != arg->last_state_) { + if (new_state != arg->state_) { arg->state_ = new_state; - arg->last_state_ = new_state; arg->changed_ = true; // Wake up the component from its disabled loop state if (arg->component_ != nullptr) { @@ -34,28 +33,27 @@ void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) { } } -void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component) { +void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, Component *component) { pin->setup(); this->isr_pin_ = pin->to_isr(); this->component_ = component; // Read initial state - this->last_state_ = pin->digital_read(); - this->state_ = this->last_state_; + this->state_ = pin->digital_read(); // Attach interrupt - from this point on, any changes will be caught by the interrupt - pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, type); + pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, this->interrupt_type_); } void GPIOBinarySensor::setup() { - if (this->use_interrupt_ && !this->pin_->is_internal()) { + if (this->store_.use_interrupt_ && !this->pin_->is_internal()) { ESP_LOGD(TAG, "GPIO is not internal, falling back to polling mode"); - this->use_interrupt_ = false; + this->store_.use_interrupt_ = false; } - if (this->use_interrupt_) { + if (this->store_.use_interrupt_) { auto *internal_pin = static_cast(this->pin_); - this->store_.setup(internal_pin, this->interrupt_type_, this); + this->store_.setup(internal_pin, this); this->publish_initial_state(this->store_.get_state()); } else { this->pin_->setup(); @@ -66,14 +64,14 @@ void GPIOBinarySensor::setup() { void GPIOBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this); LOG_PIN(" Pin: ", this->pin_); - ESP_LOGCONFIG(TAG, " Mode: %s", LOG_STR_ARG(gpio_mode_to_string(this->use_interrupt_))); - if (this->use_interrupt_) { - ESP_LOGCONFIG(TAG, " Interrupt Type: %s", LOG_STR_ARG(interrupt_type_to_string(this->interrupt_type_))); + ESP_LOGCONFIG(TAG, " Mode: %s", LOG_STR_ARG(gpio_mode_to_string(this->store_.use_interrupt_))); + if (this->store_.use_interrupt_) { + ESP_LOGCONFIG(TAG, " Interrupt Type: %s", LOG_STR_ARG(interrupt_type_to_string(this->store_.interrupt_type_))); } } void GPIOBinarySensor::loop() { - if (this->use_interrupt_) { + if (this->store_.use_interrupt_) { if (this->store_.is_changed()) { // Clear the flag immediately to minimize the window where we might miss changes this->store_.clear_changed(); diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h index 8b1cc29613..24efc2a0e6 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h @@ -8,10 +8,10 @@ namespace esphome { namespace gpio { -// Store class for ISR data (no vtables, ISR-safe) +// Store class for ISR data and configuration (no vtables, ISR-safe) class GPIOBinarySensorStore { public: - void setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component); + void setup(InternalGPIOPin *pin, Component *component); static void gpio_intr(GPIOBinarySensorStore *arg); @@ -32,11 +32,13 @@ class GPIOBinarySensorStore { } protected: + friend class GPIOBinarySensor; ISRInternalGPIOPin isr_pin_; - volatile bool state_{false}; - volatile bool last_state_{false}; - volatile bool changed_{false}; Component *component_{nullptr}; // Pointer to the component for enable_loop_soon_any_context() + volatile bool state_{false}; + volatile bool changed_{false}; + bool use_interrupt_{true}; + gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE}; }; class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Component { @@ -44,9 +46,9 @@ class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Compon // No destructor needed: ESPHome components are created at boot and live forever. // Interrupts are only detached on reboot when memory is cleared anyway. - void set_pin(GPIOPin *pin) { pin_ = pin; } - void set_use_interrupt(bool use_interrupt) { use_interrupt_ = use_interrupt; } - void set_interrupt_type(gpio::InterruptType type) { interrupt_type_ = type; } + void set_pin(GPIOPin *pin) { this->pin_ = pin; } + void set_use_interrupt(bool use_interrupt) { this->store_.use_interrupt_ = use_interrupt; } + void set_interrupt_type(gpio::InterruptType type) { this->store_.interrupt_type_ = type; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup pin @@ -59,8 +61,6 @@ class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Compon protected: GPIOPin *pin_; - bool use_interrupt_{true}; - gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE}; GPIOBinarySensorStore store_; }; From 8ad8f89e504bb93273ff6e703c6113458ae43536 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 13:56:53 -1000 Subject: [PATCH 008/115] [light] Reorder LightState fields to eliminate padding (#15112) --- esphome/components/light/light_state.h | 32 +++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index b8d72cc832..ab7f2e4df8 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -322,22 +322,6 @@ class LightState : public EntityBase, public Component { FixedVector effects_; /// Object used to store the persisted values of the light. ESPPreferenceObject rtc_; - /// Value for storing the index of the currently active effect. 0 if no effect is active - uint32_t active_effect_index_{}; - /// Default transition length for all transitions in ms. - uint32_t default_transition_length_{}; - /// Transition length to use for flash transitions. - uint32_t flash_transition_length_{}; - /// Gamma correction factor for the light. - float gamma_correct_{}; -#ifdef USE_LIGHT_GAMMA_LUT - const uint16_t *gamma_table_{nullptr}; -#endif // USE_LIGHT_GAMMA_LUT - - /// Whether the light value should be written in the next cycle. - bool next_write_{true}; - // for effects, true if a transformer (transition) is active. - bool is_transformer_active_ = false; /** Listeners for remote values changes. * @@ -361,6 +345,22 @@ class LightState : public EntityBase, public Component { /// Initial state of the light. optional initial_state_{}; + /// Value for storing the index of the currently active effect. 0 if no effect is active + uint32_t active_effect_index_{}; + /// Default transition length for all transitions in ms. + uint32_t default_transition_length_{}; + /// Transition length to use for flash transitions. + uint32_t flash_transition_length_{}; + /// Gamma correction factor for the light. + float gamma_correct_{}; +#ifdef USE_LIGHT_GAMMA_LUT + const uint16_t *gamma_table_{nullptr}; +#endif // USE_LIGHT_GAMMA_LUT + + /// Whether the light value should be written in the next cycle. + bool next_write_{true}; + // for effects, true if a transformer (transition) is active. + bool is_transformer_active_{false}; /// Restore mode of the light. LightRestoreMode restore_mode_; }; From 69911c3db1828e3d7e9447cf92f10d728eaaa543 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 13:58:36 -1000 Subject: [PATCH 009/115] [wifi] Reduce ESP8266 roaming scan dwell time to match ESP32 (#15127) --- .../components/wifi/wifi_component_esp8266.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index f2fabb9080..03800cc3a9 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -664,11 +664,22 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { config.show_hidden = 1; #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE; + // Use shorter dwell times for roaming scans - we only need to detect strong + // nearby APs, not do a thorough survey. This also reduces off-channel time + // which can cause Beacon Timeout disconnects on some APs. + // Roaming times match the ESP32 IDF scan defaults. + static constexpr uint32_t SCAN_PASSIVE_DEFAULT_MS = 500; + static constexpr uint32_t SCAN_PASSIVE_ROAMING_MS = 300; + static constexpr uint32_t SCAN_ACTIVE_MIN_DEFAULT_MS = 400; + static constexpr uint32_t SCAN_ACTIVE_MAX_DEFAULT_MS = 500; + static constexpr uint32_t SCAN_ACTIVE_MIN_ROAMING_MS = 100; + static constexpr uint32_t SCAN_ACTIVE_MAX_ROAMING_MS = 300; + bool roaming = this->roaming_state_ == RoamingState::SCANNING; if (passive) { - config.scan_time.passive = 500; + config.scan_time.passive = roaming ? SCAN_PASSIVE_ROAMING_MS : SCAN_PASSIVE_DEFAULT_MS; } else { - config.scan_time.active.min = 400; - config.scan_time.active.max = 500; + config.scan_time.active.min = roaming ? SCAN_ACTIVE_MIN_ROAMING_MS : SCAN_ACTIVE_MIN_DEFAULT_MS; + config.scan_time.active.max = roaming ? SCAN_ACTIVE_MAX_ROAMING_MS : SCAN_ACTIVE_MAX_DEFAULT_MS; } #endif bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback); From df4318505f79ce12c15bd848c3a89ae59aa1c9e9 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Tue, 24 Mar 2026 01:28:04 +0100 Subject: [PATCH 010/115] [substitutions] refactor substitute() as a pure function (package refactor part 3) (#15031) Co-authored-by: J. Nick Koston --- esphome/components/packages/__init__.py | 9 +- esphome/components/substitutions/__init__.py | 93 ++++++++------------ tests/unit_tests/test_substitutions.py | 46 ++++++++-- 3 files changed, 82 insertions(+), 66 deletions(-) diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 793cb946dd..f9bdb677a7 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -300,9 +300,14 @@ def do_packages_pass(config: dict, skip_update: bool = False) -> dict: context_vars = package_config.vars if CONF_PACKAGES in package_config or CONF_URL in package_config: # Remote package definition: eagerly resolve before PACKAGE_SCHEMA validation. - from esphome.components.substitutions import substitute_context_vars + from esphome.components.substitutions import ContextVars, substitute - substitute_context_vars(package_config, context_vars) + package_config = substitute( + package_config, + [], + ContextVars(context_vars), + strict_undefined=False, + ) package_config = PACKAGE_SCHEMA(package_config) if isinstance(package_config, str): return package_config # Jinja string, skip processing diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index ecee816ce9..aab1712b65 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -81,12 +81,6 @@ def _restore_data_base(value: Any, orig_value: ESPHomeDataBase) -> ESPHomeDataBa return value -def _try_substitute(value: Any, context: ContextVars) -> Any: - """Substitute variables in value, returning the result or the original if unchanged.""" - result = _substitute_item(value, [], context, strict_undefined=True) - return result if result is not None else value - - def _resolve_var(name: str, context_vars: ContextVars) -> Any: """Look up a substitution variable, falling back to the resolver callback.""" sub = context_vars.get(name, Missing) @@ -253,7 +247,7 @@ def _push_context( if value is Missing: return Missing try: - value = _try_substitute(value, resolver_context) + value = substitute(value, [], resolver_context, True) except UndefinedError as err: unresolvables[key] = (value, err) return Missing @@ -297,68 +291,51 @@ def push_context( return parent_context -def _substitute_item( +def substitute( item: Any, path: SubstitutionPath, parent_context: ContextVars, strict_undefined: bool, errors: ErrList | None = None, -) -> Any | None: - """Recursively substitute variables in a config item. +) -> Any: + """Returns a recursively substituted version of `item`.""" - Walks dicts, lists, strings, Lambdas, Extend, and Remove nodes, - replacing variable references with values from context_vars. - Mutates containers in-place; returns a replacement value for - strings/scalars, or None if the item was unchanged. - """ + if isinstance(item, ESPLiteralValue): + return item # do not substitute inside literal blocks - def _walk(item: Any, path: SubstitutionPath, parent_ctx: ContextVars) -> Any | None: - if isinstance(item, ESPLiteralValue): - return None # do not substitute inside literal blocks + # Push the current item's context onto the context stack + context_vars = push_context(item, parent_context, errors) - ctx = push_context(item, parent_ctx, errors) + result = item - if isinstance(item, list): - for idx, it in enumerate(item): - sub = _walk(it, path + [idx], ctx) - if sub is not None: - item[idx] = sub - elif isinstance(item, dict): - replace_keys: list[tuple[str, Any]] = [] - for k, v in item.items(): - if path or k != CONF_SUBSTITUTIONS: - sub = _walk(k, path + [k], ctx) - if sub is not None: - replace_keys.append((k, sub)) - sub = _walk(v, path + [k], ctx) - if sub is not None: - item[k] = sub - for old, new in replace_keys: - if str(new) == str(old): - item[new] = item[old] - else: - item[new] = merge_config(item.get(new), item.get(old)) - del item[old] - elif isinstance(item, str): - sub = _expand_substitutions(item, path, ctx, strict_undefined, errors) - if not isinstance(sub, str) or sub != item: - return sub - elif isinstance(item, (core.Lambda, Extend, Remove)) and item.value: - sub = _expand_substitutions(item.value, path, ctx, strict_undefined, errors) - if sub != item.value: - item.value = sub - return None + if isinstance(item, list): + result = [ + substitute(it, path + [i], context_vars, strict_undefined, errors) + for i, it in enumerate(item) + ] - return _walk(item, path, parent_context) + elif isinstance(item, dict): + result = OrderedDict() + for k, v in item.items(): + v = substitute(v, path + [k], context_vars, strict_undefined, errors) + k = substitute(k, path + [k], context_vars, strict_undefined, errors) + result[k] = merge_config(result.get(k), v) + elif isinstance(item, str): + result = _expand_substitutions( + item, path, context_vars, strict_undefined, errors + ) -def substitute_context_vars(node: Any, context_vars: dict[str, Any]) -> None: - """Eagerly substitute context vars into a config node in-place. + elif isinstance(item, (core.Lambda, Extend, Remove)) and item.value: + value = _expand_substitutions( + item.value, path, context_vars, strict_undefined, errors + ) + if item.value != value: + result = type(item)(value) - Undefined variables are silently ignored — this is used before - the main substitution pass when not all variables are visible yet. - """ - _substitute_item(node, [], ContextVars(context_vars), strict_undefined=False) + if isinstance(item, ESPHomeDataBase): + result = make_data_base(result, item) + return result def _warn_unresolved_variables(errors: ErrList) -> None: @@ -387,7 +364,7 @@ def do_substitution_pass( Extracts the ``substitutions:`` block, merges in any command-line overrides, resolves inter-variable dependencies, then walks the config tree replacing all ``$var`` / ``${expr}`` references. - Returns the (mutated) config dict with resolved substitutions + Returns a new config dict with resolved substitutions restored at the front. """ # Extract substitutions from config, overriding with substitutions coming from command line: @@ -415,7 +392,7 @@ def do_substitution_pass( errors: ErrList = [] # Collect undefined errors during substitution parent_context, substitutions = _push_context(substitutions, ContextVars(), errors) - _substitute_item(config, [], parent_context, False, errors) + config = substitute(config, [], parent_context, False, errors) if errors: _warn_unresolved_variables(errors) diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index db46a27dfb..30478f9521 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -550,8 +550,8 @@ def test_lambda_substitution() -> None: "lambda": lam, } ) - substitutions.do_substitution_pass(config) - assert lam.value == "return 42;" + config = substitutions.do_substitution_pass(config) + assert config["lambda"].value == "return 42;" def test_lambda_no_substitution_unchanged() -> None: @@ -564,8 +564,8 @@ def test_lambda_no_substitution_unchanged() -> None: "lambda": lam, } ) - substitutions.do_substitution_pass(config) - assert lam.value is original_value + config = substitutions.do_substitution_pass(config) + assert config["lambda"].value is original_value def test_extend_substitution() -> None: @@ -577,8 +577,42 @@ def test_extend_substitution() -> None: "sensor": ext, } ) - substitutions.do_substitution_pass(config) - assert ext.value == "my_sensor" + config = substitutions.do_substitution_pass(config) + assert config["sensor"].value == "my_sensor" + + +def test_substitute_does_not_mutate_input() -> None: + """substitute() must return a new tree without modifying the original.""" + inner_list = ["${var}", "static"] + inner_dict = OrderedDict({"key": "${var}"}) + lam = Lambda("return ${var};") + config = OrderedDict( + { + "a_list": inner_list, + "a_dict": inner_dict, + "a_lambda": lam, + "plain": "${var}", + } + ) + context = substitutions.ContextVars({"var": "replaced"}) + result = substitutions.substitute(config, [], context, strict_undefined=True) + + # Result has substitutions applied + assert result["plain"] == "replaced" + assert result["a_list"] == ["replaced", "static"] + assert result["a_dict"]["key"] == "replaced" + assert result["a_lambda"].value == "return replaced;" + + # Original input is untouched + assert config["plain"] == "${var}" + assert inner_list == ["${var}", "static"] + assert inner_dict["key"] == "${var}" + assert lam.value == "return ${var};" + + # Containers are new objects, not the originals + assert result["a_list"] is not inner_list + assert result["a_dict"] is not inner_dict + assert result["a_lambda"] is not lam def test_do_substitution_pass_substitutions_must_be_mapping_from_config() -> None: From fe2c4e47bfc3c622179b6d006703151a3ee7f39f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 14:40:02 -1000 Subject: [PATCH 011/115] [sensor] Deprecate .raw_state, guard raw_callback_ behind USE_SENSOR_FILTER (#15094) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/haier/hon_climate.cpp | 2 +- .../nextion/sensor/nextion_sensor.cpp | 4 --- esphome/components/sensor/sensor.cpp | 10 +++++-- esphome/components/sensor/sensor.h | 28 ++++++++++++++----- 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 92defe560e..1cee95bf16 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -748,7 +748,7 @@ void HonClimate::update_sub_sensor_(SubSensorType type, float value) { if (type < SubSensorType::SUB_SENSOR_TYPE_COUNT) { size_t index = (size_t) type; if ((this->sub_sensors_[index] != nullptr) && - ((!this->sub_sensors_[index]->has_state()) || (this->sub_sensors_[index]->raw_state != value))) + ((!this->sub_sensors_[index]->has_state()) || (this->sub_sensors_[index]->get_raw_state() != value))) this->sub_sensors_[index]->publish_state(value); } } diff --git a/esphome/components/nextion/sensor/nextion_sensor.cpp b/esphome/components/nextion/sensor/nextion_sensor.cpp index 03b7261239..9ea12cf808 100644 --- a/esphome/components/nextion/sensor/nextion_sensor.cpp +++ b/esphome/components/nextion/sensor/nextion_sensor.cpp @@ -85,10 +85,6 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { } this->publish_state(published_state); - } else { - this->raw_state = state; - this->state = state; - this->set_has_state(true); } } this->update_component_settings(); diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index b4e59dfeb5..aad7f86dcf 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -40,7 +40,10 @@ const LogString *state_class_to_string(StateClass state_class) { return StateClassStrings::get_log_str(static_cast(state_class), 0); } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" Sensor::Sensor() : state(NAN), raw_state(NAN) {} +#pragma GCC diagnostic pop int8_t Sensor::get_accuracy_decimals() { if (this->sensor_flags_.has_accuracy_override) @@ -63,8 +66,13 @@ StateClass Sensor::get_state_class() { } void Sensor::publish_state(float state) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" this->raw_state = state; +#pragma GCC diagnostic pop +#ifdef USE_SENSOR_FILTER this->raw_callback_.call(state); +#endif ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state); @@ -110,8 +118,6 @@ void Sensor::clear_filters() { this->filter_list_ = nullptr; } #endif // USE_SENSOR_FILTER -float Sensor::get_state() const { return this->state; } -float Sensor::get_raw_state() const { return this->raw_state; } void Sensor::internal_send_state_to_frontend(float state) { this->set_has_state(true); diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index b3bd962036..f4ea4af985 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -95,9 +95,14 @@ class Sensor : public EntityBase { #endif /// Getter-syntax for .state. - float get_state() const; + float get_state() const { return this->state; } /// Getter-syntax for .raw_state - float get_raw_state() const; + float get_raw_state() const { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + return this->raw_state; +#pragma GCC diagnostic pop + } /** Publish a new state to the front-end. * @@ -113,8 +118,14 @@ class Sensor : public EntityBase { /// Add a callback that will be called every time a filtered value arrives. template void add_on_state_callback(F &&callback) { this->callback_.add(std::forward(callback)); } /// Add a callback that will be called every time the sensor sends a raw value. + /// When USE_SENSOR_FILTER is not enabled, delegates to the regular callback + /// since raw state equals filtered state without filter support compiled in. template void add_on_raw_state_callback(F &&callback) { +#ifdef USE_SENSOR_FILTER this->raw_callback_.add(std::forward(callback)); +#else + this->callback_.add(std::forward(callback)); +#endif } /** This member variable stores the last state that has passed through all filters. @@ -126,17 +137,20 @@ class Sensor : public EntityBase { */ float state; - /** This member variable stores the current raw state of the sensor, without any filters applied. - * - * Unlike .state,this will be updated immediately when publish_state is called. - */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + /// @deprecated Use get_raw_state() instead. This member will be removed in ESPHome 2026.10.0. + ESPDEPRECATED("Use get_raw_state() instead of .raw_state. Will be removed in 2026.10.0", "2026.4.0") float raw_state; +#pragma GCC diagnostic pop void internal_send_state_to_frontend(float state); protected: +#ifdef USE_SENSOR_FILTER LazyCallbackManager raw_callback_; ///< Storage for raw state callbacks. - LazyCallbackManager callback_; ///< Storage for filtered state callbacks. +#endif + LazyCallbackManager callback_; ///< Storage for filtered state callbacks. #ifdef USE_SENSOR_FILTER Filter *filter_list_{nullptr}; ///< Store all active filters. From 793813790a36d782a726b9b9258b3403ce08c34a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 15:52:39 -1000 Subject: [PATCH 012/115] [api] Precompute tag bytes for forced varint and length-delimited fields (#15067) --- esphome/components/api/api_pb2.cpp | 10 ++-- esphome/components/api/proto.h | 11 +++++ script/api_protobuf/api_protobuf.py | 75 +++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 61b034c7ea..f77f4df545 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -2249,10 +2249,14 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, return true; } void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address, true); - buffer.encode_sint32(2, this->rssi, true); + buffer.write_raw_byte(8); + buffer.encode_varint_raw_64(this->address); + buffer.write_raw_byte(16); + buffer.encode_varint_raw(encode_zigzag32(this->rssi)); buffer.encode_uint32(3, this->address_type); - buffer.encode_bytes(4, this->data, this->data_len, true); + buffer.write_raw_byte(34); + buffer.encode_varint_raw(this->data_len); + buffer.encode_raw(this->data, this->data_len); } uint32_t BluetoothLERawAdvertisement::calculate_size() const { uint32_t size = 0; diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 95a79105a3..b629018a91 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -229,6 +229,17 @@ class ProtoWriteBuffer { * Following https://protobuf.dev/programming-guides/encoding/#structure */ void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); } + /// Write a single precomputed tag byte. Tag must be < 128. + inline void write_raw_byte(uint8_t b) ESPHOME_ALWAYS_INLINE { + this->debug_check_bounds_(1); + *this->pos_++ = b; + } + /// Write raw bytes to the buffer (no tag, no length prefix). + inline void encode_raw(const void *data, size_t len) ESPHOME_ALWAYS_INLINE { + this->debug_check_bounds_(len); + std::memcpy(this->pos_, data, len); + this->pos_ += len; + } /// Write a precomputed tag byte + 32-bit value in one operation. /// Tag must be a single-byte varint (< 128). No zero check. inline void write_tag_and_fixed32(uint8_t tag, uint32_t value) ESPHOME_ALWAYS_INLINE { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 81ea93caf4..f2a11141af 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -221,8 +221,58 @@ class TypeInfo(ABC): decode_64bit = None + # Mapping from encode_func to raw encode expression template. + # When a forced field has a single-byte tag, the code generator emits + # write_raw_byte(tag) + raw encode instead of the full encode_* method, + # eliminating the zero-check branch and encode_field_raw indirection. + # {value} is replaced with the actual field expression. + RAW_ENCODE_MAP: dict[str, str] = { + "encode_uint32": "buffer.encode_varint_raw({value});", + "encode_uint64": "buffer.encode_varint_raw_64({value});", + "encode_sint32": "buffer.encode_varint_raw(encode_zigzag32({value}));", + "encode_sint64": "buffer.encode_varint_raw_64(encode_zigzag64({value}));", + "encode_int64": "buffer.encode_varint_raw_64(static_cast({value}));", + "encode_bool": "buffer.write_raw_byte({value} ? 0x01 : 0x00);", + } + + def _encode_with_precomputed_tag(self, value_expr: str) -> str | None: + """Try to emit a precomputed-tag encode for a forced field. + + Returns the raw encode string if the tag is a single byte and the + encode_func has a known raw equivalent, or None otherwise. + """ + if not self.force: + return None + tag = self.calculate_tag() + if tag >= 128: + return None + raw_expr = self.RAW_ENCODE_MAP.get(self.encode_func) + if raw_expr is None: + return None + return f"buffer.write_raw_byte({tag});\n{raw_expr.format(value=value_expr)}" + + def _encode_bytes_with_precomputed_tag( + self, data_expr: str, len_expr: str + ) -> str | None: + """Try to emit a precomputed-tag encode for a forced bytes/string field. + + Returns the raw encode string if the tag is a single byte, or None. + """ + if not self.force: + return None + tag = self.calculate_tag() + if tag >= 128: + return None + return ( + f"buffer.write_raw_byte({tag});\n" + f"buffer.encode_varint_raw({len_expr});\n" + f"buffer.encode_raw({data_expr}, {len_expr});" + ) + @property def encode_content(self) -> str: + if result := self._encode_with_precomputed_tag(f"this->{self.field_name}"): + return result if self.force: return f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, true);" return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" @@ -635,6 +685,11 @@ class StringType(TypeInfo): @property def encode_content(self) -> str: # Use the StringRef + if result := self._encode_bytes_with_precomputed_tag( + f"this->{self.field_name}_ref_.c_str()", + f"this->{self.field_name}_ref_.size()", + ): + return result if self.force: return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_, true);" return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_);" @@ -801,6 +856,10 @@ class BytesType(TypeInfo): @property def encode_content(self) -> str: + if result := self._encode_bytes_with_precomputed_tag( + f"this->{self.field_name}_ptr_", f"this->{self.field_name}_len_" + ): + return result if self.force: return f"buffer.encode_bytes({self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_, true);" return f"buffer.encode_bytes({self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_);" @@ -908,6 +967,10 @@ class PointerToBytesBufferType(PointerToBufferTypeBase): @property def encode_content(self) -> str: + if result := self._encode_bytes_with_precomputed_tag( + f"this->{self.field_name}", f"this->{self.field_name}_len" + ): + return result if self.force: return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);" @@ -957,6 +1020,10 @@ class PointerToStringBufferType(PointerToBufferTypeBase): @property def encode_content(self) -> str: + if result := self._encode_bytes_with_precomputed_tag( + f"this->{self.field_name}.c_str()", f"this->{self.field_name}.size()" + ): + return result if self.force: return ( f"buffer.encode_string({self.number}, this->{self.field_name}, true);" @@ -1124,6 +1191,10 @@ class FixedArrayBytesType(TypeInfo): @property def encode_content(self) -> str: + if result := self._encode_bytes_with_precomputed_tag( + f"this->{self.field_name}", f"this->{self.field_name}_len" + ): + return result if self.force: return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);" @@ -1199,6 +1270,10 @@ class EnumType(TypeInfo): @property def encode_content(self) -> str: + if result := self._encode_with_precomputed_tag( + f"static_cast(this->{self.field_name})" + ): + return result if self.force: return f"buffer.{self.encode_func}({self.number}, static_cast(this->{self.field_name}), true);" return f"buffer.{self.encode_func}({self.number}, static_cast(this->{self.field_name}));" From 7eddf429ea3810f4e1b019b13c20f768de936411 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Tue, 24 Mar 2026 10:57:22 +0100 Subject: [PATCH 013/115] [substitutions] speed up config loading: substitutions pass and `!include` redesign (package refactor part 4) (#12126) Co-authored-by: J. Nick Koston --- esphome/components/packages/__init__.py | 406 ++++++++++++------ esphome/config.py | 6 +- tests/component_tests/packages/test_init.py | 4 +- .../component_tests/packages/test_packages.py | 178 +++++++- .../06-remote_packages.approved.yaml | 21 +- .../06-remote_packages.input.yaml | 22 +- .../07-package_merging.approved.yaml | 2 - .../08-include_hierarchy.approved.yaml | 49 +++ .../08-include_hierarchy.input.yaml | 16 + .../10-dynamic_packages.approved.yaml | 69 +++ .../10-dynamic_packages.input.yaml | 62 +++ .../substitutions/level1_package.yaml | 21 + .../substitutions/level2_package.yaml | 21 + .../substitutions/level3_package.yaml | 16 + .../fixtures/substitutions/package2.yaml | 10 + tests/unit_tests/test_substitutions.py | 4 +- tests/unit_tests/test_yaml_util.py | 42 +- 17 files changed, 781 insertions(+), 168 deletions(-) create mode 100644 tests/unit_tests/fixtures/substitutions/08-include_hierarchy.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/08-include_hierarchy.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/10-dynamic_packages.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/10-dynamic_packages.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/level1_package.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/level2_package.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/level3_package.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/package2.yaml diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index f9bdb677a7..1a6df84fe0 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import Any from esphome import git, yaml_util +from esphome.components.substitutions import ContextVars, push_context, substitute from esphome.components.substitutions.jinja import has_jinja from esphome.config_helpers import Remove, merge_config import esphome.config_validation as cv @@ -32,43 +33,44 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = CONF_PACKAGES -def validate_has_jinja(value: Any): - if not isinstance(value, str) or not has_jinja(value): - raise cv.Invalid("string does not contain Jinja syntax") - return value +def is_remote_package(package_config: dict) -> bool: + """Returns True if the package_config is a remote package definition.""" + return CONF_URL in package_config -def valid_package_contents(allow_jinja: bool = True) -> Callable[[Any], dict]: - """Returns a validator that checks if a package_config that will be merged looks as - much as possible to a valid config to fail early on obvious mistakes.""" +def valid_package_contents(package_config: dict) -> dict: + """Validate that a package looks like a plausible ESPHome config fragment. - def validator(package_config: dict) -> dict: - if isinstance(package_config, dict): - if CONF_URL in package_config: - # If a URL key is found, then make sure the config conforms to a remote package schema: - return REMOTE_PACKAGE_SCHEMA(package_config) - - # Validate manually since Voluptuous would regenerate dicts and lose metadata - # such as ESPHomeDataBase - for k, v in package_config.items(): - if not isinstance(k, str): - raise cv.Invalid("Package content keys must be strings") - if isinstance(v, (dict, list, Remove)): - continue # e.g. script: [], psram: !remove, logger: {level: debug} - if v is None: - continue # e.g. web_server: - if allow_jinja and isinstance(v, str) and has_jinja(v): - # e.g: remote package shorthand: - # package_name: github://esphome/repo/file.yaml@${ branch }, or: - # switch: ${ expression that evals to a switch } - continue - - raise cv.Invalid("Invalid component content in package definition") - return package_config + Rejects non-dict values, remote package schemas (which should have been + handled earlier), non-string keys, and scalar values that aren't Jinja + expressions. This is a lightweight check to catch obvious mistakes before + full component validation runs later. + """ + if not isinstance(package_config, dict): raise cv.Invalid("Package contents must be a dict") - return validator + if is_remote_package(package_config): + # Package contents must not contain a root `url:` key + raise cv.Invalid("Remote package schema not expected here") + + # Validate manually since Voluptuous would regenerate dicts and lose metadata + # such as ESPHomeDataBase + for k, v in package_config.items(): + if not isinstance(k, str): + raise cv.Invalid("Package content keys must be strings") + if isinstance(v, (dict, list, Remove)): + continue # e.g. script: [], psram: !remove, logger: {level: debug} + if v is None: + continue # e.g. web_server: + if isinstance(v, str) and has_jinja(v): + # e.g: remote package shorthand: + # package_name: github://esphome/repo/file.yaml@${ branch }, or: + # switch: ${ expression that evals to a switch } + continue + + raise cv.Invalid("Invalid component content in package definition") + return package_config def expand_file_to_files(config: dict): @@ -105,7 +107,7 @@ def validate_source_shorthand(value): return REMOTE_PACKAGE_SCHEMA(conf) -def deprecate_single_package(config): +def deprecate_single_package(config: dict) -> dict: _LOGGER.warning( """ Including a single package under `packages:`, i.e., `packages: !include mypackage.yaml` is deprecated. @@ -158,10 +160,7 @@ REMOTE_PACKAGE_SCHEMA = cv.All( PACKAGE_SCHEMA = cv.Any( # A package definition is either: validate_source_shorthand, # A git URL shorthand string that expands to a remote package schema, or REMOTE_PACKAGE_SCHEMA, # a valid remote package schema, or - validate_has_jinja, # a Jinja string that may resolve to a package, or - valid_package_contents( - allow_jinja=True - ), # Something that at least looks like an actual package, e.g. {wifi:{ssid: xxx}} + valid_package_contents, # Something that at least looks like an actual package, e.g. {wifi:{ssid: xxx}} # which will have to be fully validated later as per each component's schema. ) @@ -179,7 +178,15 @@ CONFIG_SCHEMA = cv.Any( # under `packages:` we can have either: def _process_remote_package(config: dict, skip_update: bool = False) -> dict: - # When skip_update is True, use NEVER_REFRESH to prevent updates + """Clone/update a git repo and load the YAML files listed in the package definition. + + Returns ``{"packages": {: , ...}}`` so the caller + can recurse into the loaded packages. Each loaded YAML node is tagged + with any ``vars:`` from the file entry via :func:`yaml_util.add_context`. + + If loading fails after cloning, attempts a revert and retry in case + a prior cached checkout is stale. + """ actual_refresh = git.NEVER_REFRESH if skip_update else config[CONF_REFRESH] repo_dir, revert = git.clone_or_update( url=config[CONF_URL], @@ -189,7 +196,7 @@ def _process_remote_package(config: dict, skip_update: bool = False) -> dict: username=config.get(CONF_USERNAME), password=config.get(CONF_PASSWORD), ) - files = [] + files: list[dict[str, Any]] = [] if base_path := config.get(CONF_PATH): repo_dir = repo_dir / base_path @@ -200,126 +207,255 @@ def _process_remote_package(config: dict, skip_update: bool = False) -> dict: else: files.append(file) - def get_packages(files) -> dict: - packages = {} + def _load_package_yaml(yaml_file: Path, filename: str) -> dict: + """Load a YAML file from a remote package, validating min_version.""" + try: + new_yaml = yaml_util.load_yaml(yaml_file) + except EsphomeError as e: + raise cv.Invalid( + f"{filename} is not a valid YAML file." + f" Please check the file contents.\n{e}" + ) from e + esphome_config = new_yaml.get(CONF_ESPHOME) or {} + min_version = esphome_config.get(CONF_MIN_VERSION) + if min_version is not None and cv.Version.parse(min_version) > cv.Version.parse( + ESPHOME_VERSION + ): + raise cv.Invalid( + f"Current ESPHome Version is too old to use" + f" this package: {ESPHOME_VERSION} < {min_version}" + ) + return new_yaml + + def get_packages(files: list[dict[str, Any]]) -> dict: + packages: dict[str, Any] = {} for idx, file in enumerate(files): filename = file[CONF_PATH] yaml_file: Path = repo_dir / filename - vars = file.get(CONF_VARS, {}) - if not yaml_file.is_file(): raise cv.Invalid( f"{filename} does not exist in repository", path=[CONF_FILES, idx, CONF_PATH], ) - - try: - new_yaml = yaml_util.load_yaml(yaml_file) - if ( - CONF_ESPHOME in new_yaml - and CONF_MIN_VERSION in new_yaml[CONF_ESPHOME] - ): - min_version = new_yaml[CONF_ESPHOME][CONF_MIN_VERSION] - if cv.Version.parse(min_version) > cv.Version.parse( - ESPHOME_VERSION - ): - raise cv.Invalid( - f"Current ESPHome Version is too old to use this package: {ESPHOME_VERSION} < {min_version}" - ) - new_yaml = yaml_util.add_context(new_yaml, vars or None) - packages[f"{filename}{idx}"] = new_yaml - except EsphomeError as e: - raise cv.Invalid( - f"{filename} is not a valid YAML file. Please check the file contents.\n{e}" - ) from e + new_yaml = _load_package_yaml(yaml_file, filename) + new_yaml = yaml_util.add_context(new_yaml, file.get(CONF_VARS)) + packages[f"{filename}{idx}"] = new_yaml return packages - packages = None - error = "" - - try: - packages = get_packages(files) - except cv.Invalid as e: - error = e + if revert is not None: + # If loading fails, the cached checkout may be stale — revert and retry once. try: - if revert is not None: - revert() - packages = get_packages(files) - except cv.Invalid as er: - error = er + return {CONF_PACKAGES: get_packages(files)} + except cv.Invalid: + revert() + try: + return {CONF_PACKAGES: get_packages(files)} + except cv.Invalid as err: + raise cv.Invalid(f"Failed to load packages. {err}", path=err.path) from err - if packages is None: - raise cv.Invalid(f"Failed to load packages. {error}", path=error.path) + return {CONF_PACKAGES: get_packages(files)} - return {"packages": packages} + +def _walk_package_dict( + packages: dict, + callback: Callable[[dict, ContextVars | None], dict], + context: ContextVars | None, +) -> cv.Invalid | None: + """Iterate a packages dict in reverse priority order, invoking callback on each entry. + + Returns ``None`` on success, or the first :class:`cv.Invalid` error if a callback fails. + """ + for package_name, package_config in reversed(packages.items()): + with cv.prepend_path(package_name): + try: + packages[package_name] = callback(package_config, context) + except cv.Invalid as err: + return err + return None + + +def _walk_package_list( + packages: list, + callback: Callable[[dict, ContextVars | None], dict], + context: ContextVars | None, +) -> None: + """Iterate a packages list in reverse priority order, invoking callback on each entry.""" + for idx in reversed(range(len(packages))): + with cv.prepend_path(idx): + packages[idx] = callback(packages[idx], context) def _walk_packages( - config: dict, callback: Callable[[dict], dict], validate_deprecated: bool = True + config: dict, + callback: Callable[[dict, ContextVars | None], dict], + context: ContextVars | None = None, + validate_deprecated: bool = True, ) -> dict: + """Walks the packages structure in priority order, invoking ``callback`` on each package definition found. + + This function only iterates over the immediate ``packages:`` entries in *config*. + If packages may contain nested ``packages:`` keys, the *callback* is responsible + for recursing by calling ``_walk_packages`` on the returned package config. + """ if CONF_PACKAGES not in config: return config packages = config[CONF_PACKAGES] - # The following block and `validate_deprecated` parameter can be safely removed - # once single-package deprecation is effective - if validate_deprecated: - packages = CONFIG_SCHEMA(packages) + if not isinstance(packages, (dict, list)): + raise cv.Invalid( + f"Packages must be a key to value mapping or list, got {type(packages)} instead" + ) with cv.prepend_path(CONF_PACKAGES): - if isinstance(packages, dict): - for package_name, package_config in reversed(packages.items()): - with cv.prepend_path(package_name): - package_config = callback(package_config) - packages[package_name] = _walk_packages(package_config, callback) - elif isinstance(packages, list): - for idx in reversed(range(len(packages))): - with cv.prepend_path(idx): - package_config = callback(packages[idx]) - packages[idx] = _walk_packages(package_config, callback) - else: - raise cv.Invalid( - f"Packages must be a key to value mapping or list, got {type(packages)} instead" - ) + if not isinstance(packages, dict): + _walk_package_list(packages, callback, context) + elif (result := _walk_package_dict(packages, callback, context)) is not None: + if not validate_deprecated: + raise result + # Fallback: treat the dict as a single deprecated package. + # Note: this catches *any* cv.Invalid from the callback, which may + # mask real validation errors in named package dicts. + # This block can be removed once the single-package + # deprecation period (2026.7.0) is over. + config[CONF_PACKAGES] = [packages] + return _walk_packages(deprecate_single_package(config), callback, context) + config[CONF_PACKAGES] = packages return config -def do_packages_pass(config: dict, skip_update: bool = False) -> dict: - """Processes, downloads and validates all packages in the config. - Also extracts and merges all substitutions found in packages into the main config substitutions. +def _substitute_package_definition( + package_config: dict | str, context_vars: ContextVars | None +) -> dict | str: + """Substitute variables in a package definition string or remote package dict. + + Only substitutes strings and remote package dicts (URLs, refs, paths). + Local package contents are left untouched — they will be substituted + later during the main substitution pass. + """ + if isinstance(package_config, str) or ( + isinstance(package_config, dict) and is_remote_package(package_config) + ): + package_config = substitute( + item=package_config, + path=[], + parent_context=context_vars or ContextVars(), + strict_undefined=False, + ) + return package_config + + +def _update_substitutions_context( + parent_context: UserDict, + package_substitutions: dict[str, Any], +) -> None: + """Resolve and add new substitutions to the parent context. + + Skips keys already present (higher-priority sources win). + String values are substituted against the current context so that + cross-references between substitutions are expanded when possible. + """ + for key, value in package_substitutions.items(): + if key in parent_context: + continue + if not isinstance(value, str): + parent_context[key] = value + continue + parent_context[key] = substitute( + item=value, + path=[CONF_SUBSTITUTIONS, key], + parent_context=ContextVars(parent_context), + strict_undefined=False, + ) + + +class _PackageProcessor: + """Stateful processor that resolves packages and collects substitutions. + + Packages are processed highest-priority first (later-declared before + earlier-declared) so that their substitutions are available when + resolving lower-priority package definitions. For each entry: + + 1. Substitute variables in remote package definitions (URLs, refs, paths). + 2. Validate against ``PACKAGE_SCHEMA`` and download remote packages. + 3. Extract ``substitutions:`` and merge into the shared context + (higher-priority packages win on conflicts). + 4. Recurse into any nested ``packages:`` keys. + + Command-line substitutions take the highest priority and are never overridden. + """ + + def __init__( + self, + substitutions: UserDict, + command_line_substitutions: dict[str, Any] | None, + skip_update: bool, + ) -> None: + self.substitutions = substitutions + self.parent_context = UserDict(command_line_substitutions or {}) + self.skip_update = skip_update + + def resolve_package( + self, package_config: dict | str, context_vars: ContextVars | None + ) -> dict: + """Substitute variables in the definition and fetch remote packages. + + The input may be a ``str`` (git shorthand or Jinja expression) or a + ``dict`` (remote or local package). After ``PACKAGE_SCHEMA`` validation + the result is always a ``dict``. + """ + package_config = _substitute_package_definition(package_config, context_vars) + package_config = PACKAGE_SCHEMA(package_config) + if is_remote_package(package_config): + package_config = _process_remote_package(package_config, self.skip_update) + return package_config + + def collect_substitutions(self, package_config: dict) -> None: + """Extract substitutions from a package and merge into the shared context.""" + if subs := package_config.pop(CONF_SUBSTITUTIONS, {}): + self.substitutions.data = merge_config(subs, self.substitutions.data) + _update_substitutions_context(self.parent_context, subs) + + def process_package( + self, package_config: dict | str, context_vars: ContextVars | None + ) -> dict: + """Resolve a single package and recurse into any nested packages.""" + package_config = self.resolve_package(package_config, context_vars) + self.collect_substitutions(package_config) + + if CONF_PACKAGES not in package_config: + return package_config + + # Push context from !include vars on the package root and on the packages key + context_vars = push_context(package_config, context_vars) + context_vars = push_context(package_config[CONF_PACKAGES], context_vars) + return _walk_packages(package_config, self.process_package, context_vars) + + +def do_packages_pass( + config: dict, + *, + command_line_substitutions: dict[str, Any] | None = None, + skip_update: bool = False, +) -> dict: + """Load, validate, and flatten all packages in the config. + + Returns the config with all packages loaded in-place (but not yet merged) + and a consolidated ``substitutions:`` block restored at the front. """ if CONF_PACKAGES not in config: return config substitutions = UserDict(config.pop(CONF_SUBSTITUTIONS, {})) + processor = _PackageProcessor( + substitutions, command_line_substitutions, skip_update + ) + _update_substitutions_context(processor.parent_context, substitutions) - def process_package_callback(package_config: dict) -> dict: - """This will be called for each package found in the config.""" - if isinstance(package_config, yaml_util.ConfigContext): - context_vars = package_config.vars - if CONF_PACKAGES in package_config or CONF_URL in package_config: - # Remote package definition: eagerly resolve before PACKAGE_SCHEMA validation. - from esphome.components.substitutions import ContextVars, substitute - - package_config = substitute( - package_config, - [], - ContextVars(context_vars), - strict_undefined=False, - ) - package_config = PACKAGE_SCHEMA(package_config) - if isinstance(package_config, str): - return package_config # Jinja string, skip processing - if CONF_URL in package_config: - package_config = _process_remote_package(package_config, skip_update) - # Extract substitutions from the package and merge them into the main substitutions: - substitutions.data = merge_config( - package_config.pop(CONF_SUBSTITUTIONS, {}), substitutions.data - ) - return package_config - - _walk_packages(config, process_package_callback) + context_vars = push_context( + config[CONF_PACKAGES], ContextVars(processor.parent_context) + ) + _walk_packages(config, processor.process_package, context_vars) if substitutions: config[CONF_SUBSTITUTIONS] = substitutions.data @@ -328,19 +464,27 @@ def do_packages_pass(config: dict, skip_update: bool = False) -> dict: def merge_packages(config: dict) -> dict: - """Merges all packages into the main config and removes the `packages:` key.""" + """Flatten the ``packages:`` tree into the main config. + + Collects every package (including nested ones) into a flat list in + priority order, then merges them into *config* using :func:`merge_config`. + Higher-priority packages (declared later) override lower-priority ones. + + The ``packages:`` key is removed from the returned config. + Must be called after :func:`do_packages_pass` has resolved all packages. + """ if CONF_PACKAGES not in config: return config # Build flat list of all package configs to merge in priority order: merge_list: list[dict] = [] - validate_package = valid_package_contents(allow_jinja=False) - - def process_package_callback(package_config: dict) -> dict: + def process_package_callback( + package_config: dict, context: ContextVars | None + ) -> dict: """This will be called for each package found in the config.""" - merge_list.append(validate_package(package_config)) - return package_config + merge_list.append(package_config) + return _walk_packages(package_config, process_package_callback) _walk_packages(config, process_package_callback, validate_deprecated=False) # Merge all packages into the main config: diff --git a/esphome/config.py b/esphome/config.py index b80aaf3700..7a6feea3d3 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -989,7 +989,11 @@ def validate_config( result.add_output_path([CONF_PACKAGES], CONF_PACKAGES) try: - config = do_packages_pass(config, skip_update=skip_external_update) + config = do_packages_pass( + config, + command_line_substitutions=command_line_substitutions, + skip_update=skip_external_update, + ) except vol.Invalid as err: result.update(config) result.add_error(err) diff --git a/tests/component_tests/packages/test_init.py b/tests/component_tests/packages/test_init.py index 779244e2ed..fd30c2433f 100644 --- a/tests/component_tests/packages/test_init.py +++ b/tests/component_tests/packages/test_init.py @@ -69,7 +69,7 @@ def test_packages_skip_update_false( } # Call with skip_update=False (default) - do_packages_pass(config, skip_update=False) + do_packages_pass(config, command_line_substitutions={}, skip_update=False) # Verify clone_or_update was called with actual refresh value mock_clone_or_update.assert_called_once() @@ -104,7 +104,7 @@ def test_packages_default_no_skip( } # Call without skip_update parameter - do_packages_pass(config) + do_packages_pass(config, command_line_substitutions={}) # Verify clone_or_update was called with actual refresh value mock_clone_or_update.assert_called_once() diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index 60dc0dccda..0893c7dcbb 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -37,6 +37,7 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.util import OrderedDict +from esphome.yaml_util import add_context # Test strings TEST_DEVICE_NAME = "test_device_name" @@ -70,7 +71,7 @@ def fixture_basic_esphome(): def packages_pass(config): - """Wrapper around packages_pass that also resolves Extend and Remove.""" + """Passes the config through the packages processing steps.""" config = do_packages_pass(config) config = do_substitution_pass(config) config = merge_packages(config) @@ -705,6 +706,85 @@ def test_remote_packages_with_files_list( assert actual == expected +@patch("esphome.yaml_util.load_yaml") +@patch("pathlib.Path.is_file") +@patch("esphome.git.clone_or_update") +def test_remote_packages_with_files_list_and_substitutions( + mock_clone_or_update, mock_is_file, mock_load_yaml +) -> None: + """ + Ensures that packages are loaded as mixed list of dictionary and strings + """ + # Mock the response from git.clone_or_update + mock_revert = MagicMock() + mock_clone_or_update.return_value = (Path("/tmp/noexists"), mock_revert) + + # Mock the response from pathlib.Path.is_file + mock_is_file.return_value = True + + # Mock the response from esphome.yaml_util.load_yaml + mock_load_yaml.side_effect = [ + OrderedDict( + { + CONF_SENSOR: [ + { + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + } + ] + } + ), + OrderedDict( + { + CONF_SENSOR: [ + { + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + } + ] + } + ), + ] + + # Define the input config + config = { + CONF_PACKAGES: { + "package1": add_context( + { + CONF_URL: r"${url}", + CONF_REF: r"${branch}", + CONF_FILES: [ + {CONF_PATH: r"$file"}, + "sensor2.yaml", + ], + CONF_REFRESH: "1d", + }, + { + "branch": "main", + "file": TEST_YAML_FILENAME, + "url": "https://github.com/esphome/non-existant-repo", + }, + ) + } + } + + expected = { + CONF_SENSOR: [ + { + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + }, + { + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + }, + ] + } + + actual = packages_pass(config) + assert actual == expected + + @patch("esphome.yaml_util.load_yaml") @patch("pathlib.Path.is_file") @patch("esphome.git.clone_or_update") @@ -906,7 +986,7 @@ def test_packages_merge_substitutions() -> None: }, } - actual = do_packages_pass(config) + actual = do_packages_pass(config, command_line_substitutions={}) assert actual == expected @@ -970,33 +1050,107 @@ def test_package_merge() -> None: assert actual == expected +def test_packages_invalid_type_raises() -> None: + """Packages that are not a dict or list raise cv.Invalid.""" + config = { + CONF_PACKAGES: "not_a_dict_or_list", + } + with pytest.raises( + cv.Invalid, match="Packages must be a key to value mapping or list" + ): + do_packages_pass(config) + + @pytest.mark.parametrize( "invalid_package", [ 6, "some string", - ["some string"], - None, True, - {"some_component": 8}, - {3: 2}, - {"some_component": r"${unevaluated expression}"}, ], ) -def test_package_merge_invalid(invalid_package) -> None: - """ - Tests that trying to merge an invalid package raises an error. - """ +def test_invalid_package_contents_rejected(invalid_package: object) -> None: + """Invalid package contents are rejected by PACKAGE_SCHEMA during do_packages_pass.""" config = { CONF_PACKAGES: { "some_package": invalid_package, }, } - with pytest.raises(cv.Invalid): + do_packages_pass(config) + + +@pytest.mark.xfail( + reason="Deprecated single-package fallback swallows these errors. " + "Remove xfail when single-package deprecation is removed (2026.7.0).", + strict=True, +) +@pytest.mark.parametrize( + "invalid_package", + [ + None, + ["some string"], + {"some_component": 8}, + {3: 2}, + ], +) +def test_invalid_package_contents_masked_by_deprecation( + invalid_package: object, +) -> None: + """These invalid packages are swallowed by the deprecated single-package fallback.""" + config = { + CONF_PACKAGES: { + "some_package": invalid_package, + }, + } + with pytest.raises(cv.Invalid): + do_packages_pass(config) + + +def test_merge_packages_invalid_nested_type_raises() -> None: + """Invalid nested packages type during merge raises cv.Invalid.""" + config = { + CONF_PACKAGES: { + "pkg": { + CONF_PACKAGES: "invalid", + }, + }, + } + with pytest.raises( + cv.Invalid, match="Packages must be a key to value mapping or list" + ): merge_packages(config) +@patch("esphome.yaml_util.load_yaml") +@patch("pathlib.Path.is_file") +@patch("esphome.git.clone_or_update") +def test_remote_packages_no_revert( + mock_clone_or_update, mock_is_file, mock_load_yaml +) -> None: + """Remote packages with revert=None load without retry logic.""" + mock_clone_or_update.return_value = (Path("/tmp/noexists"), None) + mock_is_file.return_value = True + mock_load_yaml.return_value = OrderedDict( + {CONF_SENSOR: [{CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, CONF_NAME: "test"}]} + ) + + config = { + CONF_PACKAGES: { + "pkg": { + CONF_URL: "https://github.com/esphome/repo", + CONF_REF: "main", + CONF_FILES: [{CONF_PATH: "file.yaml"}], + CONF_REFRESH: "1d", + } + } + } + actual = packages_pass(config) + assert actual[CONF_SENSOR] == [ + {CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, CONF_NAME: "test"} + ] + + def test_raw_config_contains_merged_esphome_from_package(tmp_path) -> None: """Test that CORE.raw_config contains esphome section from merged package. diff --git a/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml b/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml index 0fffbfb7cb..300cd85950 100644 --- a/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml @@ -1,7 +1,3 @@ -substitutions: - x: 10 - y: 20 - z: 30 values_from_repo1_main: - package_name: package1 x: 3 @@ -28,3 +24,20 @@ values_from_repo1_main: y: 20 z: 5 volume: 1000 + - package_name: package6 + x: 12 + y: 13 + z: 5 + volume: 780 + - package_name: default + x: 10 + y: 20 + z: 5 + volume: 1000 +substitutions: + x: 10 + y: 20 + z: 30 + my_repo: repo1 + my_file: file1 + my_ref: main diff --git a/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml b/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml index 772860bf19..c61eeab28d 100644 --- a/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml @@ -2,16 +2,26 @@ substitutions: x: 10 y: 20 z: 30 + my_repo: default_repo + my_file: default_file + my_ref: main + +# The following key is only used by the test framework +# to simulate command line substitutions +command_line_substitutions: + my_repo: repo1 + my_file: file1 + packages: package1: url: https://github.com/esphome/repo1 + ref: main files: - path: file1.yaml vars: package_name: package1 x: 3 y: 4 - ref: main package2: !include # a package that just includes the given remote package file: remote_package_proxy.yaml vars: @@ -41,3 +51,13 @@ packages: repo: repo1 file: file1.yaml ref: main + package6: + url: https://github.com/esphome/${my_repo} + ref: ${my_ref} + files: + - path: ${my_file + ".yaml"} + vars: + package_name: package6 + x: 12 + y: 13 + package7: github://esphome/${my_repo}/${my_file + ".yaml"}@${my_ref} diff --git a/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml b/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml index 867889b7bc..9e62fcae86 100644 --- a/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml @@ -37,8 +37,6 @@ substitutions: - id: component8 value: 8 fancy_package: - substitutions: - fancy_subst: 42 fancy_component: *id001 pin: 12 some_switches: *id002 diff --git a/tests/unit_tests/fixtures/substitutions/08-include_hierarchy.approved.yaml b/tests/unit_tests/fixtures/substitutions/08-include_hierarchy.approved.yaml new file mode 100644 index 0000000000..fce47b01bf --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/08-include_hierarchy.approved.yaml @@ -0,0 +1,49 @@ +substitutions: + a: 10 + b: 20 + x: 79 +test_list: + - level1: + a: 10 + b: 20 + c: 10 + d: 20 + e: ${e} + f: ${f} + g: ${g} + h: ${h} + i: ${i} + j: ${j} + x: 80 + y: 40 + level2: + - level2: + a: 10 + b: 20 + c: 10 + d: 20 + e: 20 + f: 40 + g: ${g} + h: ${h} + i: ${i} + j: ${j} + x: 81 + y: 40 + level3: + - level3: + a: 10 + b: 20 + c: 10 + d: 20 + e: 20 + f: 40 + g: 100 + h: 200 + i: 30 + j: ${undefined_variable} + x: 82 + y: 40 + - a: 10 + b: 20 + x: 79 diff --git a/tests/unit_tests/fixtures/substitutions/08-include_hierarchy.input.yaml b/tests/unit_tests/fixtures/substitutions/08-include_hierarchy.input.yaml new file mode 100644 index 0000000000..6997ef56c1 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/08-include_hierarchy.input.yaml @@ -0,0 +1,16 @@ +substitutions: + a: 10 + b: 20 + x: 79 + +test_list: + - !include + file: level1_package.yaml + vars: + x: ${x+1} + y: ${d*2} + c: ${a} + d: ${b} + - a: ${a} + b: ${b} + x: ${x} diff --git a/tests/unit_tests/fixtures/substitutions/10-dynamic_packages.approved.yaml b/tests/unit_tests/fixtures/substitutions/10-dynamic_packages.approved.yaml new file mode 100644 index 0000000000..ec2ae711bb --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/10-dynamic_packages.approved.yaml @@ -0,0 +1,69 @@ +substitutions: + a: from base config + b: from package3 + c: from nested package4 + nested_package: + nested_package_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 + package1: + package1_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 + package2: + package2_test_list: + - a: from package2 vars + - b: from package3 + - c: from nested package4 + package3: + package3_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 + package4: + packages: + - nested_package_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 + package_map: + package1: + package1_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 + package2: + package2_test_list: + - a: from package2 vars + - b: from package3 + - c: from nested package4 + package3: &id001 + package3_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 + selected_package_number: 3 + selected_package_name: package3 + selected_package: *id001 +base_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 +package1_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 +package2_test_list: + - a: from package2 vars + - b: from package3 + - c: from nested package4 +package3_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 +nested_package_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 diff --git a/tests/unit_tests/fixtures/substitutions/10-dynamic_packages.input.yaml b/tests/unit_tests/fixtures/substitutions/10-dynamic_packages.input.yaml new file mode 100644 index 0000000000..800e3cc7a8 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/10-dynamic_packages.input.yaml @@ -0,0 +1,62 @@ +command_line_substitutions: + selected_package_number: 3 + +substitutions: + a: from base config + + package1: &p1 + substitutions: + a: from package1 + b: from package1 + c: from package1 + package1_test_list: + - a: ${ a } + - b: ${ b } + - c: ${ c } + + package2: &p2 !include + file: package2.yaml + vars: + a: from package2 vars + + package3: &p3 + substitutions: + a: from package3 + b: from package3 + c: from package3 + package3_test_list: + - a: ${ a } + - b: ${ b } + - c: ${ c } + + package4: + substitutions: + nested_package: + substitutions: + c: from nested package4 + nested_package_test_list: + - a: ${ a } + - b: ${ b } + - c: ${ c } + packages: + - ${ nested_package } + + package_map: + package1: *p1 + package2: *p2 + package3: *p3 + + selected_package_number: 2 # will be overridden by command line substitutions + selected_package_name: package${ selected_package_number } + selected_package: ${ package_map[selected_package_name] } + +packages: + - ${ package1 } + - ${ package2 } + - ${ selected_package } + - ${ package4 } + +base_test_list: + - a: ${ a } + - b: ${ b } + - c: ${ c } diff --git a/tests/unit_tests/fixtures/substitutions/level1_package.yaml b/tests/unit_tests/fixtures/substitutions/level1_package.yaml new file mode 100644 index 0000000000..d8a994b7b4 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/level1_package.yaml @@ -0,0 +1,21 @@ +# this file is included by 07-include_hierarchy.input.yaml +level1: + a: ${a} # top-level substitution + b: ${b} # top-level substitution + c: ${c} # from vars when including + d: ${d} # from vars when including + e: ${e} # undefined at this level + f: ${f} # undefined at this level + g: ${g} # undefined at this level + h: ${h} # undefined at this level + i: ${i} # undefined at this level + j: ${j} # undefined at this level + x: ${x} # from vars when including, calculated + y: ${y} # from vars when including, calculated + level2: + - !include + file: level2_package.yaml + vars: + e: ${c*2} + f: ${d*2} + x: ${x+1} diff --git a/tests/unit_tests/fixtures/substitutions/level2_package.yaml b/tests/unit_tests/fixtures/substitutions/level2_package.yaml new file mode 100644 index 0000000000..460135553a --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/level2_package.yaml @@ -0,0 +1,21 @@ +# this file is included by level1_package.yaml +level2: + a: ${a} # top-level substitution + b: ${b} # top-level substitution + c: ${c} # visible from level1 vars + d: ${d} # visible from level1 vars + e: ${e} # from vars when including + f: ${f} # from vars when including + g: ${g} # undefined at this level + h: ${h} # undefined at this level + i: ${i} # undefined at this level + j: ${j} # undefined at this level + x: ${x} # from vars when including, calculated + y: ${y} # from vars when including, calculated + level3: + - !include + file: level3_package.yaml + vars: + g: ${e*5} + h: ${f*5} + x: ${x+1} diff --git a/tests/unit_tests/fixtures/substitutions/level3_package.yaml b/tests/unit_tests/fixtures/substitutions/level3_package.yaml new file mode 100644 index 0000000000..b16ed5fcf6 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/level3_package.yaml @@ -0,0 +1,16 @@ +# this file is included by level2_package.yaml +defaults: + i: 30 +level3: + a: ${a} # top-level substitution + b: ${b} # top-level substitution + c: ${c} # visible from level1 vars + d: ${d} # visible from level1 vars + e: ${e} # visible from level2 vars + f: ${f} # visible from level2 vars + g: ${g} # from vars when including + h: ${h} # from vars when including + i: ${i} # Should take the default value of 30 + j: ${undefined_variable} # Does not exist, should be output as-is + x: ${x} # from vars when including, calculated + y: ${y} # from vars when including, calculated diff --git a/tests/unit_tests/fixtures/substitutions/package2.yaml b/tests/unit_tests/fixtures/substitutions/package2.yaml new file mode 100644 index 0000000000..998cd74c52 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/package2.yaml @@ -0,0 +1,10 @@ +# included from 10-dynamic_packages.input.yaml +substitutions: + a: from package2 # must not override base config's a + # b not defined here, won't override package1's b + c: from package2 # will override package1's c + +package2_test_list: + - a: ${ a } + - b: ${ b } + - c: ${ c } diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index 30478f9521..c7b0bbcf7c 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -143,7 +143,9 @@ def test_substitutions_fixtures( command_line_substitutions = config.pop("command_line_substitutions", None) - config = do_packages_pass(config) + config = do_packages_pass( + config, command_line_substitutions=command_line_substitutions + ) config = substitutions.do_substitution_pass(config, command_line_substitutions) diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index 35a4bc3707..667b593819 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -98,13 +98,15 @@ def test_construct_secret_missing(fixture_path: Path, tmp_path: Path) -> None: """Test that missing secrets raise proper errors.""" # Create a YAML file with a secret that doesn't exist test_yaml = tmp_path / "test.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ esphome: name: test wifi: password: !secret nonexistent_secret -""") +""" + ) # Create an empty secrets file secrets_yaml = tmp_path / "secrets.yaml" @@ -118,10 +120,12 @@ def test_construct_secret_no_secrets_file(tmp_path: Path) -> None: """Test that missing secrets.yaml file raises proper error.""" # Create a YAML file with a secret but no secrets.yaml test_yaml = tmp_path / "test.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ wifi: password: !secret some_secret -""") +""" + ) # Mock CORE.config_path to avoid NoneType error with ( @@ -140,10 +144,12 @@ def test_construct_secret_fallback_to_main_config_dir( subdir.mkdir() test_yaml = subdir / "test.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ wifi: password: !secret test_secret -""") +""" + ) # Create secrets.yaml in the main directory main_secrets = tmp_path / "secrets.yaml" @@ -164,9 +170,11 @@ def test_construct_include_dir_named(fixture_path: Path, tmp_path: Path) -> None # Create test YAML that uses include_dir_named test_yaml = dst_dir / "test_include_named.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ sensor: !include_dir_named named_dir -""") +""" + ) actual = yaml_util.load_yaml(test_yaml) actual_sensor = actual["sensor"] @@ -199,9 +207,11 @@ def test_construct_include_dir_named_empty_dir(tmp_path: Path) -> None: empty_dir.mkdir() test_yaml = tmp_path / "test.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ sensor: !include_dir_named empty_dir -""") +""" + ) actual = yaml_util.load_yaml(test_yaml) @@ -231,9 +241,11 @@ def test_construct_include_dir_named_with_dots(tmp_path: Path) -> None: hidden_subfile.write_text("key: hidden_subfile_value") test_yaml = tmp_path / "test.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ test: !include_dir_named test_dir -""") +""" + ) actual = yaml_util.load_yaml(test_yaml) @@ -255,9 +267,11 @@ def test_find_files_recursive(fixture_path: Path, tmp_path: Path) -> None: # This indirectly tests _find_files by using include_dir_named test_yaml = dst_dir / "test_include_recursive.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ all_sensors: !include_dir_named named_dir -""") +""" + ) actual = yaml_util.load_yaml(test_yaml) From b3390d40fb959808fcc520bff17c4a1350f98420 Mon Sep 17 00:00:00 2001 From: Diorcet Yann Date: Tue, 24 Mar 2026 19:31:42 +0100 Subject: [PATCH 014/115] [core] Fix cg.add_define propagation to dependencies in native ESP-IDF builds (#15137) --- esphome/build_gen/espidf.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index 9df9b1069c..01923baaac 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -53,6 +53,13 @@ def get_project_cmakelists() -> str: variant = get_esp32_variant() idf_target = variant.lower().replace("-", "") + # Extract compile definitions from build flags (-DXXX -> XXX) + compile_defs = [flag for flag in CORE.build_flags if flag.startswith("-D")] + extra_compile_options = "\n".join( + f'idf_build_set_property(COMPILE_OPTIONS "{compile_def}" APPEND)' + for compile_def in compile_defs + ) + return f"""\ # Auto-generated by ESPHome cmake_minimum_required(VERSION 3.16) @@ -61,6 +68,9 @@ set(IDF_TARGET {idf_target}) set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src) include($ENV{{IDF_PATH}}/tools/cmake/project.cmake) + +{extra_compile_options} + project({CORE.name}) """ @@ -70,10 +80,6 @@ def get_component_cmakelists(minimal: bool = False) -> str: idf_requires = [] if minimal else (get_available_components() or []) requires_str = " ".join(idf_requires) - # Extract compile definitions from build flags (-DXXX -> XXX) - compile_defs = [flag[2:] for flag in CORE.build_flags if flag.startswith("-D")] - compile_defs_str = "\n ".join(sorted(compile_defs)) if compile_defs else "" - # Extract compile options (-W flags, excluding linker flags) compile_opts = [ flag @@ -104,11 +110,6 @@ idf_component_register( # Apply C++ standard target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20) -# ESPHome compile definitions -target_compile_definitions(${{COMPONENT_LIB}} PUBLIC - {compile_defs_str} -) - # ESPHome compile options target_compile_options(${{COMPONENT_LIB}} PUBLIC {compile_opts_str} From 3cd50f0495564c14f35b55448332079874682f12 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:31:08 -0400 Subject: [PATCH 015/115] [ci] Block new CONF_ constants from being added to esphome/const.py (#15145) --- script/ci-custom.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/script/ci-custom.py b/script/ci-custom.py index 25a0cf2127..7d0680a491 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -525,6 +525,29 @@ def lint_constants_usage(): return errs +# Maximum allowed CONF_ constants in esphome/const.py. +# This file is frozen — new constants go in esphome/components/const/__init__.py. +# Decrease this number when constants are moved out of const.py. +CONST_PY_MAX_CONF = 1011 + + +@lint_content_check(include=["esphome/const.py"]) +def lint_const_py_frozen(fname, content): + """Block new CONF_ constants from being added to esphome/const.py. + + New constants should go in esphome/components/const/__init__.py instead. + """ + count = sum(1 for line in content.splitlines() if line.startswith("CONF_")) + if count > CONST_PY_MAX_CONF: + return ( + "esphome/const.py is frozen. " + "Add new constants to esphome/components/const/__init__.py instead." + ) + if count < CONST_PY_MAX_CONF: + return f"CONST_PY_MAX_CONF in ci-custom.py should be updated to {count}." + return None + + def relative_cpp_search_text(fname: Path, content) -> str: parts = fname.parts integration = parts[2] From 55df21db516263a1f7b381035477a66cd84c5e8b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:44:28 -0400 Subject: [PATCH 016/115] [esp32] Default CPU frequency to maximum supported (#15143) --- esphome/components/esp32/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 1ecc270fd1..0e216485ac 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -379,12 +379,11 @@ FULL_CPU_FREQUENCIES = set(itertools.chain.from_iterable(CPU_FREQUENCIES.values( def set_core_data(config): cpu_frequency = config.get(CONF_CPU_FREQUENCY, None) variant = config[CONF_VARIANT] - # if not specified in config, set to 160MHz if supported, the fastest otherwise + # if not specified in config, default to the maximum supported frequency + # (ESP32-P4 engineering samples are limited to 360MHz, non-engineering can do 400MHz) if cpu_frequency is None: choices = CPU_FREQUENCIES[variant] - if "160MHZ" in choices: - cpu_frequency = "160MHZ" - elif "360MHZ" in choices: + if variant == VARIANT_ESP32P4 and config.get(CONF_ENGINEERING_SAMPLE): cpu_frequency = "360MHZ" else: cpu_frequency = choices[-1] From 22bc47da23a97187c74477fb282748d0935251aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Bl=C3=A4se?= Date: Tue, 24 Mar 2026 20:57:58 +0100 Subject: [PATCH 017/115] [light] Fix incorrect mode change handling on transition to off (#15147) --- esphome/components/light/transformers.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h index b6e5e08f2b..61fe098ad7 100644 --- a/esphome/components/light/transformers.h +++ b/esphome/components/light/transformers.h @@ -27,7 +27,7 @@ class LightTransitionTransformer : public LightTransformer { } // When changing color mode, go through off state, as color modes are orthogonal and there can't be two active. - if (this->start_values_.get_color_mode() != this->target_values_.get_color_mode()) { + if (this->start_values_.get_color_mode() != this->end_values_.get_color_mode()) { this->changing_color_mode_ = true; this->intermediate_values_ = this->start_values_; this->intermediate_values_.set_state(false); @@ -39,8 +39,8 @@ class LightTransitionTransformer : public LightTransformer { // Halfway through, when intermediate state (off) is reached, flip it to the target, but remain off. if (this->changing_color_mode_ && p > 0.5f && - this->intermediate_values_.get_color_mode() != this->target_values_.get_color_mode()) { - this->intermediate_values_ = this->target_values_; + this->intermediate_values_.get_color_mode() != this->end_values_.get_color_mode()) { + this->intermediate_values_ = this->end_values_; this->intermediate_values_.set_state(false); } From 8751f348c8ab50db26b4cfbc5abcc1c4703ba739 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:04:27 -0400 Subject: [PATCH 018/115] [sx127x] Fix FIFO read corruption (#15114) --- esphome/components/sx127x/sx127x.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index 66957a7342..0fddfdccdb 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -38,14 +38,18 @@ void SX127x::write_register_(uint8_t reg, uint8_t value) { void SX127x::read_fifo_(std::vector &packet) { this->enable(); this->write_byte(REG_FIFO & 0x7F); - this->read_array(packet.data(), packet.size()); + for (auto &byte : packet) { + byte = this->transfer_byte(0x00); + } this->disable(); } void SX127x::write_fifo_(const std::vector &packet) { this->enable(); this->write_byte(REG_FIFO | 0x80); - this->write_array(packet.data(), packet.size()); + for (const auto &byte : packet) { + this->transfer_byte(byte); + } this->disable(); } From 13baf260505116b2f6e3ae8e1ed8fbcd18820b58 Mon Sep 17 00:00:00 2001 From: Diorcet Yann Date: Tue, 24 Mar 2026 21:26:21 +0100 Subject: [PATCH 019/115] [core] get_log_str: fix false-positive error on null-terminated strings with stricter compilers (#15136) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/core/progmem.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/core/progmem.h b/esphome/core/progmem.h index 6c6a5252cf..031860e3a6 100644 --- a/esphome/core/progmem.h +++ b/esphome/core/progmem.h @@ -3,6 +3,7 @@ #include #include #include +#include #include "esphome/core/hal.h" // For PROGMEM definition @@ -104,7 +105,9 @@ struct LogString; static const char *get_(uint8_t idx, uint8_t fallback) { \ if (idx >= COUNT) \ idx = fallback; \ - return &BLOB[::esphome::progmem_read_byte(&OFFSETS[idx])]; \ + /* std::launder is used here to prevent the inter-procedural analysis that */ \ + /* causes the false positive that the string is not null terminated */ \ + return std::launder(&BLOB[::esphome::progmem_read_byte(&OFFSETS[idx])]); \ } \ static ::ProgmemStr get_progmem_str(uint8_t idx, uint8_t fallback) { \ return reinterpret_cast<::ProgmemStr>(get_(idx, fallback)); \ From 4ff85e2a1e0122f28a3b9d366e79b9fd112215f2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:48:17 -0400 Subject: [PATCH 020/115] [core] Fix clean-all to handle custom build paths (#15146) Co-authored-by: J. Nick Koston --- esphome/writer.py | 31 ++++++-- tests/unit_tests/test_writer.py | 124 ++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 4 deletions(-) diff --git a/esphome/writer.py b/esphome/writer.py index 69a35d00e3..4aac16ffd4 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -18,7 +18,6 @@ from esphome.core import CORE, EsphomeError from esphome.helpers import ( copy_file_if_changed, cpp_string_escape, - get_str_env, is_ha_addon, read_file, rmtree, @@ -441,18 +440,42 @@ def clean_build(clear_pio_cache: bool = True): rmtree(cache_dir) +def _get_custom_build_dir(item: Path, data_dir: Path) -> Path | None: + """Parse a YAML config to find a custom build directory.""" + from esphome import yaml_util + + try: + raw = yaml_util.load_yaml(item) + except (EsphomeError, OSError) as e: + _LOGGER.debug("Could not parse %s to find build_path: %s", item, e) + return None + if not isinstance(raw, dict): + return None + esphome_conf = raw.get("esphome", {}) + if not isinstance(esphome_conf, dict): + return None + if build_path := esphome_conf.get("build_path"): + return data_dir / build_path + return None + + def clean_all(configuration: list[str]): data_dirs = [] for config in configuration: item = Path(config) if item.is_file() and item.suffix in (".yaml", ".yml"): - data_dirs.append(item.parent / ".esphome") + data_dir = item.parent / ".esphome" + data_dirs.append(data_dir) + if custom := _get_custom_build_dir(item, data_dir): + data_dirs.append(custom) else: data_dirs.append(item / ".esphome") if is_ha_addon(): data_dirs.append(Path("/data")) - if "ESPHOME_DATA_DIR" in os.environ: - data_dirs.append(Path(get_str_env("ESPHOME_DATA_DIR", None))) + if env_data_dir := os.environ.get("ESPHOME_DATA_DIR"): + data_dirs.append(Path(env_data_dir)) + if env_build_path := os.environ.get("ESPHOME_BUILD_PATH"): + data_dirs.append(Path(env_build_path)) # Clean build dir for dir in data_dirs: diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index 134b63df4a..6ace38a7d7 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -866,6 +866,130 @@ def test_clean_all_with_yaml_file( assert str(build_dir) in caplog.text +@patch("esphome.writer.CORE") +def test_clean_all_with_yaml_build_path( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all cleans absolute build_path specified in YAML config.""" + config_dir = tmp_path / "config" + config_dir.mkdir() + + # Create an absolute custom build path directory with contents + custom_build = tmp_path / "custom_build" + custom_build.mkdir() + (custom_build / "firmware.bin").write_text("x") + sub = custom_build / "subdir" + sub.mkdir() + (sub / "file.txt").write_text("x") + + yaml_file = config_dir / "test.yaml" + # Absolute build_path: data_dir / absolute = absolute (Python Path behavior) + yaml_file.write_text(f"esphome:\n name: test\n build_path: {custom_build}\n") + + # Also create the normal .esphome dir + build_dir = config_dir / ".esphome" + build_dir.mkdir() + (build_dir / "dummy.txt").write_text("x") + + from esphome.writer import clean_all + + with caplog.at_level("INFO"): + clean_all([str(yaml_file)]) + + # Both .esphome and custom build_path should be cleaned + assert build_dir.exists() + assert not (build_dir / "dummy.txt").exists() + assert custom_build.exists() + assert not (custom_build / "firmware.bin").exists() + assert not sub.exists() + + +@patch("esphome.writer.CORE") +def test_clean_all_with_yaml_parse_error( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all still cleans .esphome when YAML parse fails.""" + config_dir = tmp_path / "config" + config_dir.mkdir() + yaml_file = config_dir / "test.yaml" + yaml_file.write_text("invalid: yaml: content: [") + + build_dir = config_dir / ".esphome" + build_dir.mkdir() + (build_dir / "dummy.txt").write_text("x") + + from esphome.writer import clean_all + + with caplog.at_level("INFO"): + clean_all([str(yaml_file)]) + + # .esphome should still be cleaned despite YAML parse failure + assert build_dir.exists() + assert not (build_dir / "dummy.txt").exists() + + +@patch("esphome.writer.CORE") +def test_clean_all_with_env_build_path( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all cleans ESPHOME_BUILD_PATH directory.""" + config_dir = tmp_path / "config" + config_dir.mkdir() + + build_dir = config_dir / ".esphome" + build_dir.mkdir() + (build_dir / "dummy.txt").write_text("x") + + # Create env build path directory + env_build = tmp_path / "env_build" + env_build.mkdir() + (env_build / "firmware.bin").write_text("x") + + from esphome.writer import clean_all + + with ( + caplog.at_level("INFO"), + patch.dict(os.environ, {"ESPHOME_BUILD_PATH": str(env_build)}), + ): + clean_all([str(config_dir)]) + + # Both should be cleaned + assert not (build_dir / "dummy.txt").exists() + assert env_build.exists() + assert not (env_build / "firmware.bin").exists() + + +@patch("esphome.writer.CORE") +def test_clean_all_ignores_empty_env_vars( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test clean_all ignores empty ESPHOME_BUILD_PATH/ESPHOME_DATA_DIR.""" + config_dir = tmp_path / "config" + config_dir.mkdir() + + # Create a file in cwd that must NOT be cleaned + marker = tmp_path / "important.txt" + marker.write_text("do not delete") + + from esphome.writer import clean_all + + with patch.dict( + os.environ, + {"ESPHOME_BUILD_PATH": "", "ESPHOME_DATA_DIR": ""}, + ): + clean_all([str(config_dir)]) + + # Empty env vars must not cause cwd to be cleaned + assert marker.exists() + + @patch("esphome.writer.CORE") def test_clean_all( mock_core: MagicMock, From 752fe30332749f1608823753dcd65c20f91448e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Mar 2026 14:01:59 -1000 Subject: [PATCH 021/115] [api] Add descriptive message to status warning when waiting for client (#15148) --- esphome/components/api/api_server.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 1151bc5983..d9c3cc6846 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -108,7 +108,7 @@ void APIServer::setup() { this->last_connected_ = App.get_loop_component_start_time(); // Set warning status if reboot timeout is enabled if (this->reboot_timeout_ != 0) { - this->status_set_warning(); + this->status_set_warning(LOG_STR("waiting for client connection")); } } @@ -187,7 +187,7 @@ void APIServer::remove_client_(size_t client_index) { // Last client disconnected - set warning and start tracking for reboot timeout if (this->clients_.empty() && this->reboot_timeout_ != 0) { - this->status_set_warning(); + this->status_set_warning(LOG_STR("waiting for client connection")); this->last_connected_ = App.get_loop_component_start_time(); } From 9fb5b6aa158582ea35e968f9b8209f4f1849fe06 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Mar 2026 14:03:18 -1000 Subject: [PATCH 022/115] [light] Replace initial_state storage with flash-resident callback (#15133) --- esphome/components/light/__init__.py | 12 +++++- esphome/components/light/light_state.cpp | 7 ++-- esphome/components/light/light_state.h | 10 +++-- .../fixtures/light_initial_state.yaml | 39 +++++++++++++++++++ tests/integration/test_light_initial_state.py | 38 ++++++++++++++++++ 5 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 tests/integration/fixtures/light_initial_state.yaml create mode 100644 tests/integration/test_light_initial_state.py diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 64452e4282..4090ca57c2 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -38,7 +38,7 @@ from esphome.const import ( CONF_WEB_SERVER, CONF_WHITE, ) -from esphome.core import CORE, ID, CoroPriority, HexInt, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, HexInt, Lambda, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -262,6 +262,8 @@ async def setup_light_core_(light_var, config, output_var): cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE])) if (initial_state_config := config.get(CONF_INITIAL_STATE)) is not None: + # Emit a stateless lambda that constructs the initial state — values live + # in flash as code, not stored in the LightState object (~40 bytes saved). initial_state = LightStateRTCState( initial_state_config.get(CONF_COLOR_MODE, ColorMode.UNKNOWN), initial_state_config.get(CONF_STATE, False), @@ -275,7 +277,13 @@ async def setup_light_core_(light_var, config, output_var): initial_state_config.get(CONF_COLD_WHITE, 1.0), initial_state_config.get(CONF_WARM_WHITE, 1.0), ) - cg.add(light_var.set_initial_state(initial_state)) + args = [(LightStateRTCState.operator("ref"), "s")] + lamb = await cg.process_lambda( + Lambda(f"s = {initial_state};"), + args, + return_type=cg.void, + ) + cg.add(light_var.set_initial_state(lamb)) if ( default_transition_length := config.get(CONF_DEFAULT_TRANSITION_LENGTH) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 1b736d84f6..bd778926d5 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -37,8 +37,9 @@ void LightState::setup() { auto call = this->make_call(); LightStateRTCState recovered{}; - if (this->initial_state_.has_value()) { - recovered = *this->initial_state_; + if (this->initial_state_callback_) { + this->initial_state_callback_(recovered); + this->initial_state_callback_ = nullptr; // One-shot — no longer needed } switch (this->restore_mode_) { case LIGHT_RESTORE_DEFAULT_OFF: @@ -195,7 +196,7 @@ void LightState::set_flash_transition_length(uint32_t flash_transition_length) { uint32_t LightState::get_flash_transition_length() const { return this->flash_transition_length_; } void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; } void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } -void LightState::set_initial_state(const LightStateRTCState &initial_state) { this->initial_state_ = initial_state; } +void LightState::set_initial_state(void (*callback)(LightStateRTCState &)) { this->initial_state_callback_ = callback; } bool LightState::supports_effects() { return !this->effects_.empty(); } const FixedVector &LightState::get_effects() const { return this->effects_; } void LightState::add_effects(const std::initializer_list &effects) { diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index ab7f2e4df8..5efc05358b 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -188,8 +188,9 @@ class LightState : public EntityBase, public Component { /// Set the restore mode of this light void set_restore_mode(LightRestoreMode restore_mode); - /// Set the initial state of this light - void set_initial_state(const LightStateRTCState &initial_state); + /// Set a callback to populate the initial state defaults during setup. + /// The callback is called once, then cleared. Values live in flash as code. + void set_initial_state(void (*callback)(LightStateRTCState &)); /// Return whether the light has any effects that meet the trait requirements. bool supports_effects(); @@ -342,8 +343,9 @@ class LightState : public EntityBase, public Component { */ std::unique_ptr> target_state_reached_listeners_; - /// Initial state of the light. - optional initial_state_{}; + /// Callback to populate initial state defaults — called once during setup, then cleared. + /// Values live in flash as function body; no per-instance data storage beyond this pointer. + void (*initial_state_callback_)(LightStateRTCState &){nullptr}; /// Value for storing the index of the currently active effect. 0 if no effect is active uint32_t active_effect_index_{}; diff --git a/tests/integration/fixtures/light_initial_state.yaml b/tests/integration/fixtures/light_initial_state.yaml new file mode 100644 index 0000000000..2654c76aa0 --- /dev/null +++ b/tests/integration/fixtures/light_initial_state.yaml @@ -0,0 +1,39 @@ +esphome: + name: light-initial-state-test +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +output: + - platform: template + id: test_red + type: float + write_action: + - lambda: "" + - platform: template + id: test_green + type: float + write_action: + - lambda: "" + - platform: template + id: test_blue + type: float + write_action: + - lambda: "" + +light: + - platform: rgb + name: "Test Light" + id: test_light + red: test_red + green: test_green + blue: test_blue + restore_mode: ALWAYS_OFF + initial_state: + color_mode: RGB + state: true + brightness: 0.75 + red: 1.0 + green: 0.5 + blue: 0.0 diff --git a/tests/integration/test_light_initial_state.py b/tests/integration/test_light_initial_state.py new file mode 100644 index 0000000000..f1cd96dbf0 --- /dev/null +++ b/tests/integration/test_light_initial_state.py @@ -0,0 +1,38 @@ +"""Integration test for light initial_state configuration. + +Tests that the initial_state values are correctly applied at boot when +no saved preferences exist. The initial_state callback populates defaults +that the restore logic uses as a fallback. +""" + +import pytest + +from .state_utils import InitialStateHelper, require_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_light_initial_state( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that initial_state values are applied at boot.""" + async with run_compiled(yaml_config), api_client_connected() as client: + entities, _ = await client.list_entities_services() + light = require_entity(entities, "test_light") + + helper = InitialStateHelper(entities) + client.subscribe_states(helper.on_state_wrapper(lambda s: None)) + await helper.wait_for_initial_states() + + state = helper.initial_states[light.key] + + # restore_mode: ALWAYS_OFF overrides state to false + assert state.state is False + + # But the color values from initial_state should be applied + assert state.brightness == pytest.approx(0.75, abs=0.05) + assert state.red == pytest.approx(1.0, abs=0.01) + assert state.green == pytest.approx(0.5, abs=0.01) + assert state.blue == pytest.approx(0.0, abs=0.01) From b6aec4fa25bb9f7fdb09e9738385f5b8fed45267 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Mar 2026 14:03:30 -1000 Subject: [PATCH 023/115] [ethernet] Add W5100 support for RP2040 (#15131) --- esphome/components/ethernet/__init__.py | 20 +++++++++----- .../components/ethernet/ethernet_component.h | 10 +++++++ .../ethernet/ethernet_component_rp2040.cpp | 26 ++++++++++++++----- esphome/core/defines.h | 1 + .../ethernet/common-w5100-rp2040.yaml | 18 +++++++++++++ .../ethernet/test-w5100.rp2040-ard.yaml | 1 + 6 files changed, 62 insertions(+), 14 deletions(-) create mode 100644 tests/components/ethernet/common-w5100-rp2040.yaml create mode 100644 tests/components/ethernet/test-w5100.rp2040-ard.yaml diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index e17abfcc93..17459cabb6 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -115,6 +115,7 @@ ETHERNET_TYPES = { "JL1101": EthernetType.ETHERNET_TYPE_JL1101, "KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081, "KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA, + "W5100": EthernetType.ETHERNET_TYPE_W5100, "W5500": EthernetType.ETHERNET_TYPE_W5500, "OPENETH": EthernetType.ETHERNET_TYPE_OPENETH, "DM9051": EthernetType.ETHERNET_TYPE_DM9051, @@ -132,6 +133,7 @@ _PHY_TYPE_TO_DEFINE = { "JL1101": "USE_ETHERNET_JL1101", "KSZ8081": "USE_ETHERNET_KSZ8081", "KSZ8081RNA": "USE_ETHERNET_KSZ8081", + "W5100": "USE_ETHERNET_W5100", "W5500": "USE_ETHERNET_W5500", "DM9051": "USE_ETHERNET_DM9051", "LAN8670": "USE_ETHERNET_LAN8670", @@ -164,9 +166,15 @@ _IDF6_ETHERNET_COMPONENTS: dict[str, IDFRegistryComponent] = { # These types are always external IDF components (never built-in to ESP-IDF) _ALWAYS_EXTERNAL_IDF_COMPONENTS = {"LAN8670", "ENC28J60"} -SPI_ETHERNET_TYPES = ["W5500", "DM9051", "ENC28J60"] +# ESP32-only SPI ethernet types (W5100 is RP2040-only, no ESP-IDF driver) +SPI_ETHERNET_TYPES = {"W5500", "DM9051", "ENC28J60"} # RP2040-supported SPI ethernet types -RP2040_SPI_ETHERNET_TYPES = ["W5500", "ENC28J60"] +RP2040_SPI_ETHERNET_TYPES = {"W5100", "W5500", "ENC28J60"} +_RP2040_SPI_LIBRARIES = { + "W5100": "lwIP_w5100", + "W5500": "lwIP_w5500", + "ENC28J60": "lwIP_enc28j60", +} SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10) emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t") @@ -295,7 +303,7 @@ def _validate(config): ) elif CORE.is_rp2040 and config[CONF_TYPE] not in RP2040_SPI_ETHERNET_TYPES: raise cv.Invalid( - f"Only {', '.join(RP2040_SPI_ETHERNET_TYPES)} are supported on RP2040, " + f"Only {', '.join(sorted(RP2040_SPI_ETHERNET_TYPES))} are supported on RP2040, " f"not {config[CONF_TYPE]}" ) return config @@ -382,6 +390,7 @@ CONFIG_SCHEMA = cv.All( "JL1101": RMII_SCHEMA, "KSZ8081": RMII_SCHEMA, "KSZ8081RNA": RMII_SCHEMA, + "W5100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])), "W5500": SPI_SCHEMA, "OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])), "DM9051": SPI_SCHEMA, @@ -574,10 +583,7 @@ async def _to_code_rp2040(var: cg.Pvariable, config: ConfigType) -> None: cg.add(var.set_reset_pin(config[CONF_RESET_PIN])) cg.add_define("USE_ETHERNET_SPI") - if config[CONF_TYPE] == "ENC28J60": - cg.add_library("lwIP_enc28j60", None) - else: - cg.add_library("lwIP_w5500", None) + cg.add_library(_RP2040_SPI_LIBRARIES[config[CONF_TYPE]], None) def _final_validate_rmii_pins(config: ConfigType) -> None: diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 4c85c39eb8..c6e37d01ea 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -25,6 +25,8 @@ extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void); #ifdef USE_RP2040 #if defined(USE_ETHERNET_W5500) #include +#elif defined(USE_ETHERNET_W5100) +#include #elif defined(USE_ETHERNET_ENC28J60) #include #else @@ -59,6 +61,7 @@ enum EthernetType : uint8_t { ETHERNET_TYPE_JL1101, ETHERNET_TYPE_KSZ8081, ETHERNET_TYPE_KSZ8081RNA, + ETHERNET_TYPE_W5100, ETHERNET_TYPE_W5500, ETHERNET_TYPE_OPENETH, ETHERNET_TYPE_DM9051, @@ -222,8 +225,15 @@ class EthernetComponent final : public Component { #ifdef USE_RP2040 static constexpr uint32_t LINK_CHECK_INTERVAL = 500; // ms between link/IP polls +#if defined(USE_ETHERNET_W5100) + static constexpr uint32_t RESET_DELAY_MS = 150; // W5100S PLL lock time +#else + static constexpr uint32_t RESET_DELAY_MS = 10; +#endif #if defined(USE_ETHERNET_W5500) Wiznet5500lwIP *eth_{nullptr}; +#elif defined(USE_ETHERNET_W5100) + Wiznet5100lwIP *eth_{nullptr}; #elif defined(USE_ETHERNET_ENC28J60) ENC28J60lwIP *eth_{nullptr}; #else diff --git a/esphome/components/ethernet/ethernet_component_rp2040.cpp b/esphome/components/ethernet/ethernet_component_rp2040.cpp index bd8c458985..9771bc59d5 100644 --- a/esphome/components/ethernet/ethernet_component_rp2040.cpp +++ b/esphome/components/ethernet/ethernet_component_rp2040.cpp @@ -31,12 +31,15 @@ void EthernetComponent::setup() { reset_pin.digital_write(false); delay(1); // NOLINT reset_pin.digital_write(true); - delay(10); // NOLINT - wait for chip to initialize after reset + // W5100S needs 150ms for PLL lock; W5500/ENC28J60 need ~10ms + delay(RESET_DELAY_MS); // NOLINT } // Create the SPI Ethernet device instance #if defined(USE_ETHERNET_W5500) this->eth_ = new Wiznet5500lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT +#elif defined(USE_ETHERNET_W5100) + this->eth_ = new Wiznet5100lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT #elif defined(USE_ETHERNET_ENC28J60) this->eth_ = new ENC28J60lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT #endif @@ -80,8 +83,8 @@ void EthernetComponent::setup() { // or via GPIO interrupt when one is provided. // Don't set started_ here — let the link polling in loop() set it - // when the W5500 link is actually up. Setting it prematurely causes - // a "Starting → Stopped → Starting" log sequence because the W5500 + // when the link is actually up. Setting it prematurely causes + // a "Starting → Stopped → Starting" log sequence because the chip // needs time after begin() before the PHY link is ready. } @@ -89,14 +92,21 @@ void EthernetComponent::loop() { // On RP2040, we need to poll connection state since there are no events. const uint32_t now = App.get_loop_component_start_time(); - // Throttle link/IP polling to avoid excessive SPI transactions from linkStatus() - // which reads the W5500 PHY register via SPI on every call. + // Throttle link/IP polling to avoid excessive SPI transactions. + // W5500/ENC28J60 read PHY register via SPI on every linkStatus() call. + // W5100 can't detect link state, so we skip the SPI read and assume link-up. // connected() reads netif->ip_addr without LwIPLock, but this is a single // 32-bit aligned read (atomic on ARM) — worst case is a one-iteration-stale // value, which is benign for polling. if (this->eth_ != nullptr && now - this->last_link_check_ >= LINK_CHECK_INTERVAL) { this->last_link_check_ = now; +#if defined(USE_ETHERNET_W5100) + // W5100 can't detect link (isLinkDetectable() returns false), so linkStatus() + // returns Unknown — assume link is up after successful begin() + bool link_up = true; +#else bool link_up = this->eth_->linkStatus() == LinkON; +#endif bool has_ip = this->eth_->connected(); if (!link_up) { @@ -171,6 +181,8 @@ void EthernetComponent::dump_config() { const char *type_str = "Unknown"; #if defined(USE_ETHERNET_W5500) type_str = "W5500"; +#elif defined(USE_ETHERNET_W5100) + type_str = "W5100"; #elif defined(USE_ETHERNET_ENC28J60) type_str = "ENC28J60"; #endif @@ -226,7 +238,7 @@ const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer( } eth_duplex_t EthernetComponent::get_duplex_mode() { - // Both W5500 and ENC28J60 are full-duplex on RP2040 + // W5100, W5500, and ENC28J60 are full-duplex on RP2040 return ETH_DUPLEX_FULL; } @@ -235,7 +247,7 @@ eth_speed_t EthernetComponent::get_link_speed() { // ENC28J60 is 10Mbps only return ETH_SPEED_10M; #else - // W5500 is always 100Mbps + // W5100 and W5500 are 100Mbps return ETH_SPEED_100M; #endif } diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 996818c2e6..676ad3024f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -284,6 +284,7 @@ #define USE_ETHERNET_SPI #define USE_ETHERNET_SPI_POLLING_SUPPORT #define USE_ETHERNET_OPENETH +#define USE_ETHERNET_W5100 #define USE_ETHERNET_W5500 #define USE_ETHERNET_DM9051 #define CONFIG_ETH_SPI_ETHERNET_W5500 1 diff --git a/tests/components/ethernet/common-w5100-rp2040.yaml b/tests/components/ethernet/common-w5100-rp2040.yaml new file mode 100644 index 0000000000..4c6d0313df --- /dev/null +++ b/tests/components/ethernet/common-w5100-rp2040.yaml @@ -0,0 +1,18 @@ +ethernet: + type: W5100 + clk_pin: 18 + mosi_pin: 19 + miso_pin: 16 + cs_pin: 17 + interrupt_pin: 21 + reset_pin: 20 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/test-w5100.rp2040-ard.yaml b/tests/components/ethernet/test-w5100.rp2040-ard.yaml new file mode 100644 index 0000000000..e101f3112a --- /dev/null +++ b/tests/components/ethernet/test-w5100.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common-w5100-rp2040.yaml From f457b995f726d54581421f0a885fdab4b922cdb0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Mar 2026 14:03:56 -1000 Subject: [PATCH 024/115] [datetime] Fix state_as_esptime() returning invalid timestamp (#15128) --- .../components/datetime/datetime_entity.cpp | 3 + esphome/core/time.cpp | 2 +- esphome/core/time.h | 18 ++++-- tests/components/time/posix_tz_parser.cpp | 56 ++++++++++++++++++- 4 files changed, 71 insertions(+), 8 deletions(-) diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index 730abb3ca8..fa50271f04 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -60,6 +60,9 @@ ESPTime DateTimeEntity::state_as_esptime() const { obj.year = this->year_; obj.month = this->month_; obj.day_of_month = this->day_; + obj.day_of_week = 0; + obj.day_of_year = 0; + obj.is_dst = false; obj.hour = this->hour_; obj.minute = this->minute_; obj.second = this->second_; diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 6add82e7d1..650c61d37b 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -231,7 +231,7 @@ void ESPTime::increment_day() { void ESPTime::recalc_timestamp_utc(bool use_day_of_year) { time_t res = 0; - if (!this->fields_in_range()) { + if (!this->fields_in_range(false, use_day_of_year)) { this->timestamp = -1; return; } diff --git a/esphome/core/time.h b/esphome/core/time.h index 1716c51ffd..ed47432038 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -79,11 +79,19 @@ struct ESPTime { /// Check if this ESPTime is valid (all fields in range and year is greater than or equal to 2019) bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); } - /// Check if all time fields of this ESPTime are in range. - bool fields_in_range() const { - return this->second < 61 && this->minute < 60 && this->hour < 24 && this->day_of_week > 0 && - this->day_of_week < 8 && this->day_of_year > 0 && this->day_of_year < 367 && this->month > 0 && - this->month < 13 && this->day_of_month > 0 && this->day_of_month <= days_in_month(this->month, this->year); + /// Check if time fields are in range. + /// @param check_day_of_week validate day_of_week (not always available when constructing from date/time fields) + /// @param check_day_of_year validate day_of_year (not always available when constructing from date/time fields) + bool fields_in_range(bool check_day_of_week = true, bool check_day_of_year = true) const { + bool valid = this->second < 61 && this->minute < 60 && this->hour < 24 && this->month > 0 && this->month < 13 && + this->day_of_month > 0 && this->day_of_month <= days_in_month(this->month, this->year); + if (check_day_of_week) { + valid = valid && this->day_of_week > 0 && this->day_of_week < 8; + } + if (check_day_of_year) { + valid = valid && this->day_of_year > 0 && this->day_of_year < 367; + } + return valid; } /** Convert a string to ESPTime struct as specified by the format argument. diff --git a/tests/components/time/posix_tz_parser.cpp b/tests/components/time/posix_tz_parser.cpp index d1747ef5b1..b7cf2a4afa 100644 --- a/tests/components/time/posix_tz_parser.cpp +++ b/tests/components/time/posix_tz_parser.cpp @@ -1036,8 +1036,6 @@ static time_t esptime_recalc_local(int year, int month, int day, int hour, int m t.hour = hour; t.minute = min; t.second = sec; - t.day_of_week = 1; // Placeholder for fields_in_range() - t.day_of_year = 1; t.recalc_timestamp_local(); return t.timestamp; } @@ -1187,6 +1185,60 @@ TEST(RecalcTimestampLocal, NonDefaultTransitionTime) { EXPECT_EQ(esp_result, libc_result); } +TEST(RecalcTimestampLocal, MinimalFieldsWithoutDayOfWeekOrYear) { + // Regression test for issue #15115: DateTimeEntity::state_as_esptime() constructs + // an ESPTime with only year/month/day/hour/minute/second set (no day_of_week or + // day_of_year). recalc_timestamp_local() must work without those fields. + const char *tz_str = "CET-1CEST,M3.5.0,M10.5.0"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + // Construct ESPTime with only date/time fields (like state_as_esptime does) + ESPTime t{}; + t.year = 2026; + t.month = 3; + t.day_of_month = 20; + t.hour = 23; + t.minute = 14; + t.second = 55; + // day_of_week and day_of_year are deliberately left as 0 + t.recalc_timestamp_local(); + + // Must NOT return -1 (the bug: fields_in_range() rejected valid times) + EXPECT_NE(t.timestamp, -1); + + // Verify against libc + time_t libc_result = libc_mktime(2026, 3, 20, 23, 14, 55); + EXPECT_EQ(t.timestamp, libc_result); +} + +TEST(RecalcTimestampLocal, MinimalFieldsNoDST) { + // Same test but with a timezone that has no DST + const char *tz_str = "IST-5:30"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + ESPTime t{}; + t.year = 2026; + t.month = 3; + t.day_of_month = 23; + t.hour = 10; + t.minute = 0; + t.second = 0; + t.recalc_timestamp_local(); + + EXPECT_NE(t.timestamp, -1); + + time_t libc_result = libc_mktime(2026, 3, 23, 10, 0, 0); + EXPECT_EQ(t.timestamp, libc_result); +} + TEST(RecalcTimestampLocal, YearBoundaryDST) { // Test southern hemisphere DST across year boundary // Australia/Sydney: DST active from October to April (spans Jan 1) From 238adbe008b5adbe60fca970cf96343e96a3dba9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Mar 2026 14:04:17 -1000 Subject: [PATCH 025/115] [wifi] Fix roaming counter reset from delayed disconnect and successful retry (#15126) --- esphome/components/wifi/wifi_component.cpp | 66 +++++++++++++++++----- esphome/components/wifi/wifi_component.h | 6 ++ 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e1d4b07471..2ed4b32a7a 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -287,18 +287,25 @@ bool CompactString::operator==(const StringRef &other) const { /// │ │ (counter reset to 0) │ │ (retry_connect called) │ /// │ └──────────────────────────────────┘ └───────────┬─────────────┘ /// │ │ │ -/// │ ↓ │ -/// │ ┌───────────────────────┐ │ -/// │ │ → IDLE │ │ -/// │ │ (counter preserved!) │ │ -/// │ └───────────────────────┘ │ +/// │ ┌─────────┴─────────┐ │ +/// │ ↓ ↓ │ +/// │ on target BSSID on other AP │ +/// │ │ │ │ +/// │ ↓ ↓ │ +/// │ ┌──────────────────┐ ┌────────────┐│ +/// │ │ → IDLE │ │ → IDLE ││ +/// │ │ (counter reset) │ │ (counter ││ +/// │ │ (roam worked!) │ │ preserved)││ +/// │ └──────────────────┘ └────────────┘│ /// │ │ /// │ Key behaviors: │ /// │ - After 3 checks: attempts >= 3, stop checking │ /// │ - Non-roaming disconnect: clear_roaming_state_() resets counter │ -/// │ - Disconnect during scan (SCANNING→RECONNECTING): counter preserved │ +/// │ - Disconnect during scan (SCANNING→RECONNECTING): counter preserved │ +/// │ - Disconnect after scan (within grace period): counter preserved │ /// │ - Roaming success (CONNECTING→IDLE): counter reset (can roam again) │ -/// │ - Roaming fail (RECONNECTING→IDLE): counter preserved (ping-pong) │ +/// │ - Roaming success via retry (on target BSSID): counter reset │ +/// │ - Roaming fail (RECONNECTING on other AP): counter preserved │ /// └──────────────────────────────────────────────────────────────────────┘ // Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266) @@ -1576,17 +1583,33 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { // Only preserve attempts if reconnecting after a failed roam attempt // This prevents ping-pong between APs when a roam target is unreachable if (this->roaming_state_ == RoamingState::CONNECTING) { - // Successful roam to better AP - reset attempts so we can roam again later + // Successful roam to better AP on first try - reset attempts so we can roam again later ESP_LOGD(TAG, "Roam successful"); this->roaming_attempts_ = 0; } else if (this->roaming_state_ == RoamingState::RECONNECTING) { - // Failed roam, reconnected via normal recovery - keep attempts to prevent ping-pong - ESP_LOGD(TAG, "Reconnected after failed roam (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + // Check if we ended up on the roam target despite needing a retry + // (e.g., first connect failed but scan-based retry found and connected to the same better AP) + bssid_t current_bssid = this->wifi_bssid(); + if (this->roaming_target_bssid_ != bssid_t{} && current_bssid == this->roaming_target_bssid_) { + char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(current_bssid.data(), bssid_buf); + ESP_LOGD(TAG, "Roam successful (via retry, attempt %u/%u) to %s", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS, + bssid_buf); + this->roaming_attempts_ = 0; + } else if (this->roaming_target_bssid_ != bssid_t{}) { + // Failed roam to specific target, reconnected to different AP - keep attempts to prevent ping-pong + ESP_LOGD(TAG, "Reconnected after failed roam (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + } else { + // Reconnected after scan-induced disconnect (no roam target) - keep attempts + ESP_LOGD(TAG, "Reconnected after roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + } } else { // Normal connection (boot, credentials changed, etc.) this->roaming_attempts_ = 0; } this->roaming_state_ = RoamingState::IDLE; + this->roaming_target_bssid_ = {}; + this->roaming_scan_end_ = 0; // Clear all priority penalties - the next reconnect will happen when an AP disconnects, // which means the landscape has likely changed and previous tracked failures are stale @@ -2073,8 +2096,16 @@ void WiFiComponent::retry_connect() { ESP_LOGD(TAG, "Disconnected during roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); this->roaming_state_ = RoamingState::RECONNECTING; } else if (this->roaming_state_ == RoamingState::IDLE) { - // Not a roaming-triggered reconnect, reset state - this->clear_roaming_state_(); + // Check if a roaming scan recently completed - on ESP8266, going off-channel + // during scan can cause a delayed Beacon Timeout 8-20 seconds after scan finishes. + // Transition to RECONNECTING so the attempts counter is preserved on reconnect. + if (this->roaming_scan_end_ != 0 && millis() - this->roaming_scan_end_ < ROAMING_SCAN_GRACE_PERIOD) { + ESP_LOGD(TAG, "Disconnect after roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + this->roaming_state_ = RoamingState::RECONNECTING; + } else { + // Not a roaming-triggered reconnect, reset state + this->clear_roaming_state_(); + } } // RECONNECTING: keep state and counter, still trying to reconnect @@ -2307,6 +2338,8 @@ bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this-> void WiFiComponent::clear_roaming_state_() { this->roaming_attempts_ = 0; this->roaming_last_check_ = 0; + this->roaming_scan_end_ = 0; + this->roaming_target_bssid_ = {}; this->roaming_state_ = RoamingState::IDLE; } @@ -2374,7 +2407,7 @@ void WiFiComponent::check_roaming_(uint32_t now) { // Guard: skip scan if signal is already good (no meaningful improvement possible) int8_t rssi = this->wifi_rssi(); if (rssi > ROAMING_GOOD_RSSI) { - ESP_LOGV(TAG, "Roam check skipped, signal good (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_, + ESP_LOGD(TAG, "Roam check skipped, signal good (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); return; } @@ -2388,6 +2421,9 @@ void WiFiComponent::process_roaming_scan_() { this->scan_done_ = false; // Default to IDLE - will be set to CONNECTING if we find a better AP this->roaming_state_ = RoamingState::IDLE; + // Record when scan completed so delayed disconnects (e.g., ESP8266 Beacon Timeout) + // can be attributed to the scan and avoid resetting the attempts counter + this->roaming_scan_end_ = millis(); // Get current connection info int8_t current_rssi = this->wifi_rssi(); @@ -2436,10 +2472,12 @@ void WiFiComponent::process_roaming_scan_() { WiFiAP roam_params = *selected; apply_scan_result_to_params(roam_params, *best); - this->release_scan_results_(); // Mark as roaming attempt - affects retry behavior if connection fails this->roaming_state_ = RoamingState::CONNECTING; + this->roaming_target_bssid_ = best->get_bssid(); // Must read before releasing scan results + + this->release_scan_results_(); // Connect directly - wifi_sta_connect_ handles disconnect internally this->start_connecting(roam_params); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 718f4a6e12..99b23436f7 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -779,6 +779,10 @@ class WiFiComponent final : public Component { static constexpr int8_t ROAMING_MIN_IMPROVEMENT = 10; // dB static constexpr int8_t ROAMING_GOOD_RSSI = -49; // Skip scan if signal is excellent static constexpr uint8_t ROAMING_MAX_ATTEMPTS = 3; + // Grace period after roaming scan completes. If WiFi disconnects within this + // window (e.g., ESP8266 Beacon Timeout caused by going off-channel during scan), + // the disconnect is treated as roaming-related and the attempts counter is preserved. + static constexpr uint32_t ROAMING_SCAN_GRACE_PERIOD = 30 * 1000; // 30 seconds // 4-byte members float output_power_{NAN}; @@ -786,6 +790,7 @@ class WiFiComponent final : public Component { uint32_t last_connected_{0}; uint32_t reboot_timeout_{}; uint32_t roaming_last_check_{0}; + uint32_t roaming_scan_end_{0}; // Timestamp when last roaming scan completed #ifdef USE_WIFI_AP uint32_t ap_timeout_{}; #endif @@ -810,6 +815,7 @@ class WiFiComponent final : public Component { bool error_from_callback_{false}; RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY}; RoamingState roaming_state_{RoamingState::IDLE}; + bssid_t roaming_target_bssid_{}; // BSSID of the AP we're trying to roam to #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE}; #endif From 9c9ae190ee040b7eb97f7e82d74e73b7ea6cd7d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Mar 2026 14:13:59 -1000 Subject: [PATCH 026/115] [core] Use compile-time HasElse parameter in IfAction (#15134) --- esphome/automation.py | 7 +++++-- esphome/core/base_automation.h | 27 ++++++++++++++------------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index 36ab30b654..17966dc782 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -413,13 +413,16 @@ async def if_action_to_code( template_arg: cg.TemplateArguments, args: TemplateArgsType, ) -> MockObj: + has_else = CONF_ELSE in config + # Prepend HasElse bool to template arguments: IfAction + if_template_arg = cg.TemplateArguments(has_else, *template_arg) cond_conf = next(el for el in config if el in (CONF_ANY, CONF_ALL, CONF_CONDITION)) condition = await build_condition(config[cond_conf], template_arg, args) - var = cg.new_Pvariable(action_id, template_arg, condition) + var = cg.new_Pvariable(action_id, if_template_arg, condition) if CONF_THEN in config: actions = await build_action_list(config[CONF_THEN], template_arg, args) cg.add(var.add_then(actions)) - if CONF_ELSE in config: + if has_else: actions = await build_action_list(config[CONF_ELSE], template_arg, args) cg.add(var.add_else(actions)) return var diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 985f26e711..efcffa8824 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -264,7 +264,7 @@ template class WhileLoopContinuation : public Action { WhileAction *parent_; }; -template class IfAction : public Action { +template class IfAction : public Action { public: explicit IfAction(Condition *condition) : condition_(condition) {} @@ -273,27 +273,25 @@ template class IfAction : public Action { this->then_.add_action(new ContinuationAction(this)); } - void add_else(const std::initializer_list *> &actions) { + void add_else(const std::initializer_list *> &actions) requires(HasElse) { this->else_.add_actions(actions); this->else_.add_action(new ContinuationAction(this)); } void play_complex(const Ts &...x) override { this->num_running_++; - bool res = this->condition_->check(x...); - if (res) { - if (this->then_.empty()) { - this->play_next_(x...); - } else if (this->num_running_ > 0) { + if (this->condition_->check(x...)) { + if (!this->then_.empty() && this->num_running_ > 0) { this->then_.play(x...); + return; } - } else { - if (this->else_.empty()) { - this->play_next_(x...); - } else if (this->num_running_ > 0) { + } else if constexpr (HasElse) { + if (!this->else_.empty() && this->num_running_ > 0) { this->else_.play(x...); + return; } } + this->play_next_(x...); } void play(const Ts &...x) override { /* ignore - see play_complex */ @@ -301,13 +299,16 @@ template class IfAction : public Action { void stop() override { this->then_.stop(); - this->else_.stop(); + if constexpr (HasElse) { + this->else_.stop(); + } } protected: Condition *condition_; ActionList then_; - ActionList else_; + struct NoElse {}; + [[no_unique_address]] std::conditional_t, NoElse> else_; }; template class WhileAction : public Action { From 26e78c840ce4e35589245279e7d39f27039af851 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Mar 2026 20:21:04 -0400 Subject: [PATCH 027/115] [wifi] Filter fast_connect by band_mode and use background scan for roaming (#15152) --- esphome/components/wifi/wifi_component.cpp | 8 ++++++++ esphome/components/wifi/wifi_component.h | 2 ++ esphome/components/wifi/wifi_component_esp_idf.cpp | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 2ed4b32a7a..620d1a083d 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -2222,6 +2222,14 @@ bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { params.set_hidden(false); ESP_LOGD(TAG, "Loaded fast_connect settings"); +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) + if ((this->band_mode_ == WIFI_BAND_MODE_5G_ONLY && fast_connect_save.channel < FIRST_5GHZ_CHANNEL) || + (this->band_mode_ == WIFI_BAND_MODE_2G_ONLY && fast_connect_save.channel >= FIRST_5GHZ_CHANNEL)) { + ESP_LOGW(TAG, "Saved channel %u not allowed by band mode, ignoring fast_connect", fast_connect_save.channel); + this->selected_sta_index_ = -1; + return false; + } +#endif return true; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 99b23436f7..55e532c37d 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -774,6 +774,8 @@ class WiFiComponent final : public Component { SemaphoreHandle_t high_performance_semaphore_{nullptr}; #endif + static constexpr uint8_t FIRST_5GHZ_CHANNEL = 36; + // Post-connect roaming constants static constexpr uint32_t ROAMING_CHECK_INTERVAL = 5 * 60 * 1000; // 5 minutes static constexpr int8_t ROAMING_MIN_IMPROVEMENT = 10; // dB diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 2866ec1513..1b80adc82e 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -987,6 +987,11 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { config.scan_time.active.min = 100; config.scan_time.active.max = 300; } + // When scanning while connected (roaming), return to home channel between + // each scanned channel to maintain the connection (helps with BLE/WiFi coexistence) + if (this->roaming_state_ == RoamingState::SCANNING) { + config.coex_background_scan = true; + } esp_err_t err = esp_wifi_scan_start(&config, false); if (err != ESP_OK) { From 690dc324c97d753788491b1fa8c423776af2a1a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Mar 2026 14:52:37 -1000 Subject: [PATCH 028/115] [logger] Move task log buffer storage to BSS (#15153) --- esphome/components/logger/__init__.py | 20 +++++-------- esphome/components/logger/logger.cpp | 26 +++++------------ esphome/components/logger/logger.h | 13 +++------ .../logger/task_log_buffer_esp32.cpp | 18 +++--------- .../components/logger/task_log_buffer_esp32.h | 13 ++++----- .../logger/task_log_buffer_host.cpp | 17 +++-------- .../components/logger/task_log_buffer_host.h | 13 ++------- .../logger/task_log_buffer_libretiny.cpp | 29 +++++++------------ .../logger/task_log_buffer_libretiny.h | 12 ++++---- .../logger/task_log_buffer_zephyr.cpp | 13 ++++----- .../logger/task_log_buffer_zephyr.h | 10 ++++--- esphome/core/defines.h | 6 ++++ tests/benchmarks/components/main.cpp | 2 +- tests/components/main.cpp | 2 +- tests/dummy_main.cpp | 2 +- 15 files changed, 69 insertions(+), 127 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 4345e291a3..4144543b89 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -331,9 +331,8 @@ async def to_code(config: ConfigType) -> None: CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level tx_buffer_size = config[CONF_TX_BUFFER_SIZE] cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size) - # Determine task log buffer size and define USE_ESPHOME_TASK_LOG_BUFFER early - # so the constructor can allocate the buffer immediately, preventing a race - # where another task logs before the buffer is initialized. + # Determine task log buffer size. The buffer is a direct member of Logger + # (no separate heap allocation). task_log_buffer_size = 0 if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] @@ -341,16 +340,11 @@ async def to_code(config: ConfigType) -> None: task_log_buffer_size = 64 # Fixed 64 slots for host if task_log_buffer_size > 0: cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") - log = cg.new_Pvariable( - config[CONF_ID], - baud_rate, - task_log_buffer_size, - ) - else: - log = cg.new_Pvariable( - config[CONF_ID], - baud_rate, - ) + cg.add_define("ESPHOME_TASK_LOG_BUFFER_SIZE", task_log_buffer_size) + log = cg.new_Pvariable( + config[CONF_ID], + baud_rate, + ) if CORE.is_esp32 or CORE.is_host: cg.add(log.create_pthread_key()) # set_uart_selection() must be called before pre_setup() because diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index ceacded775..cd6543bfb8 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -83,7 +83,7 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li #ifdef USE_ESPHOME_TASK_LOG_BUFFER // For non-main threads/tasks, queue the message for callbacks message_sent = - this->log_buffer_->send_message_thread_safe(level, tag, static_cast(line), thread_name, format, args); + this->log_buffer_.send_message_thread_safe(level, tag, static_cast(line), thread_name, format, args); if (message_sent) { // Enable logger loop to process the buffered message // This is safe to call from any context including ISRs @@ -152,23 +152,13 @@ inline uint8_t Logger::level_for(const char *tag) { return this->current_level_; } -#ifdef USE_ESPHOME_TASK_LOG_BUFFER -Logger::Logger(uint32_t baud_rate, size_t task_log_buffer_size) : baud_rate_(baud_rate) { -#else Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) { -#endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) this->main_task_ = xTaskGetCurrentTaskHandle(); #elif defined(USE_ZEPHYR) this->main_task_ = k_current_get(); #elif defined(USE_HOST) -this->main_thread_ = pthread_self(); -#endif -#ifdef USE_ESPHOME_TASK_LOG_BUFFER - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed - this->log_buffer_ = new logger::TaskLogBuffer(task_log_buffer_size); - // Note: we don't disable loop here because the component isn't registered with App yet. - // The loop self-disables on its first iteration when it finds no messages to process. + this->main_thread_ = pthread_self(); #endif } @@ -184,16 +174,16 @@ void Logger::loop() { void Logger::process_messages_() { #ifdef USE_ESPHOME_TASK_LOG_BUFFER // Process any buffered messages when available - if (this->log_buffer_->has_messages()) { + if (this->log_buffer_.has_messages()) { logger::TaskLogBuffer::LogMessage *message; uint16_t text_length; - while (this->log_buffer_->borrow_message_main_loop(message, text_length)) { + while (this->log_buffer_.borrow_message_main_loop(message, text_length)) { const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; LogBuffer buf{this->tx_buffer_, ESPHOME_LOGGER_TX_BUFFER_SIZE}; this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text_data(), text_length, buf); // Release the message to allow other tasks to use it as soon as possible - this->log_buffer_->release_message_main_loop(); + this->log_buffer_.release_message_main_loop(); this->write_log_buffer_to_console_(buf); } } @@ -239,13 +229,11 @@ void Logger::dump_config() { this->baud_rate_, LOG_STR_ARG(get_uart_selection_())); #endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER - if (this->log_buffer_) { #ifdef USE_HOST - ESP_LOGCONFIG(TAG, " Task Log Buffer Slots: %u", static_cast(this->log_buffer_->size())); + ESP_LOGCONFIG(TAG, " Task Log Buffer Slots: %u", static_cast(this->log_buffer_.size())); #else - ESP_LOGCONFIG(TAG, " Task Log Buffer Size: %u bytes", static_cast(this->log_buffer_->size())); + ESP_LOGCONFIG(TAG, " Task Log Buffer Size: %u bytes", static_cast(this->log_buffer_.size())); #endif - } #endif #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index c81b8e4e94..784cbea67e 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -143,11 +143,7 @@ enum UARTSelection : uint8_t { */ class Logger final : public Component { public: -#ifdef USE_ESPHOME_TASK_LOG_BUFFER - explicit Logger(uint32_t baud_rate, size_t task_log_buffer_size); -#else explicit Logger(uint32_t baud_rate); -#endif #if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void loop() override; #endif @@ -353,10 +349,6 @@ class Logger final : public Component { #ifdef USE_LOGGER_LEVEL_LISTENERS std::vector level_listeners_; // Log level change listeners #endif -#ifdef USE_ESPHOME_TASK_LOG_BUFFER - logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed -#endif - // Group smaller types together at the end uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) @@ -374,8 +366,11 @@ class Logger final : public Component { bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms #endif - // Large buffer placed last to keep frequently-accessed member offsets small + // Large buffers placed last to keep frequently-accessed member offsets small char tx_buffer_[ESPHOME_LOGGER_TX_BUFFER_SIZE + 1]; // +1 for null terminator +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + logger::TaskLogBuffer log_buffer_; // Embedded in Logger (no separate heap allocation) +#endif // --- get_thread_name_ overloads (per-platform) --- diff --git a/esphome/components/logger/task_log_buffer_esp32.cpp b/esphome/components/logger/task_log_buffer_esp32.cpp index e747ddc4d8..cb97f5504f 100644 --- a/esphome/components/logger/task_log_buffer_esp32.cpp +++ b/esphome/components/logger/task_log_buffer_esp32.cpp @@ -1,33 +1,23 @@ #ifdef USE_ESP32 #include "task_log_buffer_esp32.h" -#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESPHOME_TASK_LOG_BUFFER namespace esphome::logger { -TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) { - // Store the buffer size - this->size_ = total_buffer_size; - // Allocate memory for the ring buffer using ESPHome's RAM allocator - RAMAllocator allocator; - this->storage_ = allocator.allocate(this->size_); +TaskLogBuffer::TaskLogBuffer() { // Create a static ring buffer with RINGBUF_TYPE_NOSPLIT for message integrity - this->ring_buffer_ = xRingbufferCreateStatic(this->size_, RINGBUF_TYPE_NOSPLIT, this->storage_, &this->structure_); + // Storage is a member array (embedded in Logger), no heap allocation needed + this->ring_buffer_ = + xRingbufferCreateStatic(sizeof(this->storage_), RINGBUF_TYPE_NOSPLIT, this->storage_, &this->structure_); } TaskLogBuffer::~TaskLogBuffer() { if (this->ring_buffer_ != nullptr) { - // Delete the ring buffer vRingbufferDelete(this->ring_buffer_); this->ring_buffer_ = nullptr; - - // Free the allocated memory - RAMAllocator allocator; - allocator.deallocate(this->storage_, this->size_); - this->storage_ = nullptr; } } diff --git a/esphome/components/logger/task_log_buffer_esp32.h b/esphome/components/logger/task_log_buffer_esp32.h index 88d72eacfc..e819766795 100644 --- a/esphome/components/logger/task_log_buffer_esp32.h +++ b/esphome/components/logger/task_log_buffer_esp32.h @@ -8,7 +8,6 @@ #ifdef USE_ESPHOME_TASK_LOG_BUFFER #include #include -#include #include #include #include @@ -47,8 +46,7 @@ class TaskLogBuffer { inline const char *text_data() const { return reinterpret_cast(this) + sizeof(LogMessage); } }; - // Constructor that takes a total buffer size - explicit TaskLogBuffer(size_t total_buffer_size); + TaskLogBuffer(); ~TaskLogBuffer(); // NOT thread-safe - borrow a message from the ring buffer, only call from main loop @@ -67,13 +65,12 @@ class TaskLogBuffer { } // Get the total buffer size in bytes - inline size_t size() const { return size_; } + static constexpr size_t size() { return ESPHOME_TASK_LOG_BUFFER_SIZE; } private: - RingbufHandle_t ring_buffer_{nullptr}; // FreeRTOS ring buffer handle - StaticRingbuffer_t structure_; // Static structure for the ring buffer - uint8_t *storage_{nullptr}; // Pointer to allocated memory - size_t size_{0}; // Size of allocated memory + RingbufHandle_t ring_buffer_{nullptr}; // FreeRTOS ring buffer handle + StaticRingbuffer_t structure_; // Static structure for the ring buffer + uint8_t storage_[ESPHOME_TASK_LOG_BUFFER_SIZE]; // Embedded in Logger (no separate heap allocation) // Atomic counter for message tracking (only differences matter) std::atomic message_counter_{0}; // Incremented when messages are committed diff --git a/esphome/components/logger/task_log_buffer_host.cpp b/esphome/components/logger/task_log_buffer_host.cpp index c2ab009db4..8ebc946383 100644 --- a/esphome/components/logger/task_log_buffer_host.cpp +++ b/esphome/components/logger/task_log_buffer_host.cpp @@ -10,22 +10,13 @@ namespace esphome::logger { -TaskLogBuffer::TaskLogBuffer(size_t slot_count) : slot_count_(slot_count) { - // Allocate message slots - this->slots_ = std::make_unique(slot_count); -} - -TaskLogBuffer::~TaskLogBuffer() { - // unique_ptr handles cleanup automatically -} - int TaskLogBuffer::acquire_write_slot_() { // Try to reserve a slot using compare-and-swap size_t current_reserve = this->reserve_index_.load(std::memory_order_relaxed); while (true) { // Calculate next index (with wrap-around) - size_t next_reserve = (current_reserve + 1) % this->slot_count_; + size_t next_reserve = (current_reserve + 1) % ESPHOME_TASK_LOG_BUFFER_SIZE; // Check if buffer would be full // Buffer is full when next write position equals read position @@ -50,7 +41,7 @@ void TaskLogBuffer::commit_write_slot_(int slot_index) { // Try to advance the write_index if we're the next expected commit // This ensures messages are read in order size_t expected = slot_index; - size_t next = (slot_index + 1) % this->slot_count_; + size_t next = (slot_index + 1) % ESPHOME_TASK_LOG_BUFFER_SIZE; // We only advance write_index if this slot is the next one expected // This handles out-of-order commits correctly @@ -63,7 +54,7 @@ void TaskLogBuffer::commit_write_slot_(int slot_index) { // Successfully advanced, check if next slot is also ready expected = next; - next = (next + 1) % this->slot_count_; + next = (next + 1) % ESPHOME_TASK_LOG_BUFFER_SIZE; if (!this->slots_[expected].ready.load(std::memory_order_acquire)) { break; } @@ -142,7 +133,7 @@ void TaskLogBuffer::release_message_main_loop() { this->slots_[current_read].ready.store(false, std::memory_order_release); // Advance read index - size_t next_read = (current_read + 1) % this->slot_count_; + size_t next_read = (current_read + 1) % ESPHOME_TASK_LOG_BUFFER_SIZE; this->read_index_.store(next_read, std::memory_order_release); } diff --git a/esphome/components/logger/task_log_buffer_host.h b/esphome/components/logger/task_log_buffer_host.h index 1d4d2b0ec1..25e9c4da58 100644 --- a/esphome/components/logger/task_log_buffer_host.h +++ b/esphome/components/logger/task_log_buffer_host.h @@ -11,7 +11,6 @@ #include #include #include -#include #include namespace esphome::logger { @@ -50,9 +49,6 @@ namespace esphome::logger { */ class TaskLogBuffer { public: - // Default number of message slots - host has plenty of memory - static constexpr size_t DEFAULT_SLOT_COUNT = 64; - // Structure for a log message (fixed size for lock-free operation) struct LogMessage { // Size constants @@ -74,9 +70,7 @@ class TaskLogBuffer { inline char *text_data() { return this->text; } }; - /// Constructor that takes the number of message slots - explicit TaskLogBuffer(size_t slot_count); - ~TaskLogBuffer(); + TaskLogBuffer() = default; // NOT thread-safe - get next message from buffer, only call from main loop // Returns true if a message was retrieved, false if buffer is empty @@ -96,7 +90,7 @@ class TaskLogBuffer { } // Get the buffer size (number of slots) - inline size_t size() const { return slot_count_; } + static constexpr size_t size() { return ESPHOME_TASK_LOG_BUFFER_SIZE; } private: // Acquire a slot for writing (thread-safe) @@ -106,8 +100,7 @@ class TaskLogBuffer { // Commit a slot after writing (thread-safe) void commit_write_slot_(int slot_index); - std::unique_ptr slots_; // Pre-allocated message slots - size_t slot_count_; // Number of slots + LogMessage slots_[ESPHOME_TASK_LOG_BUFFER_SIZE]; // Embedded in Logger (no separate heap allocation) // Lock-free indices using atomics // - reserve_index_: Next slot to reserve (producers CAS this to claim slots) diff --git a/esphome/components/logger/task_log_buffer_libretiny.cpp b/esphome/components/logger/task_log_buffer_libretiny.cpp index 5969f6fb40..b6d6b22ab5 100644 --- a/esphome/components/logger/task_log_buffer_libretiny.cpp +++ b/esphome/components/logger/task_log_buffer_libretiny.cpp @@ -1,19 +1,15 @@ #ifdef USE_LIBRETINY #include "task_log_buffer_libretiny.h" -#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESPHOME_TASK_LOG_BUFFER namespace esphome::logger { -TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) { - this->size_ = total_buffer_size; - // Allocate memory for the circular buffer using ESPHome's RAM allocator - RAMAllocator allocator; - this->storage_ = allocator.allocate(this->size_); +TaskLogBuffer::TaskLogBuffer() { // Create mutex for thread-safe access + // Storage is a member array (embedded in Logger), no heap allocation needed this->mutex_ = xSemaphoreCreateMutex(); } @@ -22,11 +18,6 @@ TaskLogBuffer::~TaskLogBuffer() { vSemaphoreDelete(this->mutex_); this->mutex_ = nullptr; } - if (this->storage_ != nullptr) { - RAMAllocator allocator; - allocator.deallocate(this->storage_, this->size_); - this->storage_ = nullptr; - } } size_t TaskLogBuffer::available_contiguous_space() const { @@ -34,7 +25,7 @@ size_t TaskLogBuffer::available_contiguous_space() const { // head is ahead of or equal to tail // Available space is from head to end, plus from start to tail // But for contiguous, just from head to end (minus 1 to avoid head==tail ambiguity) - size_t space_to_end = this->size_ - this->head_; + size_t space_to_end = ESPHOME_TASK_LOG_BUFFER_SIZE - this->head_; if (this->tail_ == 0) { // Can't use the last byte or head would equal tail return space_to_end > 0 ? space_to_end - 1 : 0; @@ -48,8 +39,8 @@ size_t TaskLogBuffer::available_contiguous_space() const { } bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) { - // Check if buffer was initialized successfully - if (this->mutex_ == nullptr || this->storage_ == nullptr) { + // Check if mutex was initialized successfully + if (this->mutex_ == nullptr) { return false; } @@ -86,7 +77,7 @@ void TaskLogBuffer::release_message_main_loop() { this->tail_ += this->current_message_size_; // Handle wrap-around if we've reached the end - if (this->tail_ >= this->size_) { + if (this->tail_ >= ESPHOME_TASK_LOG_BUFFER_SIZE) { this->tail_ = 0; } @@ -115,9 +106,9 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin // Calculate total size needed (header + text length + null terminator) size_t total_size = message_total_size(text_length); - // Check if buffer was initialized successfully - if (this->mutex_ == nullptr || this->storage_ == nullptr) { - return false; // Buffer not initialized, fall back to direct output + // Check if mutex was initialized successfully + if (this->mutex_ == nullptr) { + return false; // Mutex not initialized, fall back to direct output } // Try to acquire mutex without blocking - don't block logging tasks @@ -185,7 +176,7 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin this->head_ += total_size; // Handle wrap-around (shouldn't happen due to contiguous space check, but be safe) - if (this->head_ >= this->size_) { + if (this->head_ >= ESPHOME_TASK_LOG_BUFFER_SIZE) { this->head_ = 0; } diff --git a/esphome/components/logger/task_log_buffer_libretiny.h b/esphome/components/logger/task_log_buffer_libretiny.h index c065065fe7..b42894502a 100644 --- a/esphome/components/logger/task_log_buffer_libretiny.h +++ b/esphome/components/logger/task_log_buffer_libretiny.h @@ -59,8 +59,7 @@ class TaskLogBuffer { // Valid log levels are 0-7, so 0xFF cannot be a real message static constexpr uint8_t PADDING_MARKER_LEVEL = 0xFF; - // Constructor that takes a total buffer size - explicit TaskLogBuffer(size_t total_buffer_size); + TaskLogBuffer(); ~TaskLogBuffer(); // NOT thread-safe - borrow a message from the buffer, only call from main loop @@ -78,7 +77,7 @@ class TaskLogBuffer { inline bool HOT has_messages() const { return this->message_count_ != 0; } // Get the total buffer size in bytes - inline size_t size() const { return this->size_; } + static constexpr size_t size() { return ESPHOME_TASK_LOG_BUFFER_SIZE; } private: // Calculate total size needed for a message (header + text + null terminator) @@ -87,10 +86,9 @@ class TaskLogBuffer { // Calculate available contiguous space at write position size_t available_contiguous_space() const; - uint8_t *storage_{nullptr}; // Pointer to allocated memory - size_t size_{0}; // Size of allocated memory - size_t head_{0}; // Write position - size_t tail_{0}; // Read position + uint8_t storage_[ESPHOME_TASK_LOG_BUFFER_SIZE]; // Embedded in Logger (no separate heap allocation) + size_t head_{0}; // Write position + size_t tail_{0}; // Read position SemaphoreHandle_t mutex_{nullptr}; // FreeRTOS mutex for thread safety volatile uint16_t message_count_{0}; // Fast check counter (dirty read OK) diff --git a/esphome/components/logger/task_log_buffer_zephyr.cpp b/esphome/components/logger/task_log_buffer_zephyr.cpp index 44d12d08a3..a994925a54 100644 --- a/esphome/components/logger/task_log_buffer_zephyr.cpp +++ b/esphome/components/logger/task_log_buffer_zephyr.cpp @@ -17,19 +17,16 @@ static inline uint32_t get_wlen(const mpsc_pbuf_generic *item) { return total_size_in_32bit_words(reinterpret_cast(item)->text_length); } -TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) { - // alignment to 4 bytes - total_buffer_size = (total_buffer_size + 3) / sizeof(uint32_t); - this->mpsc_config_.buf = new uint32_t[total_buffer_size]; - this->mpsc_config_.size = total_buffer_size; +TaskLogBuffer::TaskLogBuffer() { + // Storage is a member array (embedded in Logger), no heap allocation needed + this->mpsc_config_.buf = this->buf_storage_; + this->mpsc_config_.size = BUF_WORD_COUNT; this->mpsc_config_.flags = MPSC_PBUF_MODE_OVERWRITE; - this->mpsc_config_.get_wlen = get_wlen, + this->mpsc_config_.get_wlen = get_wlen; mpsc_pbuf_init(&this->log_buffer_, &this->mpsc_config_); } -TaskLogBuffer::~TaskLogBuffer() { delete[] this->mpsc_config_.buf; } - bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, const char *format, va_list args) { // First, calculate the exact length needed using a null buffer (no actual writing) diff --git a/esphome/components/logger/task_log_buffer_zephyr.h b/esphome/components/logger/task_log_buffer_zephyr.h index cc2ed1f687..4f192366ad 100644 --- a/esphome/components/logger/task_log_buffer_zephyr.h +++ b/esphome/components/logger/task_log_buffer_zephyr.h @@ -33,15 +33,14 @@ class TaskLogBuffer { // Methods for accessing message contents inline char *text_data() { return reinterpret_cast(this) + sizeof(LogMessage); } }; - // Constructor that takes a total buffer size - explicit TaskLogBuffer(size_t total_buffer_size); - ~TaskLogBuffer(); + TaskLogBuffer(); + ~TaskLogBuffer() = default; // Check if there are messages ready to be processed using an atomic counter for performance inline bool HOT has_messages() { return mpsc_pbuf_is_pending(&this->log_buffer_); } // Get the total buffer size in bytes - inline size_t size() const { return this->mpsc_config_.size * sizeof(uint32_t); } + static constexpr size_t size() { return BUF_WORD_COUNT * sizeof(uint32_t); } // NOT thread-safe - borrow a message from the ring buffer, only call from main loop bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length); @@ -54,6 +53,9 @@ class TaskLogBuffer { const char *format, va_list args); protected: + // Round up byte size to 32-bit word count for mpsc_pbuf alignment requirement + static constexpr size_t BUF_WORD_COUNT = (ESPHOME_TASK_LOG_BUFFER_SIZE + 3) / sizeof(uint32_t); + uint32_t buf_storage_[BUF_WORD_COUNT]; // Embedded in Logger (no separate heap allocation) mpsc_pbuf_buffer_config mpsc_config_{}; mpsc_pbuf_buffer log_buffer_{}; const mpsc_pbuf_generic *current_token_{}; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 676ad3024f..b5612a1d3f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -200,6 +200,7 @@ #define USE_ESP32_CRASH_HANDLER #define USE_MQTT_IDF_ENQUEUE #define USE_ESPHOME_TASK_LOG_BUFFER +#define ESPHOME_TASK_LOG_BUFFER_SIZE 768 #define USE_OTA_ROLLBACK #define USE_ESP32_MIN_CHIP_REVISION_SET #define USE_ESP32_SRAM1_AS_IRAM @@ -373,18 +374,23 @@ #define USE_WEBSERVER #define USE_WEBSERVER_AUTH #define USE_WEBSERVER_PORT 80 // NOLINT +#define USE_ESPHOME_TASK_LOG_BUFFER +#define ESPHOME_TASK_LOG_BUFFER_SIZE 768 #endif #ifdef USE_HOST #define USE_HTTP_REQUEST_RESPONSE #define USE_SOCKET_IMPL_BSD_SOCKETS #define USE_SOCKET_SELECT_SUPPORT +#define USE_ESPHOME_TASK_LOG_BUFFER +#define ESPHOME_TASK_LOG_BUFFER_SIZE 64 #endif #ifdef USE_NRF52 #define ESPHOME_BLE_NUS_TX_RING_BUFFER_SIZE 512 #define ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE 512 #define USE_ESPHOME_TASK_LOG_BUFFER +#define ESPHOME_TASK_LOG_BUFFER_SIZE 768 #define USE_LOGGER_EARLY_MESSAGE #define USE_LOGGER_UART_SELECTION_USB_CDC #define USE_LOGGER_USB_CDC diff --git a/tests/benchmarks/components/main.cpp b/tests/benchmarks/components/main.cpp index 901dc44c07..9bc0c31a15 100644 --- a/tests/benchmarks/components/main.cpp +++ b/tests/benchmarks/components/main.cpp @@ -26,7 +26,7 @@ void setup() { // Log functions call global_logger->log_vprintf_() without a null check, // so we must set up a Logger before any test that triggers logging. - static esphome::logger::Logger test_logger(0, 64); + static esphome::logger::Logger test_logger(0); test_logger.set_log_level(ESPHOME_LOG_LEVEL); test_logger.pre_setup(); diff --git a/tests/components/main.cpp b/tests/components/main.cpp index 622b1f107b..373fde7151 100644 --- a/tests/components/main.cpp +++ b/tests/components/main.cpp @@ -22,7 +22,7 @@ void original_setup() { void setup() { // Log functions call global_logger->log_vprintf_() without a null check, // so we must set up a Logger before any test that triggers logging. - static esphome::logger::Logger test_logger(0, 64); + static esphome::logger::Logger test_logger(0); test_logger.set_log_level(ESPHOME_LOG_LEVEL); test_logger.pre_setup(); diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 329286e2fa..6fa0c08aa3 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -15,7 +15,7 @@ void setup() { static char name[] = "livingroom"; static char friendly_name[] = "LivingRoom"; App.pre_setup(name, sizeof(name) - 1, friendly_name, sizeof(friendly_name) - 1); - auto *log = new logger::Logger(115200, 512); // NOLINT + auto *log = new logger::Logger(115200); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); App.register_component_(log); From af5b98c635f3209cfcc940a8341545eaaf74749a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Mar 2026 15:07:28 -1000 Subject: [PATCH 029/115] [time] Remove dummy placeholder values for recalc_timestamp_utc() (#15129) --- esphome/components/bm8563/bm8563.cpp | 1 - esphome/components/ds1307/ds1307.cpp | 3 --- esphome/components/gps/time/gps_time.cpp | 4 ---- esphome/components/pcf85063/pcf85063.cpp | 3 --- esphome/components/pcf8563/pcf8563.cpp | 3 --- esphome/components/rx8130/rx8130.cpp | 3 --- 6 files changed, 17 deletions(-) diff --git a/esphome/components/bm8563/bm8563.cpp b/esphome/components/bm8563/bm8563.cpp index 07831485c1..269acfea44 100644 --- a/esphome/components/bm8563/bm8563.cpp +++ b/esphome/components/bm8563/bm8563.cpp @@ -56,7 +56,6 @@ void BM8563::read_time() { ESPTime rtc_time; this->get_time_(rtc_time); this->get_date_(rtc_time); - rtc_time.day_of_year = 1; // unused by recalc_timestamp_utc, but needs to be valid ESP_LOGD(TAG, "Read time: %i-%i-%i %i, %i:%i:%i", rtc_time.year, rtc_time.month, rtc_time.day_of_month, rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second); diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp index 5c0e98290b..8fff4213b4 100644 --- a/esphome/components/ds1307/ds1307.cpp +++ b/esphome/components/ds1307/ds1307.cpp @@ -40,11 +40,8 @@ void DS1307Component::read_time() { .hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10), .day_of_week = uint8_t(ds1307_.reg.weekday), .day_of_month = uint8_t(ds1307_.reg.day + 10u * ds1307_.reg.day_10), - .day_of_year = 1, // ignored by recalc_timestamp_utc(false) .month = uint8_t(ds1307_.reg.month + 10u * ds1307_.reg.month_10), .year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000), - .is_dst = false, // not used - .timestamp = 0 // overwritten by recalc_timestamp_utc(false) }; rtc_time.recalc_timestamp_utc(false); if (!rtc_time.is_valid()) { diff --git a/esphome/components/gps/time/gps_time.cpp b/esphome/components/gps/time/gps_time.cpp index cff8c1fb07..fb662a3d60 100644 --- a/esphome/components/gps/time/gps_time.cpp +++ b/esphome/components/gps/time/gps_time.cpp @@ -16,10 +16,6 @@ void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) { val.year = tiny_gps.date.year(); val.month = tiny_gps.date.month(); val.day_of_month = tiny_gps.date.day(); - // Set these to valid value for recalc_timestamp_utc - it's not used for calculation - val.day_of_week = 1; - val.day_of_year = 1; - val.hour = tiny_gps.time.hour(); val.minute = tiny_gps.time.minute(); val.second = tiny_gps.time.second(); diff --git a/esphome/components/pcf85063/pcf85063.cpp b/esphome/components/pcf85063/pcf85063.cpp index 03ed78654f..1cf28a4955 100644 --- a/esphome/components/pcf85063/pcf85063.cpp +++ b/esphome/components/pcf85063/pcf85063.cpp @@ -40,11 +40,8 @@ void PCF85063Component::read_time() { .hour = uint8_t(pcf85063_.reg.hour + 10u * pcf85063_.reg.hour_10), .day_of_week = uint8_t(pcf85063_.reg.weekday), .day_of_month = uint8_t(pcf85063_.reg.day + 10u * pcf85063_.reg.day_10), - .day_of_year = 1, // ignored by recalc_timestamp_utc(false) .month = uint8_t(pcf85063_.reg.month + 10u * pcf85063_.reg.month_10), .year = uint16_t(pcf85063_.reg.year + 10u * pcf85063_.reg.year_10 + 2000), - .is_dst = false, // not used - .timestamp = 0, // overwritten by recalc_timestamp_utc(false) }; rtc_time.recalc_timestamp_utc(false); if (!rtc_time.is_valid()) { diff --git a/esphome/components/pcf8563/pcf8563.cpp b/esphome/components/pcf8563/pcf8563.cpp index dc68807aef..b748f0156a 100644 --- a/esphome/components/pcf8563/pcf8563.cpp +++ b/esphome/components/pcf8563/pcf8563.cpp @@ -40,11 +40,8 @@ void PCF8563Component::read_time() { .hour = uint8_t(pcf8563_.reg.hour + 10u * pcf8563_.reg.hour_10), .day_of_week = uint8_t(pcf8563_.reg.weekday), .day_of_month = uint8_t(pcf8563_.reg.day + 10u * pcf8563_.reg.day_10), - .day_of_year = 1, // ignored by recalc_timestamp_utc(false) .month = uint8_t(pcf8563_.reg.month + 10u * pcf8563_.reg.month_10), .year = uint16_t(pcf8563_.reg.year + 10u * pcf8563_.reg.year_10 + 2000), - .is_dst = false, // not used - .timestamp = 0, // overwritten by recalc_timestamp_utc(false) }; rtc_time.recalc_timestamp_utc(false); if (!rtc_time.is_valid()) { diff --git a/esphome/components/rx8130/rx8130.cpp b/esphome/components/rx8130/rx8130.cpp index 07ed7acc56..3b704d2551 100644 --- a/esphome/components/rx8130/rx8130.cpp +++ b/esphome/components/rx8130/rx8130.cpp @@ -77,11 +77,8 @@ void RX8130Component::read_time() { .hour = bcd2dec(date[2] & 0x3f), .day_of_week = static_cast((date[3] & 0x7f) ? __builtin_ctz(date[3] & 0x7f) + 1 : 1), .day_of_month = bcd2dec(date[4] & 0x3f), - .day_of_year = 1, // ignored by recalc_timestamp_utc(false) .month = bcd2dec(date[5] & 0x1f), .year = static_cast(bcd2dec(date[6]) + 2000), - .is_dst = false, // not used - .timestamp = 0 // overwritten by recalc_timestamp_utc(false) }; rtc_time.recalc_timestamp_utc(false); if (!rtc_time.is_valid()) { From 7a407595678d7b855fb79f8b2912fc13d1f1aaad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:55:12 +0000 Subject: [PATCH 030/115] Bump aioesphomeapi from 44.7.0 to 44.8.0 (#15159) 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 2e09e2ed99..9e75e6d039 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.7.0 +aioesphomeapi==44.8.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From c45c9da771c47dfbdfa966902a60540e778792c8 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:51:23 +1000 Subject: [PATCH 031/115] [lvgl] Various 9.5 fixes (#15157) --- esphome/components/lvgl/lvgl_esphome.cpp | 4 +- esphome/components/lvgl/lvgl_esphome.h | 2 +- esphome/components/lvgl/widgets/__init__.py | 2 +- esphome/components/lvgl/widgets/meter.py | 45 +++++++++++++++------ 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index b3cb4d56ad..d26bcdc714 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -673,14 +673,14 @@ void LvglComponent::static_flush_cb(lv_display_t *disp_drv, const lv_area_t *are * @param color_end The color to apply to the last tick * @param width */ -void lv_scale_draw_event_cb(lv_event_t *e, uint16_t range_start, uint16_t range_end, lv_color_t color_start, +void lv_scale_draw_event_cb(lv_event_t *e, int16_t range_start, int16_t range_end, lv_color_t color_start, lv_color_t color_end, int width, bool local) { auto *scale = static_cast(lv_event_get_target(e)); lv_draw_task_t *task = lv_event_get_draw_task(e); if (lv_draw_task_get_type(task) == LV_DRAW_TASK_TYPE_LINE) { auto *line_dsc = static_cast(lv_draw_task_get_draw_dsc(task)); - auto tick = line_dsc->base.id1; + int tick = line_dsc->base.id2; if (tick >= range_start && tick <= range_end) { unsigned range = range_end - range_start; if (local) { diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 66f823d549..7baeeb233b 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -52,7 +52,7 @@ extern std::string lv_event_code_name_for(lv_event_t *event); lv_obj_t *lv_container_create(lv_obj_t *parent); #ifdef USE_LVGL_SCALE -void lv_scale_draw_event_cb(lv_event_t *e, uint16_t range_start, uint16_t range_end, lv_color_t color_start, +void lv_scale_draw_event_cb(lv_event_t *e, int16_t range_start, int16_t range_end, lv_color_t color_start, lv_color_t color_end, int width, bool local); #endif #if LV_COLOR_DEPTH == 16 diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index a2a8cf2129..b383196963 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -158,7 +158,7 @@ class WidgetType: await self.on_create(var, config) w = Widget.create(wid, var, self, config) - if theme := theme_widget_map.get(self.w_type.name): + if theme := theme_widget_map.get(self.name): for part, states in theme.items(): part = "LV_PART_" + part.upper() for state, style in states.items(): diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index 6a7559c42c..d32efd145b 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -79,6 +79,7 @@ from ..types import ( from . import Widget, WidgetType, get_widgets, widget_to_code from .arc import CONF_ARC from .img import CONF_IMAGE +from .label import CONF_LABEL from .line import CONF_LINE CONF_ANGLE_RANGE = "angle_range" @@ -222,12 +223,31 @@ INDICATOR_SCHEMA = cv.Schema( } ) + +def _scale_validate(config): + if indicators := config.get(CONF_INDICATORS): + style_index = next( + ( + i + for i, indicator in enumerate(indicators) + if CONF_TICK_STYLE in indicator + ), + -1, + ) + if style_index >= 0 and CONF_TICKS not in config: + raise cv.Invalid( + "'tick_style' can't be applied if the enclosing scale has no 'ticks' configured", + path=[CONF_INDICATORS, style_index], + ) + return config + + SCALE_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(lv_scale_t), cv.Optional(CONF_TICKS): cv.Schema( { - cv.Optional(CONF_COUNT, default=12): cv.positive_int, + cv.Optional(CONF_COUNT, default=12): cv.int_range(min=2), cv.Optional(CONF_WIDTH, default=2): cv.positive_int, cv.Optional(CONF_LENGTH, default=10): size, cv.Optional(CONF_RADIAL_OFFSET, default=0): size, @@ -251,7 +271,7 @@ SCALE_SCHEMA = cv.Schema( cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA), cv.Optional(CONF_DRAW_TICKS_ON_TOP, default=True): bool, } -) +).add_extra(_scale_validate) METER_SCHEMA = { cv.Optional(CONF_PIVOT): STATE_SCHEMA, @@ -259,17 +279,14 @@ METER_SCHEMA = { cv.Optional(CONF_SCALES): cv.ensure_list(SCALE_SCHEMA), } +# Only handling light style at the moment LIGHT_STYLE = LVStyle( "lv_meter_light", { "bg_opa": 1.0, - "bg_color": 0xEEEEEE, - "line_width": 1, - "line_color": 0xEEEEEE, - "arc_width": 2, - "arc_color": 0xEEEEEE, + "bg_color": 0xFFFFFF, "pad_all": 10, - "border_width": 2, + "border_width": 3, "border_color": 0xEEEEEE, "radius": "LV_RADIUS_CIRCLE", }, @@ -329,7 +346,7 @@ class MeterType(WidgetType): ) def get_uses(self): - return CONF_SCALE, CONF_LINE, CONF_IMAGE + return CONF_SCALE, CONF_LINE, CONF_IMAGE, CONF_LABEL def validate(self, value): return cv.has_at_most_one_key(CONF_INDICATOR, CONF_PIVOT)(value) @@ -478,6 +495,8 @@ class MeterType(WidgetType): await iw.set_property(CONF_SRC, await lv_image.process(src)) await set_indicator_values(iw, v) + # Hide the scale line + lv.obj_set_style_arc_opa(scale_var, LV_OPA.TRANSP, LV_PART.MAIN) if ticks := scale_conf.get(CONF_TICKS): # Set total tick count lv.scale_set_total_tick_count(scale_var, ticks[CONF_COUNT]) @@ -503,8 +522,6 @@ class MeterType(WidgetType): LV_PART.ITEMS, ) - # Hide the scale line - lv.obj_set_style_arc_opa(scale_var, LV_OPA.TRANSP, LV_PART.MAIN) if CONF_MAJOR in ticks: major = ticks[CONF_MAJOR] # Set major tick frequency @@ -547,7 +564,11 @@ class MeterType(WidgetType): else: lv.scale_set_major_tick_every(scale_var, 0) else: - lv.scale_set_total_tick_count(scale_var, 0) + # Must have at least 2 ticks otherwise the scale isn't even drawn + lv.scale_set_total_tick_count(scale_var, 2) + # Hide the ticks by making them 0 width + lv_obj.set_style_line_width(scale_var, 0, LV_PART.ITEMS) + lv.scale_set_major_tick_every(scale_var, 0) # Add a pivot # Get the default style From f5bbff0b05a677e7aa0d28218291a8b868c0fb39 Mon Sep 17 00:00:00 2001 From: Piotr Szulc Date: Wed, 25 Mar 2026 12:40:39 +0100 Subject: [PATCH 032/115] [core] Add CONF_LIBRETINY constant to const.py (#15141) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/const/__init__.py | 1 + esphome/components/libretiny/const.py | 1 - esphome/components/libretiny/text_sensor.py | 3 ++- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 2a972a2939..1fbf88c276 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -13,6 +13,7 @@ CONF_DATA_BITS = "data_bits" CONF_DRAW_ROUNDING = "draw_rounding" CONF_ENABLED = "enabled" CONF_IGNORE_NOT_FOUND = "ignore_not_found" +CONF_LIBRETINY = "libretiny" CONF_ON_PACKET = "on_packet" CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" diff --git a/esphome/components/libretiny/const.py b/esphome/components/libretiny/const.py index bc4ca99ab4..332be0de1d 100644 --- a/esphome/components/libretiny/const.py +++ b/esphome/components/libretiny/const.py @@ -14,7 +14,6 @@ class LibreTinyComponent: supports_atomics: bool = False # True for Cortex-M4(F) with LDREX/STREX -CONF_LIBRETINY = "libretiny" CONF_LOGLEVEL = "loglevel" CONF_SDK_SILENT = "sdk_silent" CONF_GPIO_RECOVER = "gpio_recover" diff --git a/esphome/components/libretiny/text_sensor.py b/esphome/components/libretiny/text_sensor.py index fa33fb6c02..c1012774c8 100644 --- a/esphome/components/libretiny/text_sensor.py +++ b/esphome/components/libretiny/text_sensor.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import text_sensor +from esphome.components.const import CONF_LIBRETINY import esphome.config_validation as cv from esphome.const import ( CONF_VERSION, @@ -7,7 +8,7 @@ from esphome.const import ( ICON_CELLPHONE_ARROW_DOWN, ) -from .const import CONF_LIBRETINY, LTComponent +from .const import LTComponent DEPENDENCIES = ["libretiny"] From 2355fcb44e0804b68d2892fbe49f574baf9a7a6a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 25 Mar 2026 23:51:51 +1000 Subject: [PATCH 033/115] [lvgl] Update function and type names (#15109) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/lvgl/gradient.py | 2 +- esphome/components/lvgl/hello_world.yaml | 6 ++-- esphome/components/lvgl/lvgl_esphome.cpp | 10 +++---- esphome/components/lvgl/lvgl_esphome.h | 8 +++--- esphome/components/lvgl/schemas.py | 12 ++++++-- esphome/components/lvgl/trigger.py | 2 +- esphome/components/lvgl/types.py | 3 +- esphome/components/lvgl/widgets/canvas.py | 4 ++- esphome/components/lvgl/widgets/img.py | 4 +-- esphome/components/lvgl/widgets/meter.py | 4 +-- esphome/components/lvgl/widgets/tileview.py | 8 +++--- tests/components/lvgl/lvgl-package.yaml | 31 ++++++++------------- 12 files changed, 47 insertions(+), 47 deletions(-) diff --git a/esphome/components/lvgl/gradient.py b/esphome/components/lvgl/gradient.py index f3ded6a518..c4a3c8f2cb 100644 --- a/esphome/components/lvgl/gradient.py +++ b/esphome/components/lvgl/gradient.py @@ -31,7 +31,7 @@ GRADIENT_SCHEMA = cv.ensure_list( cv.Required(CONF_DIRECTION): cv.one_of( "HOR", "HORIZONTAL", "VER", "VERTICAL", upper=True ), - cv.Optional(CONF_DITHER, default="NONE"): LV_DITHER.one_of, + cv.Optional(CONF_DITHER): LV_DITHER.one_of, cv.Required(CONF_STOPS): cv.All( [ cv.Schema( diff --git a/esphome/components/lvgl/hello_world.yaml b/esphome/components/lvgl/hello_world.yaml index 359e73cd52..4af179a589 100644 --- a/esphome/components/lvgl/hello_world.yaml +++ b/esphome/components/lvgl/hello_world.yaml @@ -43,14 +43,14 @@ on_boot: lvgl.widget.refresh: hello_world_title_ hidden: !lambda |- - return lv_obj_get_width(lv_scr_act()) < 400; + return lv_obj_get_width(lv_screen_active()) < 400; - checkbox: text: Checkbox id: hello_world_checkbox_ on_boot: lvgl.widget.refresh: hello_world_checkbox_ hidden: !lambda |- - return lv_obj_get_width(lv_scr_act()) < 240; + return lv_obj_get_width(lv_screen_active()) < 240; on_click: lvgl.label.update: id: hello_world_label_ @@ -94,7 +94,7 @@ outline_width: 0 border_width: 0 hidden: !lambda |- - return lv_obj_get_width(lv_scr_act()) < 300 && lv_obj_get_height(lv_scr_act()) < 400; + return lv_obj_get_width(lv_screen_active()) < 300 && lv_obj_get_height(lv_screen_active()) < 400; widgets: - label: text_font: montserrat_14 diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index d26bcdc714..bf86a4e9ee 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -172,18 +172,18 @@ void LvglComponent::add_page(LvPageType *page) { page->setup(this->pages_.size() - 1); } -void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) { +void LvglComponent::show_page(size_t index, lv_screen_load_anim_t anim, uint32_t time) { if (index >= this->pages_.size()) return; this->current_page_ = index; if (anim == LV_SCREEN_LOAD_ANIM_NONE) { - lv_scr_load(this->pages_[this->current_page_]->obj); + lv_screen_load(this->pages_[this->current_page_]->obj); } else { - lv_scr_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false); + lv_screen_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false); } } -void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { +void LvglComponent::show_next_page(lv_screen_load_anim_t anim, uint32_t time) { if (this->pages_.empty() || (this->current_page_ == this->pages_.size() - 1 && !this->page_wrap_)) return; size_t start = this->current_page_; @@ -195,7 +195,7 @@ void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { this->show_page(this->current_page_, anim, time); } -void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) { +void LvglComponent::show_prev_page(lv_screen_load_anim_t anim, uint32_t time) { if (this->pages_.empty() || (this->current_page_ == 0 && !this->page_wrap_)) return; size_t start = this->current_page_; diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 7baeeb233b..8de82d50c0 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -163,7 +163,7 @@ class LvglComponent : public PollingComponent { static void render_end_cb(lv_event_t *event); static void render_start_cb(lv_event_t *event); void dump_config() override; - lv_disp_t *get_disp() { return this->disp_; } + lv_display_t *get_disp() { return this->disp_; } lv_obj_t *get_screen_active() { return lv_display_get_screen_active(this->disp_); } // Pause or resume the display. // @param paused If true, pause the display. If false, resume the display. @@ -189,9 +189,9 @@ class LvglComponent : public PollingComponent { lv_event_code_t event3); void add_page(LvPageType *page); - void show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time); - void show_next_page(lv_scr_load_anim_t anim, uint32_t time); - void show_prev_page(lv_scr_load_anim_t anim, uint32_t time); + void show_page(size_t index, lv_screen_load_anim_t anim, uint32_t time); + void show_next_page(lv_screen_load_anim_t anim, uint32_t time); + void show_prev_page(lv_screen_load_anim_t anim, uint32_t time); void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; } void set_big_endian(bool big_endian) { this->big_endian_ = big_endian; } size_t get_current_page() const; diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 4e2bfeae85..bcbb193ce3 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -250,9 +250,17 @@ STYLE_REMAP = { } -def remap_property(prop): +def remap_property(prop, record=True): + """ + Remap an old style property to new style property. + Optionally record the use of the deprecated property. + :param prop: Name of the style property to remap. + :param record: Whether to record the use of the deprecated property. + :return: The remapped property name, or ``prop`` if no remapping exists. + """ if prop in STYLE_REMAP: - get_remapped_uses().add(prop) + if record: + get_remapped_uses().add(prop) return STYLE_REMAP[prop] return prop diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py index c5ad4d402e..077ff06bb7 100644 --- a/esphome/components/lvgl/trigger.py +++ b/esphome/components/lvgl/trigger.py @@ -72,7 +72,7 @@ async def generate_triggers(): dir = DIRECTIONS.mapper(dir) w.clear_flag("LV_OBJ_FLAG_SCROLLABLE") selected = literal( - f"lv_indev_get_gesture_dir(lv_indev_get_act()) == {dir}" + f"lv_indev_get_gesture_dir(lv_indev_active()) == {dir}" ) await add_trigger( conf, w, literal("LV_EVENT_GESTURE"), is_selected=selected diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 03739f3ff1..8343a542a9 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -59,7 +59,6 @@ lv_style_t = cg.global_ns.struct("lv_style_t") lv_pseudo_button_t = lvgl_ns.class_("LvPseudoButton") lv_obj_base_t = cg.global_ns.class_("lv_obj_t", lv_pseudo_button_t) lv_obj_t_ptr = lv_obj_base_t.operator("ptr") -lv_disp_t = cg.global_ns.struct("lv_disp_t") lv_color_t = cg.global_ns.struct("lv_color_t") lv_opa_t = cg.global_ns.struct("lv_opa_t") lv_group_t = cg.global_ns.struct("lv_group_t") @@ -67,7 +66,7 @@ LVTouchListener = lvgl_ns.class_("LVTouchListener") LVEncoderListener = lvgl_ns.class_("LVEncoderListener") lv_obj_t = LvType("lv_obj_t") lv_page_t = LvType("LvPageType", parents=(LvCompound,)) -lv_img_t = LvType("lv_img_t") +lv_image_t = LvType("lv_image_t") lv_gradient_t = LvType("lv_grad_dsc_t") lv_event_t = LvType("lv_event_t") diff --git a/esphome/components/lvgl/widgets/canvas.py b/esphome/components/lvgl/widgets/canvas.py index c670e3732c..0e40d0dfbe 100644 --- a/esphome/components/lvgl/widgets/canvas.py +++ b/esphome/components/lvgl/widgets/canvas.py @@ -369,7 +369,9 @@ def _scale_map(config): def _get_prop_validator(prop): - return STYLE_PROPS.get(f"transform_{remap_property(prop)}") or STYLE_PROPS.get(prop) + return STYLE_PROPS.get( + f"transform_{remap_property(prop, False)}" + ) or STYLE_PROPS.get(prop) def _prop_validator(prop): diff --git a/esphome/components/lvgl/widgets/img.py b/esphome/components/lvgl/widgets/img.py index ed6fd30c09..8a046fea33 100644 --- a/esphome/components/lvgl/widgets/img.py +++ b/esphome/components/lvgl/widgets/img.py @@ -17,7 +17,7 @@ from ..defines import ( CONF_ZOOM, ) from ..lv_validation import lv_angle, lv_bool, lv_image, scale, size -from ..types import lv_img_t +from ..types import lv_image_t from . import Widget, WidgetType from .label import CONF_LABEL @@ -55,7 +55,7 @@ class ImgType(WidgetType): def __init__(self): super().__init__( CONF_IMAGE, - lv_img_t, + lv_image_t, (CONF_MAIN,), IMG_SCHEMA, IMG_MODIFY_SCHEMA, diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index d32efd145b..494f811a8e 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -73,7 +73,7 @@ from ..types import ( LvType, ObjUpdateAction, lv_event_t, - lv_img_t, + lv_image_t, lv_obj_t, ) from . import Widget, WidgetType, get_widgets, widget_to_code @@ -205,7 +205,7 @@ INDICATOR_SCHEMA = cv.Schema( INDICATOR_IMG_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(lv_meter_indicator_image_t), - cv.GenerateID(CONF_IMAGE_ID): cv.declare_id(lv_img_t), + cv.GenerateID(CONF_IMAGE_ID): cv.declare_id(lv_image_t), } ), requires_component("image"), diff --git a/esphome/components/lvgl/widgets/tileview.py b/esphome/components/lvgl/widgets/tileview.py index dadaef7d07..8e9d95f349 100644 --- a/esphome/components/lvgl/widgets/tileview.py +++ b/esphome/components/lvgl/widgets/tileview.py @@ -29,7 +29,7 @@ lv_tile_t = LvType("lv_tileview_tile_t") lv_tileview_t = LvType( "lv_tileview_t", largs=[(lv_obj_t_ptr, "tile")], - lvalue=lambda w: w.get_property("tile_act"), + lvalue=lambda w: w.get_property("tile_active"), has_on_value=True, ) @@ -85,7 +85,7 @@ class TileviewType(WidgetType): await add_widgets(tile, tile_conf) if tiles: # Set the first tile as active - lv_obj.set_tile_id( + lv.tileview_set_tile_by_index( w.obj, tiles[0][CONF_COLUMN], tiles[0][CONF_ROW], literal("LV_ANIM_OFF") ) @@ -122,11 +122,11 @@ async def tileview_select(config, action_id, template_arg, args): async def do_select(w: Widget): if tile := config.get(CONF_TILE_ID): tile = await cg.get_variable(tile) - lv_obj.set_tile(w.obj, tile, literal(config[CONF_ANIMATED])) + lv.tileview_set_tile(w.obj, tile, literal(config[CONF_ANIMATED])) else: row = await lv_int.process(config[CONF_ROW]) column = await lv_int.process(config[CONF_COLUMN]) - lv_obj.set_tile_id( + lv.tileview_set_tile_by_index( widgets[0].obj, column, row, literal(config[CONF_ANIMATED]) ) lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 606f57d6a1..b168578a98 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -43,9 +43,6 @@ lvgl: start_value: 0 end_value: 180 bg_color: light_blue - disp_bg_color: color_id - disp_bg_image: cat_image - disp_bg_opa: cover bottom_layer: widgets: - obj: @@ -58,7 +55,6 @@ lvgl: gradients: - id: color_bar direction: hor - # dither: err_diff stops: - color: 0xFF0000 position: 0 @@ -143,12 +139,11 @@ lvgl: body: text: This is a sample messagebox bg_color: 0x808080 - button_style: - bg_color: 0xff00 - border_width: 4 buttons: - id: msgbox_button text: Button + bg_color: 0x00ff00 + border_width: 4 - id: msgbox_apply text: "Close" on_click: @@ -160,8 +155,8 @@ lvgl: bg_opa: !lambda return 0.5; - lvgl.image.update: id: lv_image - zoom: !lambda return 512; - angle: !lambda return 100; + scale: !lambda return 512; + rotation: !lambda return 100; pivot_x: !lambda return 20; pivot_y: !lambda return 20; offset_x: !lambda return 20; @@ -287,8 +282,8 @@ lvgl: then: - lvgl.animimg.stop: anim_img - lvgl.update: - disp_bg_color: 0xffff00 - disp_bg_image: none + bottom_layer: + bg_color: 0xffff00 - lvgl.widget.show: message_box - label: text: "Hello shiny day" @@ -361,8 +356,6 @@ lvgl: pad_right: 10px pad_top: 10px shadow_color: light_blue - shadow_ofs_x: 5 - shadow_ofs_y: 5 shadow_opa: cover shadow_spread: 5 shadow_width: 10 @@ -373,12 +366,10 @@ lvgl: text_letter_space: 4 text_line_space: 4 text_opa: cover - transform_angle: 180 transform_rotation: 90 transform_height: 100 transform_pivot_x: 50% transform_pivot_y: 50% - transform_zoom: 0.5 transform_scale: 2.0 transform_scale_x: 1.5 transform_scale_y: 0.8 @@ -470,11 +461,11 @@ lvgl: id: button_button width: 20% height: 10% - transform_angle: !lambda return(180*100); + transform_rotation: !lambda return(180*100); arc_width: !lambda return 4; border_width: !lambda return 6; - shadow_ofs_x: !lambda return 6; - shadow_ofs_y: !lambda return 6; + shadow_offset_x: !lambda return 6; + shadow_offset_y: !lambda return 6; shadow_spread: !lambda return 6; shadow_width: !lambda return 6; pressed: @@ -646,8 +637,8 @@ lvgl: border_opa: 80% shadow_color: black shadow_width: 10 - shadow_ofs_x: 5 - shadow_ofs_y: 5 + shadow_offset_x: 5 + shadow_offset_y: 5 shadow_spread: 4 shadow_opa: cover outline_color: red From 6c981e83db9186381742618e5c5bf17f9da689e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brandon=20der=20Bl=C3=A4tter?= Date: Wed, 25 Mar 2026 06:52:50 -0700 Subject: [PATCH 034/115] [hub75] Add SCAN_1_8_32PX_FULL wiring option (#15130) --- esphome/components/hub75/display.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/hub75/display.py b/esphome/components/hub75/display.py index ede5078c33..0d1b87941d 100644 --- a/esphome/components/hub75/display.py +++ b/esphome/components/hub75/display.py @@ -128,6 +128,7 @@ SCAN_WIRINGS = { "STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN, "SCAN_1_4_16PX_HIGH": Hub75ScanWiring.SCAN_1_4_16PX_HIGH, "SCAN_1_8_32PX_HIGH": Hub75ScanWiring.SCAN_1_8_32PX_HIGH, + "SCAN_1_8_32PX_FULL": Hub75ScanWiring.SCAN_1_8_32PX_FULL, "SCAN_1_8_40PX_HIGH": Hub75ScanWiring.SCAN_1_8_40PX_HIGH, "SCAN_1_8_64PX_HIGH": Hub75ScanWiring.SCAN_1_8_64PX_HIGH, } From b66ff374a2be7a6ebf4761f8124546c9b14684f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Metrich?= <45318189+FredM67@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:26:33 +0100 Subject: [PATCH 035/115] [esp32] Fix GPIO strapping pins and add USB-JTAG warnings (#15105) Co-authored-by: Claude Opus 4.6 Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/esp32/gpio_esp32_c3.py | 8 ++++++++ esphome/components/esp32/gpio_esp32_c6.py | 10 +++++++++- esphome/components/esp32/gpio_esp32_h2.py | 6 +++--- esphome/components/esp32/gpio_esp32_p4.py | 4 ++-- esphome/components/esp32/gpio_esp32_s3.py | 22 +++++++++++++++------- 5 files changed, 37 insertions(+), 13 deletions(-) diff --git a/esphome/components/esp32/gpio_esp32_c3.py b/esphome/components/esp32/gpio_esp32_c3.py index 93e0b97093..6eb002f3f0 100644 --- a/esphome/components/esp32/gpio_esp32_c3.py +++ b/esphome/components/esp32/gpio_esp32_c3.py @@ -14,6 +14,8 @@ _ESP32C3_SPI_PSRAM_PINS = { 17: "SPIQ", } +_ESP32C3_USB_JTAG_PINS = {18, 19} + _ESP32C3_STRAPPING_PINS = {2, 8, 9} _LOGGER = logging.getLogger(__name__) @@ -26,6 +28,12 @@ def esp32_c3_validate_gpio_pin(value: int) -> int: raise cv.Invalid( f"This pin cannot be used on ESP32-C3s and is already used by the SPI/PSRAM interface (function: {_ESP32C3_SPI_PSRAM_PINS[value]})" ) + if value in _ESP32C3_USB_JTAG_PINS: + _LOGGER.warning( + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", + value, + ) return value diff --git a/esphome/components/esp32/gpio_esp32_c6.py b/esphome/components/esp32/gpio_esp32_c6.py index cfd3bca833..993606d9de 100644 --- a/esphome/components/esp32/gpio_esp32_c6.py +++ b/esphome/components/esp32/gpio_esp32_c6.py @@ -18,7 +18,9 @@ _ESP32C6_SPI_PSRAM_PINS = { 30: "SPID", } -_ESP32C6_STRAPPING_PINS = {8, 9, 15} +_ESP32C6_USB_JTAG_PINS = {12, 13} + +_ESP32C6_STRAPPING_PINS = {4, 5, 8, 9, 15} _LOGGER = logging.getLogger(__name__) @@ -30,6 +32,12 @@ def esp32_c6_validate_gpio_pin(value: int) -> int: raise cv.Invalid( f"This pin cannot be used on ESP32-C6s and is already used by the SPI/PSRAM interface (function: {_ESP32C6_SPI_PSRAM_PINS[value]})" ) + if value in _ESP32C6_USB_JTAG_PINS: + _LOGGER.warning( + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", + value, + ) return value diff --git a/esphome/components/esp32/gpio_esp32_h2.py b/esphome/components/esp32/gpio_esp32_h2.py index 5e7a6158f9..9dd6537694 100644 --- a/esphome/components/esp32/gpio_esp32_h2.py +++ b/esphome/components/esp32/gpio_esp32_h2.py @@ -9,7 +9,7 @@ _ESP32H2_SPI_FLASH_PINS = {6, 7, 15, 16, 17, 18, 19, 20, 21} _ESP32H2_USB_JTAG_PINS = {26, 27} -_ESP32H2_STRAPPING_PINS = {2, 3, 8, 9, 25} +_ESP32H2_STRAPPING_PINS = {8, 9, 25} _LOGGER = logging.getLogger(__name__) @@ -26,8 +26,8 @@ def esp32_h2_validate_gpio_pin(value: int) -> int: ) if value in _ESP32H2_USB_JTAG_PINS: _LOGGER.warning( - "GPIO%d is reserved for the USB-Serial-JTAG interface.\n" - "To use this pin as GPIO, USB-Serial-JTAG will be disabled.", + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", value, ) diff --git a/esphome/components/esp32/gpio_esp32_p4.py b/esphome/components/esp32/gpio_esp32_p4.py index 865db92652..6e9227c501 100644 --- a/esphome/components/esp32/gpio_esp32_p4.py +++ b/esphome/components/esp32/gpio_esp32_p4.py @@ -20,8 +20,8 @@ def esp32_p4_validate_gpio_pin(value: int) -> int: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)") if value in _ESP32P4_USB_JTAG_PINS: _LOGGER.warning( - "GPIO%d is reserved for the USB-Serial-JTAG interface.\n" - "To use this pin as GPIO, USB-Serial-JTAG will be disabled.", + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", value, ) diff --git a/esphome/components/esp32/gpio_esp32_s3.py b/esphome/components/esp32/gpio_esp32_s3.py index cb0eb8178c..f528de4ccd 100644 --- a/esphome/components/esp32/gpio_esp32_s3.py +++ b/esphome/components/esp32/gpio_esp32_s3.py @@ -5,7 +5,7 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER from esphome.pins import check_strapping_pin -_ESP_32S3_SPI_PSRAM_PINS = { +_ESP32S3_SPI_PSRAM_PINS = { 26: "SPICS1", 27: "SPIHD", 28: "SPIWP", @@ -15,7 +15,7 @@ _ESP_32S3_SPI_PSRAM_PINS = { 32: "SPID", } -_ESP_32_ESP32_S3R8_PSRAM_PINS = { +_ESP32S3R8_PSRAM_PINS = { 33: "SPIIO4", 34: "SPIIO5", 35: "SPIIO6", @@ -23,7 +23,9 @@ _ESP_32_ESP32_S3R8_PSRAM_PINS = { 37: "SPIDQS", } -_ESP_32S3_STRAPPING_PINS = {0, 3, 45, 46} +_ESP32S3_USB_JTAG_PINS = {19, 20} + +_ESP32S3_STRAPPING_PINS = {0, 3, 45, 46} _LOGGER = logging.getLogger(__name__) @@ -32,11 +34,11 @@ def esp32_s3_validate_gpio_pin(value: int) -> int: if value < 0 or value > 48: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-48)") - if value in _ESP_32S3_SPI_PSRAM_PINS: + if value in _ESP32S3_SPI_PSRAM_PINS: raise cv.Invalid( - f"This pin cannot be used on ESP32-S3s and is already used by the SPI/PSRAM interface(function: {_ESP_32S3_SPI_PSRAM_PINS[value]})" + f"This pin cannot be used on ESP32-S3s and is already used by the SPI/PSRAM interface(function: {_ESP32S3_SPI_PSRAM_PINS[value]})" ) - if value in _ESP_32_ESP32_S3R8_PSRAM_PINS: + if value in _ESP32S3R8_PSRAM_PINS: _LOGGER.warning( "GPIO%d is used by the PSRAM interface on ESP32-S3R8 / ESP32-S3R8V and should be avoided on these models", value, @@ -46,6 +48,12 @@ def esp32_s3_validate_gpio_pin(value: int) -> int: # These pins are not exposed in GPIO mux (reason unknown) # but they're missing from IO_MUX list in datasheet raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32-S3s.") + if value in _ESP32S3_USB_JTAG_PINS: + _LOGGER.warning( + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", + value, + ) return value @@ -61,5 +69,5 @@ def esp32_s3_validate_supports(value: dict[str, Any]) -> dict[str, Any]: # All ESP32 pins support input mode pass - check_strapping_pin(value, _ESP_32S3_STRAPPING_PINS, _LOGGER) + check_strapping_pin(value, _ESP32S3_STRAPPING_PINS, _LOGGER) return value From e0d8000007beb22f66fb093399379de8f592075c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 26 Mar 2026 00:34:34 +1000 Subject: [PATCH 036/115] [ai] Add instructions regarding constructor parameters (#15091) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .ai/instructions.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.ai/instructions.md b/.ai/instructions.md index 240a47a52f..a7e08f9c4d 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -124,6 +124,28 @@ This document provides essential context for AI models interacting with this pro * **Indentation:** Use spaces (two per indentation level), not tabs * **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;` * **Line length:** Wrap lines at no more than 120 characters + * **Constructor parameters vs setters:** Component properties that are both **required** and **invariant** + (never change after construction) should be constructor parameters rather than set via setter methods. + This makes the dependency explicit and prevents use of the object in an incompletely-initialized state. + In code generation, when calling `cg.new_Pvariable()` or the relevant helper function to create the component, pass these as arguments. + ```cpp + // Good - required invariant dependency as constructor parameter + class SourceTextSensor : public text_sensor::TextSensor, public Component { + public: + explicit SourceTextSensor(text::Text *source) : source_(source) {} + protected: + text::Text *source_; + }; + ``` + ```cpp + // Bad - required invariant dependency as setter + class SourceTextSensor : public text_sensor::TextSensor, public Component { + public: + void set_source(text::Text *source) { this->source_ = source; } + protected: + text::Text *source_{nullptr}; + }; + ``` * **Component Structure:** * **Standard Files:** From 5d67868ac6d72159b40f080f556bc8534a1831e9 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:39:46 +0100 Subject: [PATCH 037/115] [nextion] Fix inline doc parameter types for page and touch callbacks (#14972) --- esphome/components/nextion/nextion.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 2842e57ce8..bb5998cf5d 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1160,13 +1160,13 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe /** Add a callback to be notified when the nextion changes pages. * - * @param callback The void(std::string) callback. + * @param callback The void(uint8_t) callback. */ template void add_new_page_callback(F &&callback) { this->page_callback_.add(std::forward(callback)); } /** Add a callback to be notified when Nextion has a touch event. * - * @param callback The void() callback. + * @param callback The void(uint8_t, uint8_t, bool) callback. */ template void add_touch_event_callback(F &&callback) { this->touch_callback_.add(std::forward(callback)); From a15389318f41373a4c4466c69056cff9f6466e4b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:57:33 -0400 Subject: [PATCH 038/115] [audio] Bump esp-audio-libs to 2.0.4 (#15164) --- esphome/components/audio/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 9cc80b9b33..acc3b5d351 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -204,7 +204,7 @@ async def to_code(config): add_idf_component( name="esphome/esp-audio-libs", - ref="2.0.3", + ref="2.0.4", ) data = _get_data() diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 4148147a3b..c44853969e 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -2,7 +2,7 @@ dependencies: bblanchon/arduinojson: version: "7.4.2" esphome/esp-audio-libs: - version: 2.0.3 + version: 2.0.4 esphome/micro-opus: version: 0.3.6 espressif/esp-dsp: From 010516aef2f1a155f569fe51a8437e6a48a2f876 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Mar 2026 07:33:17 -1000 Subject: [PATCH 039/115] [benchmark] Add sensor publish_state benchmarks (#15034) --- .../sensor/bench_sensor_publish.cpp | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/benchmarks/components/sensor/bench_sensor_publish.cpp diff --git a/tests/benchmarks/components/sensor/bench_sensor_publish.cpp b/tests/benchmarks/components/sensor/bench_sensor_publish.cpp new file mode 100644 index 0000000000..9639191a4d --- /dev/null +++ b/tests/benchmarks/components/sensor/bench_sensor_publish.cpp @@ -0,0 +1,79 @@ +#include + +#include "esphome/components/sensor/sensor.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +// Without this, the ~60ns per-iteration valgrind start/stop cost dominates +// sub-microsecond benchmarks. +static constexpr int kInnerIterations = 2000; + +// Test subclass to access protected configure_entity_() for benchmark setup. +class TestSensor : public sensor::Sensor { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } +}; + +// --- Sensor::publish_state() with no callbacks registered --- +// Measures baseline publish overhead: state assignment, logging, +// internal_send_state_to_frontend, ControllerRegistry notification. + +static void SensorPublish_NoCallbacks(benchmark::State &state) { + TestSensor sensor; + sensor.configure("test_sensor"); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(static_cast(i)); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SensorPublish_NoCallbacks); + +// --- Sensor::publish_state() with one state callback --- +// Measures callback dispatch overhead through LazyCallbackManager. + +static void SensorPublish_WithCallback(benchmark::State &state) { + TestSensor sensor; + sensor.configure("test_sensor"); + + float callback_value = 0.0f; + sensor.add_on_state_callback([&callback_value](float value) { callback_value = value; }); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(static_cast(i)); + } + benchmark::DoNotOptimize(callback_value); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SensorPublish_WithCallback); + +// --- Sensor::publish_state() with the same value every time --- +// Steady-state pattern: sensor reports an unchanged reading. +// Sensor doesn't dedup today, so this exercises the same code path +// as changing values, but tracks the common real-world pattern +// separately for regression detection. + +static void SensorPublish_SameValue(benchmark::State &state) { + TestSensor sensor; + sensor.configure("test_sensor"); + + // Warm up so has_state is already set + sensor.publish_state(23.5f); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(23.5f); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SensorPublish_SameValue); + +} // namespace esphome::benchmarks From a22d47c71924c2e6355d12cc014a134619e0e536 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Mar 2026 07:36:53 -1000 Subject: [PATCH 040/115] [api] Add --no-states flag to esphome logs command (#15160) --- esphome/__main__.py | 11 +++++++- esphome/components/api/client.py | 18 +++++++++--- tests/unit_tests/test_main.py | 47 ++++++++++++++++++++++++++++---- 3 files changed, 65 insertions(+), 11 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 4b0fc2cec7..87abd7f796 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1046,7 +1046,11 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int ): from esphome.components.api.client import run_logs - return run_logs(config, network_devices) + return run_logs( + config, + network_devices, + subscribe_states=not getattr(args, "no_states", False), + ) if port_type in (PortType.NETWORK, PortType.MQTT) and has_mqtt_logging(): from esphome import mqtt @@ -1664,6 +1668,11 @@ def parse_args(argv): help="Reset the device before starting serial logs.", default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"), ) + parser_logs.add_argument( + "--no-states", + action="store_true", + help="Do not show entity state changes in log output.", + ) parser_discover = subparsers.add_parser( "discover", diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 0e71ad8fcb..0c6c569c7d 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -32,7 +32,11 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: +async def async_run_logs( + config: dict[str, Any], + addresses: list[str], + subscribe_states: bool = True, +) -> None: """Run the logs command in the event loop.""" conf = config["api"] name = config["esphome"]["name"] @@ -89,14 +93,20 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: config, raw_line, backtrace_state=backtrace_state ) - stop = await async_run(cli, on_log, name=name) + stop = await async_run(cli, on_log, name=name, subscribe_states=subscribe_states) try: await asyncio.Event().wait() finally: await stop() -def run_logs(config: dict[str, Any], addresses: list[str]) -> None: +def run_logs( + config: dict[str, Any], + addresses: list[str], + subscribe_states: bool = True, +) -> None: """Run the logs command.""" with contextlib.suppress(KeyboardInterrupt): - asyncio.run(async_run_logs(config, addresses)) + asyncio.run( + async_run_logs(config, addresses, subscribe_states=subscribe_states) + ) diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 5e36c06bb3..115ce38c93 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -1762,7 +1762,34 @@ def test_show_logs_api( assert result == 0 mock_run_logs.assert_called_once_with( - CORE.config, ["192.168.1.100", "192.168.1.101"] + CORE.config, ["192.168.1.100", "192.168.1.101"], subscribe_states=True + ) + + +@patch("esphome.components.api.client.run_logs") +def test_show_logs_api_no_states( + mock_run_logs: Mock, +) -> None: + """Test show_logs with --no-states flag.""" + setup_core( + config={ + "logger": {}, + CONF_API: {}, + CONF_MDNS: {CONF_DISABLED: False}, + }, + platform=PLATFORM_ESP32, + ) + mock_run_logs.return_value = 0 + + args = MockArgs() + args.no_states = True + devices = ["192.168.1.100"] + + result = show_logs(CORE.config, args, devices) + + assert result == 0 + mock_run_logs.assert_called_once_with( + CORE.config, ["192.168.1.100"], subscribe_states=False ) @@ -1788,7 +1815,9 @@ def test_show_logs_api_with_fqdn_mdns_disabled( assert result == 0 # Should use the FQDN directly, not try MQTT lookup - mock_run_logs.assert_called_once_with(CORE.config, ["device.example.com"]) + mock_run_logs.assert_called_once_with( + CORE.config, ["device.example.com"], subscribe_states=True + ) @patch("esphome.components.api.client.run_logs") @@ -1816,7 +1845,9 @@ def test_show_logs_api_with_mqtt_fallback( assert result == 0 mock_mqtt_get_ip.assert_called_once_with(CORE.config, "user", "pass", "client") - mock_run_logs.assert_called_once_with(CORE.config, ["192.168.1.200"]) + mock_run_logs.assert_called_once_with( + CORE.config, ["192.168.1.200"], subscribe_states=True + ) @patch("esphome.mqtt.show_logs") @@ -2746,7 +2777,7 @@ def test_show_logs_api_static_ip_with_mqttip( # Verify run_logs was called with both IPs mock_run_logs.assert_called_once_with( - CORE.config, ["192.168.1.100", "192.168.2.50"] + CORE.config, ["192.168.1.100", "192.168.2.50"], subscribe_states=True ) @@ -2782,7 +2813,9 @@ def test_show_logs_api_multiple_mqttip_resolves_once( # Note: "MQTT" is a different magic string from "MQTTIP", but both trigger MQTT resolution # The _resolve_network_devices helper filters out both after first resolution mock_run_logs.assert_called_once_with( - CORE.config, ["192.168.2.50", "192.168.2.51", "192.168.1.100"] + CORE.config, + ["192.168.2.50", "192.168.2.51", "192.168.1.100"], + subscribe_states=True, ) @@ -2862,7 +2895,9 @@ def test_show_logs_api_mqtt_timeout_fallback( mock_mqtt_get_ip.assert_called_once_with(CORE.config, "user", "pass", "client") # Verify run_logs was called with only the static IP (MQTT failed) - mock_run_logs.assert_called_once_with(CORE.config, ["192.168.1.100"]) + mock_run_logs.assert_called_once_with( + CORE.config, ["192.168.1.100"], subscribe_states=True + ) def test_detect_external_components_no_external( From 65d0a91fcc0ad85de3d7a1792ad95a0e8c8977ec Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:01:52 +0100 Subject: [PATCH 041/115] [nextion] Add defined keys to `defines.h` (#14971) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/nextion/nextion.cpp | 8 ++--- esphome/core/defines.h | 7 ++++ tests/components/nextion/common.yaml | 47 ++++++++++++++++---------- 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 85da6af48a..ac17e14312 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -50,10 +50,10 @@ bool Nextion::check_connect_() { return true; #ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE - ESP_LOGW(TAG, "Connected (no handshake)"); // Log the connection status without handshake - this->is_connected_ = true; // Set the connection status to true - return true; // Return true indicating the connection is set -#else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE + ESP_LOGW(TAG, "Connected (no handshake)"); // Log the connection status without handshake + this->connection_state_.is_connected_ = true; // Set the connection status to true + return true; // Return true indicating the connection is set +#else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE if (this->comok_sent_ == 0) { this->reset_(false); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index b5612a1d3f..8cf331c4d6 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -115,6 +115,13 @@ #define SNTP_SERVER_COUNT 3 #define USE_MEDIA_PLAYER #define USE_MEDIA_SOURCE +#define USE_NEXTION_COMMAND_SPACING +#define USE_NEXTION_CONF_START_UP_PAGE +#define USE_NEXTION_CONFIG_DUMP_DEVICE_INFO +#define USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START +#define USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE +#define USE_NEXTION_MAX_COMMANDS_PER_LOOP +#define USE_NEXTION_MAX_QUEUE_SIZE #define USE_NEXTION_TFT_UPLOAD #define USE_NUMBER #define USE_OUTPUT diff --git a/tests/components/nextion/common.yaml b/tests/components/nextion/common.yaml index 4373fe5462..d9493db50c 100644 --- a/tests/components/nextion/common.yaml +++ b/tests/components/nextion/common.yaml @@ -273,26 +273,39 @@ text_sensor: display: - platform: nextion id: main_lcd + auto_wake_on_touch: true + brightness: 80% + command_spacing: 5ms + dump_device_info: true + exit_reparse_on_start: true + lambda: |- + ESP_LOGD("display","Display is being tested!"); max_commands_per_loop: 20 + max_queue_age: 5000ms # Remove queue items after 5s max_queue_size: 50 - update_interval: 5s - on_sleep: - then: - lambda: 'ESP_LOGD("display","Display went to sleep");' - on_wake: - then: - lambda: 'ESP_LOGD("display","Display woke up");' - on_setup: - then: - lambda: 'ESP_LOGD("display","Display setup completed");' - on_page: - then: - lambda: 'ESP_LOGD("display","Display shows new page %u", x);' on_buffer_overflow: then: logger.log: "Nextion reported a buffer overflow!" - - command_spacing: 5ms - dump_device_info: true - max_queue_age: 5000ms # Remove queue items after 5s + on_page: + then: + lambda: 'ESP_LOGD("display","Display shows new page %u", x);' + on_setup: + then: + lambda: 'ESP_LOGD("display","Display setup completed");' + on_sleep: + then: + lambda: 'ESP_LOGD("display","Display went to sleep");' + on_touch: + then: + lambda: |- + ESP_LOGD("display", + "Display was touched at page %u, component %u, touch event: %s", + page_id, component_id, touch_event ? "press" : "release"); + on_wake: + then: + lambda: 'ESP_LOGD("display","Display woke up");' + update_interval: 5s + start_up_page: 1 startup_override_ms: 10000ms # Wait 10s for display ready + touch_sleep_timeout: 3 + wake_up_page: 2 From c42c6745b960b989e3373a7bedc663b6360d57f5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:06:48 -0400 Subject: [PATCH 042/115] [mcp9600] Fix setup success check using OR instead of AND (#15165) --- esphome/components/mcp9600/mcp9600.cpp | 28 +++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/esphome/components/mcp9600/mcp9600.cpp b/esphome/components/mcp9600/mcp9600.cpp index ff411bef7a..0c5362b4ba 100644 --- a/esphome/components/mcp9600/mcp9600.cpp +++ b/esphome/components/mcp9600/mcp9600.cpp @@ -40,20 +40,20 @@ void MCP9600Component::setup() { } bool success = this->write_byte(MCP9600_REGISTER_STATUS, 0x00); - success |= this->write_byte(MCP9600_REGISTER_SENSOR_CONFIG, uint8_t(0x00 | thermocouple_type_ << 4)); - success |= this->write_byte(MCP9600_REGISTER_CONFIG, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT1_CONFIG, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT2_CONFIG, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT3_CONFIG, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT4_CONFIG, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT1_HYSTERESIS, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT2_HYSTERESIS, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT3_HYSTERESIS, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT4_HYSTERESIS, 0x00); - success |= this->write_byte_16(MCP9600_REGISTER_ALERT1_LIMIT, 0x0000); - success |= this->write_byte_16(MCP9600_REGISTER_ALERT2_LIMIT, 0x0000); - success |= this->write_byte_16(MCP9600_REGISTER_ALERT3_LIMIT, 0x0000); - success |= this->write_byte_16(MCP9600_REGISTER_ALERT4_LIMIT, 0x0000); + success &= this->write_byte(MCP9600_REGISTER_SENSOR_CONFIG, uint8_t(0x00 | thermocouple_type_ << 4)); + success &= this->write_byte(MCP9600_REGISTER_CONFIG, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT1_CONFIG, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT2_CONFIG, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT3_CONFIG, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT4_CONFIG, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT1_HYSTERESIS, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT2_HYSTERESIS, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT3_HYSTERESIS, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT4_HYSTERESIS, 0x00); + success &= this->write_byte_16(MCP9600_REGISTER_ALERT1_LIMIT, 0x0000); + success &= this->write_byte_16(MCP9600_REGISTER_ALERT2_LIMIT, 0x0000); + success &= this->write_byte_16(MCP9600_REGISTER_ALERT3_LIMIT, 0x0000); + success &= this->write_byte_16(MCP9600_REGISTER_ALERT4_LIMIT, 0x0000); if (!success) { this->error_code_ = FAILED_TO_UPDATE_CONFIGURATION; From 19615f2eaeeb37c5245a7de66abe0965b0e740f8 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:10:04 -0400 Subject: [PATCH 043/115] [bme68x_bsec2] Fix uninitialized bme68x_conf in measurement duration calculation (#15168) --- esphome/components/bme68x_bsec2/bme68x_bsec2.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index ed2ec80896..cf516f6ca6 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -276,8 +276,8 @@ void BME68xBSEC2Component::run_() { } if (this->bsec_settings_.trigger_measurement && this->bsec_settings_.op_mode != BME68X_SLEEP_MODE) { - uint32_t meas_dur = 0; - meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_); + bme68x_get_conf(&bme68x_conf, &this->bme68x_); + uint32_t meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_); ESP_LOGV(TAG, "Queueing read in %uus", meas_dur); this->trigger_time_ns_ = curr_time_ns; this->set_timeout("read", meas_dur / 1000, [this]() { this->read_(this->trigger_time_ns_); }); From f6c5767a8347ecccb446e4ff60627f18bd354d18 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:10:28 -0400 Subject: [PATCH 044/115] [inkplate] Use atomic GPIO write to prevent ISR race (#15166) --- esphome/components/inkplate/inkplate.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/inkplate/inkplate.cpp b/esphome/components/inkplate/inkplate.cpp index 326bdff774..3b4b1a63d5 100644 --- a/esphome/components/inkplate/inkplate.cpp +++ b/esphome/components/inkplate/inkplate.cpp @@ -229,7 +229,7 @@ void Inkplate::eink_off_() { this->oe_pin_->digital_write(false); this->gmod_pin_->digital_write(false); - GPIO.out &= ~(this->get_data_pin_mask_() | (1UL << this->cl_pin_->get_pin()) | (1UL << this->le_pin_->get_pin())); + GPIO.out_w1tc = this->get_data_pin_mask_() | (1UL << this->cl_pin_->get_pin()) | (1UL << this->le_pin_->get_pin()); this->ckv_pin_->digital_write(false); this->sph_pin_->digital_write(false); this->spv_pin_->digital_write(false); From d8fbce365aff406360a8ea64e980f25ff510f503 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 09:38:20 -1000 Subject: [PATCH 045/115] Bump requests from 2.32.5 to 2.33.0 (#15170) 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 9e75e6d039..ce735f398a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,7 @@ freetype-py==2.5.1 jinja2==3.1.6 bleak==2.1.1 smpclient==6.0.0 -requests==2.32.5 +requests==2.33.0 # esp-idf >= 5.0 requires this pyparsing >= 3.0 From ec60da893f6613edceccf6003bc23a3001ed50fe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Mar 2026 09:45:06 -1000 Subject: [PATCH 046/115] [core] Move state logging to client-side formatting, console to VERBOSE (#15155) --- .../alarm_control_panel.cpp | 2 +- .../binary_sensor/binary_sensor.cpp | 2 +- esphome/components/climate/climate.cpp | 48 +++++++++---------- esphome/components/cover/cover.cpp | 26 +++++----- esphome/components/datetime/date_entity.cpp | 10 ++-- .../components/datetime/datetime_entity.cpp | 16 +++---- esphome/components/datetime/time_entity.cpp | 10 ++-- esphome/components/event/event.cpp | 2 +- esphome/components/fan/fan.cpp | 22 ++++----- esphome/components/light/light_call.cpp | 24 +++++----- esphome/components/lock/lock.cpp | 6 +-- .../components/media_player/media_player.cpp | 10 ++-- esphome/components/number/number.cpp | 2 +- esphome/components/number/number_call.cpp | 8 ++-- esphome/components/select/select.cpp | 2 +- esphome/components/select/select_call.cpp | 4 +- esphome/components/sensor/sensor.cpp | 2 +- esphome/components/switch/switch.cpp | 2 +- esphome/components/text/text.cpp | 4 +- esphome/components/text/text_call.cpp | 4 +- .../components/text_sensor/text_sensor.cpp | 2 +- esphome/components/update/update_entity.cpp | 14 +++--- esphome/components/valve/valve.cpp | 22 ++++----- .../components/water_heater/water_heater.cpp | 26 +++++----- 24 files changed, 135 insertions(+), 135 deletions(-) diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index fb61776532..623241851a 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -31,7 +31,7 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { this->last_update_ = millis(); if (state != this->current_state_) { auto prev_state = this->current_state_; - ESP_LOGD(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(), + ESP_LOGV(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(), LOG_STR_ARG(alarm_control_panel_state_to_string(state)), LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); this->current_state_ = state; diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index c4d3a29a1e..8ace7eafd1 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -45,7 +45,7 @@ bool BinarySensor::set_new_state(const optional &new_state) { #if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_binary_sensor_update(this); #endif - ESP_LOGD(TAG, "'%s' >> %s", this->get_name().c_str(), ONOFFMAYBE(new_state)); + ESP_LOGV(TAG, "'%s' >> %s", this->get_name().c_str(), ONOFFMAYBE(new_state)); return true; } return false; diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 3f44b986dc..5cbe9a5daf 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -46,45 +46,45 @@ constexpr StringToUint8 CLIMATE_SWING_MODES_BY_STR[] = { void ClimateCall::perform() { this->parent_->control_callback_.call(*this); - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); this->validate_(); if (this->mode_.has_value()) { const LogString *mode_s = climate_mode_to_string(*this->mode_); - ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(mode_s)); + ESP_LOGV(TAG, " Mode: %s", LOG_STR_ARG(mode_s)); } if (this->custom_fan_mode_ != nullptr) { this->fan_mode_.reset(); - ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_); + ESP_LOGV(TAG, " Custom Fan: %s", this->custom_fan_mode_); } if (this->fan_mode_.has_value()) { this->custom_fan_mode_ = nullptr; const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_); - ESP_LOGD(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s)); + ESP_LOGV(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s)); } if (this->custom_preset_ != nullptr) { this->preset_.reset(); - ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); + ESP_LOGV(TAG, " Custom Preset: %s", this->custom_preset_); } if (this->preset_.has_value()) { this->custom_preset_ = nullptr; const LogString *preset_s = climate_preset_to_string(*this->preset_); - ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s)); + ESP_LOGV(TAG, " Preset: %s", LOG_STR_ARG(preset_s)); } if (this->swing_mode_.has_value()) { const LogString *swing_mode_s = climate_swing_mode_to_string(*this->swing_mode_); - ESP_LOGD(TAG, " Swing: %s", LOG_STR_ARG(swing_mode_s)); + ESP_LOGV(TAG, " Swing: %s", LOG_STR_ARG(swing_mode_s)); } if (this->target_temperature_.has_value()) { - ESP_LOGD(TAG, " Target Temperature: %.2f", *this->target_temperature_); + ESP_LOGV(TAG, " Target Temperature: %.2f", *this->target_temperature_); } if (this->target_temperature_low_.has_value()) { - ESP_LOGD(TAG, " Target Temperature Low: %.2f", *this->target_temperature_low_); + ESP_LOGV(TAG, " Target Temperature Low: %.2f", *this->target_temperature_low_); } if (this->target_temperature_high_.has_value()) { - ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_); + ESP_LOGV(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_); } if (this->target_humidity_.has_value()) { - ESP_LOGD(TAG, " Target Humidity: %.0f", *this->target_humidity_); + ESP_LOGV(TAG, " Target Humidity: %.0f", *this->target_humidity_); } this->parent_->control(*this); } @@ -435,43 +435,43 @@ void Climate::save_state_() { } void Climate::publish_state() { - ESP_LOGD(TAG, "'%s' >>", this->name_.c_str()); + ESP_LOGV(TAG, "'%s' >>", this->name_.c_str()); auto traits = this->get_traits(); - ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode))); + ESP_LOGV(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode))); if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { - ESP_LOGD(TAG, " Action: %s", LOG_STR_ARG(climate_action_to_string(this->action))); + ESP_LOGV(TAG, " Action: %s", LOG_STR_ARG(climate_action_to_string(this->action))); } if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) { - ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); + ESP_LOGV(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); } if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) { - ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode_); + ESP_LOGV(TAG, " Custom Fan Mode: %s", this->custom_fan_mode_); } if (traits.get_supports_presets() && this->preset.has_value()) { - ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); + ESP_LOGV(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); } if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) { - ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); + ESP_LOGV(TAG, " Custom Preset: %s", this->custom_preset_); } if (traits.get_supports_swing_modes()) { - ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); + ESP_LOGV(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { - ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature); + ESP_LOGV(TAG, " Current Temperature: %.2f°C", this->current_temperature); } if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { - ESP_LOGD(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low, + ESP_LOGV(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low, this->target_temperature_high); } else { - ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature); + ESP_LOGV(TAG, " Target Temperature: %.2f°C", this->target_temperature); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) { - ESP_LOGD(TAG, " Current Humidity: %.0f%%", this->current_humidity); + ESP_LOGV(TAG, " Current Humidity: %.0f%%", this->current_humidity); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { - ESP_LOGD(TAG, " Target Humidity: %.0f%%", this->target_humidity); + ESP_LOGV(TAG, " Target Humidity: %.0f%%", this->target_humidity); } // Send state to frontend diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index bb5965d861..e98a555fe5 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -68,24 +68,24 @@ CoverCall &CoverCall::set_tilt(float tilt) { return *this; } void CoverCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); auto traits = this->parent_->get_traits(); this->validate_(); if (this->stop_) { - ESP_LOGD(TAG, " Command: STOP"); + ESP_LOGV(TAG, " Command: STOP"); } if (this->position_.has_value()) { if (traits.get_supports_position()) { - ESP_LOGD(TAG, " Position: %.0f%%", *this->position_ * 100.0f); + ESP_LOGV(TAG, " Position: %.0f%%", *this->position_ * 100.0f); } else { - ESP_LOGD(TAG, " Command: %s", LOG_STR_ARG(cover_command_to_str(*this->position_))); + ESP_LOGV(TAG, " Command: %s", LOG_STR_ARG(cover_command_to_str(*this->position_))); } } if (this->tilt_.has_value()) { - ESP_LOGD(TAG, " Tilt: %.0f%%", *this->tilt_ * 100.0f); + ESP_LOGV(TAG, " Tilt: %.0f%%", *this->tilt_ * 100.0f); } if (this->toggle_.has_value()) { - ESP_LOGD(TAG, " Command: TOGGLE"); + ESP_LOGV(TAG, " Command: TOGGLE"); } this->parent_->control(*this); } @@ -143,23 +143,23 @@ void Cover::publish_state(bool save) { this->position = clamp(this->position, 0.0f, 1.0f); this->tilt = clamp(this->tilt, 0.0f, 1.0f); - ESP_LOGD(TAG, "'%s' >>", this->name_.c_str()); + ESP_LOGV(TAG, "'%s' >>", this->name_.c_str()); auto traits = this->get_traits(); if (traits.get_supports_position()) { - ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f); + ESP_LOGV(TAG, " Position: %.0f%%", this->position * 100.0f); } else { if (this->position == COVER_OPEN) { - ESP_LOGD(TAG, " State: OPEN"); + ESP_LOGV(TAG, " State: OPEN"); } else if (this->position == COVER_CLOSED) { - ESP_LOGD(TAG, " State: CLOSED"); + ESP_LOGV(TAG, " State: CLOSED"); } else { - ESP_LOGD(TAG, " State: UNKNOWN"); + ESP_LOGV(TAG, " State: UNKNOWN"); } } if (traits.get_supports_tilt()) { - ESP_LOGD(TAG, " Tilt: %.0f%%", this->tilt * 100.0f); + ESP_LOGV(TAG, " Tilt: %.0f%%", this->tilt * 100.0f); } - ESP_LOGD(TAG, " Current Operation: %s", LOG_STR_ARG(cover_operation_to_str(this->current_operation))); + ESP_LOGV(TAG, " Current Operation: %s", LOG_STR_ARG(cover_operation_to_str(this->current_operation))); this->state_callback_.call(); #if defined(USE_COVER) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp index 3ba488c0aa..997aec3f69 100644 --- a/esphome/components/datetime/date_entity.cpp +++ b/esphome/components/datetime/date_entity.cpp @@ -30,7 +30,7 @@ void DateEntity::publish_state() { return; } this->set_has_state(true); - ESP_LOGD(TAG, "'%s' >> %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); + ESP_LOGV(TAG, "'%s' >> %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); this->state_callback_.call(); #if defined(USE_DATETIME_DATE) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_date_update(this); @@ -83,16 +83,16 @@ void DateCall::validate_() { void DateCall::perform() { this->validate_(); - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); if (this->year_.has_value()) { - ESP_LOGD(TAG, " Year: %d", *this->year_); + ESP_LOGV(TAG, " Year: %d", *this->year_); } if (this->month_.has_value()) { - ESP_LOGD(TAG, " Month: %d", *this->month_); + ESP_LOGV(TAG, " Month: %d", *this->month_); } if (this->day_.has_value()) { - ESP_LOGD(TAG, " Day: %d", *this->day_); + ESP_LOGV(TAG, " Day: %d", *this->day_); } this->parent_->control(*this); } diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index fa50271f04..a8e00d6eb3 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -45,7 +45,7 @@ void DateTimeEntity::publish_state() { return; } this->set_has_state(true); - ESP_LOGD(TAG, "'%s' >> %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_, + ESP_LOGV(TAG, "'%s' >> %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_, this->day_, this->hour_, this->minute_, this->second_); this->state_callback_.call(); #if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY) @@ -127,25 +127,25 @@ void DateTimeCall::validate_() { void DateTimeCall::perform() { this->validate_(); - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); if (this->year_.has_value()) { - ESP_LOGD(TAG, " Year: %d", *this->year_); + ESP_LOGV(TAG, " Year: %d", *this->year_); } if (this->month_.has_value()) { - ESP_LOGD(TAG, " Month: %d", *this->month_); + ESP_LOGV(TAG, " Month: %d", *this->month_); } if (this->day_.has_value()) { - ESP_LOGD(TAG, " Day: %d", *this->day_); + ESP_LOGV(TAG, " Day: %d", *this->day_); } if (this->hour_.has_value()) { - ESP_LOGD(TAG, " Hour: %d", *this->hour_); + ESP_LOGV(TAG, " Hour: %d", *this->hour_); } if (this->minute_.has_value()) { - ESP_LOGD(TAG, " Minute: %d", *this->minute_); + ESP_LOGV(TAG, " Minute: %d", *this->minute_); } if (this->second_.has_value()) { - ESP_LOGD(TAG, " Second: %d", *this->second_); + ESP_LOGV(TAG, " Second: %d", *this->second_); } this->parent_->control(*this); } diff --git a/esphome/components/datetime/time_entity.cpp b/esphome/components/datetime/time_entity.cpp index 74e43fbbe7..1cc9eaf2fb 100644 --- a/esphome/components/datetime/time_entity.cpp +++ b/esphome/components/datetime/time_entity.cpp @@ -26,7 +26,7 @@ void TimeEntity::publish_state() { return; } this->set_has_state(true); - ESP_LOGD(TAG, "'%s' >> %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_); + ESP_LOGV(TAG, "'%s' >> %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_); this->state_callback_.call(); #if defined(USE_DATETIME_TIME) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_time_update(this); @@ -52,15 +52,15 @@ void TimeCall::validate_() { void TimeCall::perform() { this->validate_(); - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); if (this->hour_.has_value()) { - ESP_LOGD(TAG, " Hour: %d", *this->hour_); + ESP_LOGV(TAG, " Hour: %d", *this->hour_); } if (this->minute_.has_value()) { - ESP_LOGD(TAG, " Minute: %d", *this->minute_); + ESP_LOGV(TAG, " Minute: %d", *this->minute_); } if (this->second_.has_value()) { - ESP_LOGD(TAG, " Second: %d", *this->second_); + ESP_LOGV(TAG, " Second: %d", *this->second_); } this->parent_->control(*this); } diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index ec63fd9c3e..a5d64a2748 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -22,7 +22,7 @@ void Event::trigger(const std::string &event_type) { return; } this->last_event_type_ = found; - ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_); + ESP_LOGV(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_); this->event_callback_.call(StringRef(found)); #if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_event(this); diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 97336e17b5..dc7a75018c 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -44,22 +44,22 @@ FanCall &FanCall::set_preset_mode(const char *preset_mode, size_t len) { } void FanCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting:", this->parent_.get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting:", this->parent_.get_name().c_str()); this->validate_(); if (this->binary_state_.has_value()) { - ESP_LOGD(TAG, " State: %s", ONOFF(*this->binary_state_)); + ESP_LOGV(TAG, " State: %s", ONOFF(*this->binary_state_)); } if (this->oscillating_.has_value()) { - ESP_LOGD(TAG, " Oscillating: %s", YESNO(*this->oscillating_)); + ESP_LOGV(TAG, " Oscillating: %s", YESNO(*this->oscillating_)); } if (this->speed_.has_value()) { - ESP_LOGD(TAG, " Speed: %d", *this->speed_); + ESP_LOGV(TAG, " Speed: %d", *this->speed_); } if (this->direction_.has_value()) { - ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_))); + ESP_LOGV(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_))); } if (this->preset_mode_ != nullptr) { - ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_); + ESP_LOGV(TAG, " Preset Mode: %s", this->preset_mode_); } this->parent_.control(*this); } @@ -196,21 +196,21 @@ void Fan::apply_preset_mode_(const FanCall &call) { void Fan::publish_state() { auto traits = this->get_traits(); - ESP_LOGD(TAG, + ESP_LOGV(TAG, "'%s' >>\n" " State: %s", this->name_.c_str(), ONOFF(this->state)); if (traits.supports_speed()) { - ESP_LOGD(TAG, " Speed: %d", this->speed); + ESP_LOGV(TAG, " Speed: %d", this->speed); } if (traits.supports_oscillation()) { - ESP_LOGD(TAG, " Oscillating: %s", YESNO(this->oscillating)); + ESP_LOGV(TAG, " Oscillating: %s", YESNO(this->oscillating)); } if (traits.supports_direction()) { - ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); + ESP_LOGV(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); } if (this->preset_mode_ != nullptr) { - ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_); + ESP_LOGV(TAG, " Preset Mode: %s", this->preset_mode_); } this->state_callback_.call(); #if defined(USE_FAN) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 0b2d391fd6..41bd98de7b 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -62,9 +62,9 @@ static const LogString *color_mode_to_human(ColorMode color_mode) { } // Helper to log percentage values -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE static void log_percent(const LogString *param, float value) { - ESP_LOGD(TAG, " %s: %.0f%%", LOG_STR_ARG(param), value * 100.0f); + ESP_LOGV(TAG, " %s: %.0f%%", LOG_STR_ARG(param), value * 100.0f); } #else #define log_percent(param, value) @@ -76,20 +76,20 @@ void LightCall::perform() { const bool publish = this->get_publish_(); if (publish) { - ESP_LOGD(TAG, "'%s' Setting:", name); + ESP_LOGV(TAG, "'%s' Setting:", name); // Only print color mode when it's being changed ColorMode current_color_mode = this->parent_->remote_values.get_color_mode(); ColorMode target_color_mode = this->has_color_mode() ? this->color_mode_ : current_color_mode; if (target_color_mode != current_color_mode) { - ESP_LOGD(TAG, " Color mode: %s", LOG_STR_ARG(color_mode_to_human(v.get_color_mode()))); + ESP_LOGV(TAG, " Color mode: %s", LOG_STR_ARG(color_mode_to_human(v.get_color_mode()))); } // Only print state when it's being changed bool current_state = this->parent_->remote_values.is_on(); bool target_state = this->has_state() ? this->state_ : current_state; if (target_state != current_state) { - ESP_LOGD(TAG, " State: %s", ONOFF(v.is_on())); + ESP_LOGV(TAG, " State: %s", ONOFF(v.is_on())); } if (this->has_brightness()) { @@ -100,7 +100,7 @@ void LightCall::perform() { log_percent(LOG_STR("Color brightness"), v.get_color_brightness()); } if (this->has_red() || this->has_green() || this->has_blue()) { - ESP_LOGD(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, + ESP_LOGV(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, v.get_blue() * 100.0f); } @@ -108,11 +108,11 @@ void LightCall::perform() { log_percent(LOG_STR("White"), v.get_white()); } if (this->has_color_temperature()) { - ESP_LOGD(TAG, " Color temperature: %.1f mireds", v.get_color_temperature()); + ESP_LOGV(TAG, " Color temperature: %.1f mireds", v.get_color_temperature()); } if (this->has_cold_white() || this->has_warm_white()) { - ESP_LOGD(TAG, " Cold white: %.0f%%, warm white: %.0f%%", v.get_cold_white() * 100.0f, + ESP_LOGV(TAG, " Cold white: %.0f%%, warm white: %.0f%%", v.get_cold_white() * 100.0f, v.get_warm_white() * 100.0f); } } @@ -120,20 +120,20 @@ void LightCall::perform() { if (this->has_flash_()) { // FLASH if (publish) { - ESP_LOGD(TAG, " Flash length: %.1fs", this->flash_length_ / 1e3f); + ESP_LOGV(TAG, " Flash length: %.1fs", this->flash_length_ / 1e3f); } this->parent_->start_flash_(v, this->flash_length_, publish); } else if (this->has_transition_()) { // TRANSITION if (publish) { - ESP_LOGD(TAG, " Transition length: %.1fs", this->transition_length_ / 1e3f); + ESP_LOGV(TAG, " Transition length: %.1fs", this->transition_length_ / 1e3f); } // Special case: Transition and effect can be set when turning off if (this->has_effect_()) { if (publish) { - ESP_LOGD(TAG, " Effect: 'None'"); + ESP_LOGV(TAG, " Effect: 'None'"); } this->parent_->stop_effect_(); } @@ -150,7 +150,7 @@ void LightCall::perform() { } if (publish) { - ESP_LOGD(TAG, " Effect: '%.*s'", (int) effect_s.size(), effect_s.c_str()); + ESP_LOGV(TAG, " Effect: '%.*s'", (int) effect_s.size(), effect_s.c_str()); } this->parent_->start_effect_(this->effect_); diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 4aa636e998..90937485b9 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -41,7 +41,7 @@ void Lock::publish_state(LockState state) { this->state = state; this->rtc_.save(&this->state); - ESP_LOGD(TAG, "'%s' >> %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state))); + ESP_LOGV(TAG, "'%s' >> %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state))); this->state_callback_.call(); #if defined(USE_LOCK) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_lock_update(this); @@ -49,10 +49,10 @@ void Lock::publish_state(LockState state) { } void LockCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); this->validate_(); if (this->state_.has_value()) { - ESP_LOGD(TAG, " State: %s", LOG_STR_ARG(lock_state_to_string(*this->state_))); + ESP_LOGV(TAG, " State: %s", LOG_STR_ARG(lock_state_to_string(*this->state_))); } this->parent_->control(*this); } diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index 70086089ff..a0eb7b5500 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -110,20 +110,20 @@ void MediaPlayerCall::validate_() { } void MediaPlayerCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); this->validate_(); if (this->command_.has_value()) { const char *command_s = media_player_command_to_string(this->command_.value()); - ESP_LOGD(TAG, " Command: %s", command_s); + ESP_LOGV(TAG, " Command: %s", command_s); } if (this->media_url_.has_value()) { - ESP_LOGD(TAG, " Media URL: %s", this->media_url_.value().c_str()); + ESP_LOGV(TAG, " Media URL: %s", this->media_url_.value().c_str()); } if (this->volume_.has_value()) { - ESP_LOGD(TAG, " Volume: %.2f", this->volume_.value()); + ESP_LOGV(TAG, " Volume: %.2f", this->volume_.value()); } if (this->announcement_.has_value()) { - ESP_LOGD(TAG, " Announcement: %s", this->announcement_.value() ? "yes" : "no"); + ESP_LOGV(TAG, " Announcement: %s", this->announcement_.value() ? "yes" : "no"); } this->parent_->control(*this); } diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index fb5d6e9f28..ca5aab6469 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -22,7 +22,7 @@ void log_number(const char *tag, const char *prefix, const char *type, Number *o void Number::publish_state(float state) { this->set_has_state(true); this->state = state; - ESP_LOGD(TAG, "'%s' >> %.2f", this->get_name().c_str(), state); + ESP_LOGV(TAG, "'%s' >> %.2f", this->get_name().c_str(), state); this->state_callback_.call(state); #if defined(USE_NUMBER) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_number_update(this); diff --git a/esphome/components/number/number_call.cpp b/esphome/components/number/number_call.cpp index aac9b2a23d..e300ca72de 100644 --- a/esphome/components/number/number_call.cpp +++ b/esphome/components/number/number_call.cpp @@ -61,7 +61,7 @@ void NumberCall::perform() { float max_value = traits.get_max_value(); if (this->operation_ == NUMBER_OP_SET) { - ESP_LOGD(TAG, "'%s': Setting value", name); + ESP_LOGV(TAG, "'%s': Setting value", name); if (!this->value_.has_value() || std::isnan(*this->value_)) { this->log_perform_warning_(LOG_STR("No value")); return; @@ -80,7 +80,7 @@ void NumberCall::perform() { target_value = max_value; } } else if (this->operation_ == NUMBER_OP_INCREMENT) { - ESP_LOGD(TAG, "'%s': Increment with%s cycling", name, this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); + ESP_LOGV(TAG, "'%s': Increment with%s cycling", name, this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); if (!parent->has_state()) { this->log_perform_warning_(LOG_STR("Can't increment, no state")); return; @@ -90,7 +90,7 @@ void NumberCall::perform() { if (target_value > max_value) target_value = this->cycle_or_clamp_(max_value, min_value); } else if (this->operation_ == NUMBER_OP_DECREMENT) { - ESP_LOGD(TAG, "'%s': Decrement with%s cycling", name, this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); + ESP_LOGV(TAG, "'%s': Decrement with%s cycling", name, this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); if (!parent->has_state()) { this->log_perform_warning_(LOG_STR("Can't decrement, no state")); return; @@ -110,7 +110,7 @@ void NumberCall::perform() { return; } - ESP_LOGD(TAG, " New value: %f", target_value); + ESP_LOGV(TAG, " New value: %f", target_value); this->parent_->control(target_value); } diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index df90c657e2..7c3dab15ad 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -31,7 +31,7 @@ void Select::publish_state(size_t index) { #pragma GCC diagnostic ignored "-Wdeprecated-declarations" this->state = option; // Update deprecated member for backward compatibility #pragma GCC diagnostic pop - ESP_LOGD(TAG, "'%s' >> %s (%zu)", this->get_name().c_str(), option, index); + ESP_LOGV(TAG, "'%s' >> %s (%zu)", this->get_name().c_str(), option, index); this->state_callback_.call(index); #if defined(USE_SELECT) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_select_update(this); diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 83f5052fc8..0e14371d00 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -64,7 +64,7 @@ optional SelectCall::calculate_target_index_(const char *name) { } if (this->operation_ == SELECT_OP_SET) { - ESP_LOGD(TAG, "'%s' - Setting", name); + ESP_LOGV(TAG, "'%s' - Setting", name); if (!this->index_.has_value()) { ESP_LOGW(TAG, "'%s' - No option set", name); return nullopt; @@ -73,7 +73,7 @@ optional SelectCall::calculate_target_index_(const char *name) { } // SELECT_OP_NEXT or SELECT_OP_PREVIOUS - ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, + ESP_LOGV(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? LOG_STR_LITERAL("next") : LOG_STR_LITERAL("previous"), this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index aad7f86dcf..59e011932b 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -122,7 +122,7 @@ void Sensor::clear_filters() { void Sensor::internal_send_state_to_frontend(float state) { this->set_has_state(true); this->state = state; - ESP_LOGD(TAG, "'%s' >> %.*f %s", this->get_name().c_str(), std::max(0, (int) this->get_accuracy_decimals()), state, + ESP_LOGV(TAG, "'%s' >> %.*f %s", this->get_name().c_str(), std::max(0, (int) this->get_accuracy_decimals()), state, this->get_unit_of_measurement_ref().c_str()); this->callback_.call(state); #if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index df762addbb..11840db3a3 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -61,7 +61,7 @@ void Switch::publish_state(bool state) { if (restore_mode & RESTORE_MODE_PERSISTENT_MASK) this->rtc_.save(&this->state); - ESP_LOGD(TAG, "'%s' >> %s", this->name_.c_str(), ONOFF(this->state)); + ESP_LOGV(TAG, "'%s' >> %s", this->name_.c_str(), ONOFF(this->state)); this->state_callback_.call(this->state); #if defined(USE_SWITCH) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_switch_update(this); diff --git a/esphome/components/text/text.cpp b/esphome/components/text/text.cpp index 12abc5d939..032ea468e6 100644 --- a/esphome/components/text/text.cpp +++ b/esphome/components/text/text.cpp @@ -19,9 +19,9 @@ void Text::publish_state(const char *state, size_t len) { this->state.assign(state, len); } if (this->traits.get_mode() == TEXT_MODE_PASSWORD) { - ESP_LOGD(TAG, "'%s' >> " LOG_SECRET("'%s'"), this->get_name().c_str(), this->state.c_str()); + ESP_LOGV(TAG, "'%s' >> " LOG_SECRET("'%s'"), this->get_name().c_str(), this->state.c_str()); } else { - ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->state.c_str()); + ESP_LOGV(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->state.c_str()); } this->state_callback_.call(this->state); #if defined(USE_TEXT) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/text/text_call.cpp b/esphome/components/text/text_call.cpp index b7aed098c7..b7692658af 100644 --- a/esphome/components/text/text_call.cpp +++ b/esphome/components/text/text_call.cpp @@ -48,10 +48,10 @@ void TextCall::perform() { std::string target_value = this->value_.value(); if (this->parent_->traits.get_mode() == TEXT_MODE_PASSWORD) { - ESP_LOGD(TAG, "'%s' - Setting password value: " LOG_SECRET("'%s'"), this->parent_->get_name().c_str(), + ESP_LOGV(TAG, "'%s' - Setting password value: " LOG_SECRET("'%s'"), this->parent_->get_name().c_str(), target_value.c_str()); } else { - ESP_LOGD(TAG, "'%s' - Setting text value: %s", this->parent_->get_name().c_str(), target_value.c_str()); + ESP_LOGV(TAG, "'%s' - Setting text value: %s", this->parent_->get_name().c_str(), target_value.c_str()); } this->parent_->control(target_value); } diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 0dc29f9a94..31543117b8 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -112,7 +112,7 @@ void TextSensor::internal_send_state_to_frontend(const char *state, size_t len) void TextSensor::notify_frontend_() { this->set_has_state(true); - ESP_LOGD(TAG, "'%s' >> '%s'", this->name_.c_str(), this->state.c_str()); + ESP_LOGV(TAG, "'%s' >> '%s'", this->name_.c_str(), this->state.c_str()); this->callback_.call(this->state); #if defined(USE_TEXT_SENSOR) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_text_sensor_update(this); diff --git a/esphome/components/update/update_entity.cpp b/esphome/components/update/update_entity.cpp index 7edea2fe22..1a5a55577f 100644 --- a/esphome/components/update/update_entity.cpp +++ b/esphome/components/update/update_entity.cpp @@ -18,28 +18,28 @@ const LogString *update_state_to_string(UpdateState state) { } void UpdateEntity::publish_state() { - ESP_LOGD(TAG, + ESP_LOGV(TAG, "'%s' >>\n" " Current Version: %s", this->name_.c_str(), this->update_info_.current_version.c_str()); if (!this->update_info_.md5.empty()) { - ESP_LOGD(TAG, " Latest Version: %s", this->update_info_.latest_version.c_str()); + ESP_LOGV(TAG, " Latest Version: %s", this->update_info_.latest_version.c_str()); } if (!this->update_info_.firmware_url.empty()) { - ESP_LOGD(TAG, " Firmware URL: %s", this->update_info_.firmware_url.c_str()); + ESP_LOGV(TAG, " Firmware URL: %s", this->update_info_.firmware_url.c_str()); } - ESP_LOGD(TAG, " Title: %s", this->update_info_.title.c_str()); + ESP_LOGV(TAG, " Title: %s", this->update_info_.title.c_str()); if (!this->update_info_.summary.empty()) { - ESP_LOGD(TAG, " Summary: %s", this->update_info_.summary.c_str()); + ESP_LOGV(TAG, " Summary: %s", this->update_info_.summary.c_str()); } if (!this->update_info_.release_url.empty()) { - ESP_LOGD(TAG, " Release URL: %s", this->update_info_.release_url.c_str()); + ESP_LOGV(TAG, " Release URL: %s", this->update_info_.release_url.c_str()); } if (this->update_info_.has_progress) { - ESP_LOGD(TAG, " Progress: %.0f%%", this->update_info_.progress); + ESP_LOGV(TAG, " Progress: %.0f%%", this->update_info_.progress); } this->set_has_state(true); diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index 636da1f3c3..9e1ef9da50 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -68,21 +68,21 @@ ValveCall &ValveCall::set_position(float position) { return *this; } void ValveCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); auto traits = this->parent_->get_traits(); this->validate_(); if (this->stop_) { - ESP_LOGD(TAG, " Command: STOP"); + ESP_LOGV(TAG, " Command: STOP"); } if (this->position_.has_value()) { if (traits.get_supports_position()) { - ESP_LOGD(TAG, " Position: %.0f%%", *this->position_ * 100.0f); + ESP_LOGV(TAG, " Position: %.0f%%", *this->position_ * 100.0f); } else { - ESP_LOGD(TAG, " Command: %s", LOG_STR_ARG(valve_command_to_str(*this->position_))); + ESP_LOGV(TAG, " Command: %s", LOG_STR_ARG(valve_command_to_str(*this->position_))); } } if (this->toggle_.has_value()) { - ESP_LOGD(TAG, " Command: TOGGLE"); + ESP_LOGV(TAG, " Command: TOGGLE"); } this->parent_->control(*this); } @@ -128,20 +128,20 @@ ValveCall Valve::make_call() { return {this}; } void Valve::publish_state(bool save) { this->position = clamp(this->position, 0.0f, 1.0f); - ESP_LOGD(TAG, "'%s' >>", this->name_.c_str()); + ESP_LOGV(TAG, "'%s' >>", this->name_.c_str()); auto traits = this->get_traits(); if (traits.get_supports_position()) { - ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f); + ESP_LOGV(TAG, " Position: %.0f%%", this->position * 100.0f); } else { if (this->position == VALVE_OPEN) { - ESP_LOGD(TAG, " State: OPEN"); + ESP_LOGV(TAG, " State: OPEN"); } else if (this->position == VALVE_CLOSED) { - ESP_LOGD(TAG, " State: CLOSED"); + ESP_LOGV(TAG, " State: CLOSED"); } else { - ESP_LOGD(TAG, " State: UNKNOWN"); + ESP_LOGV(TAG, " State: UNKNOWN"); } } - ESP_LOGD(TAG, " Current Operation: %s", LOG_STR_ARG(valve_operation_to_str(this->current_operation))); + ESP_LOGV(TAG, " Current Operation: %s", LOG_STR_ARG(valve_operation_to_str(this->current_operation))); this->state_callback_.call(); #if defined(USE_VALVE) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index 3989230d2d..9a74877f0a 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -83,25 +83,25 @@ WaterHeaterCall &WaterHeaterCall::set_on(bool on) { } void WaterHeaterCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); this->validate_(); if (this->mode_.has_value()) { - ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(water_heater_mode_to_string(*this->mode_))); + ESP_LOGV(TAG, " Mode: %s", LOG_STR_ARG(water_heater_mode_to_string(*this->mode_))); } if (!std::isnan(this->target_temperature_)) { - ESP_LOGD(TAG, " Target Temperature: %.2f", this->target_temperature_); + ESP_LOGV(TAG, " Target Temperature: %.2f", this->target_temperature_); } if (!std::isnan(this->target_temperature_low_)) { - ESP_LOGD(TAG, " Target Temperature Low: %.2f", this->target_temperature_low_); + ESP_LOGV(TAG, " Target Temperature Low: %.2f", this->target_temperature_low_); } if (!std::isnan(this->target_temperature_high_)) { - ESP_LOGD(TAG, " Target Temperature High: %.2f", this->target_temperature_high_); + ESP_LOGV(TAG, " Target Temperature High: %.2f", this->target_temperature_high_); } if (this->state_mask_ & WATER_HEATER_STATE_AWAY) { - ESP_LOGD(TAG, " Away: %s", (this->state_ & WATER_HEATER_STATE_AWAY) ? "YES" : "NO"); + ESP_LOGV(TAG, " Away: %s", (this->state_ & WATER_HEATER_STATE_AWAY) ? "YES" : "NO"); } if (this->state_mask_ & WATER_HEATER_STATE_ON) { - ESP_LOGD(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO"); + ESP_LOGV(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO"); } this->parent_->control(*this); } @@ -158,24 +158,24 @@ void WaterHeaterCall::validate_() { void WaterHeater::publish_state() { auto traits = this->get_traits(); - ESP_LOGD(TAG, + ESP_LOGV(TAG, "'%s' >>\n" " Mode: %s", this->name_.c_str(), LOG_STR_ARG(water_heater_mode_to_string(this->mode_))); if (!std::isnan(this->current_temperature_)) { - ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature_); + ESP_LOGV(TAG, " Current Temperature: %.2f°C", this->current_temperature_); } if (traits.get_supports_two_point_target_temperature()) { - ESP_LOGD(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low_, + ESP_LOGV(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low_, this->target_temperature_high_); } else if (!std::isnan(this->target_temperature_)) { - ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature_); + ESP_LOGV(TAG, " Target Temperature: %.2f°C", this->target_temperature_); } if (this->state_ & WATER_HEATER_STATE_AWAY) { - ESP_LOGD(TAG, " Away: YES"); + ESP_LOGV(TAG, " Away: YES"); } if (traits.has_feature_flags(WATER_HEATER_SUPPORTS_ON_OFF)) { - ESP_LOGD(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO"); + ESP_LOGV(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO"); } #if defined(USE_WATER_HEATER) && defined(USE_CONTROLLER_REGISTRY) From a075f63b59c68dd6e627c505397c1cdf5a052b45 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:50:37 -0400 Subject: [PATCH 047/115] [uart] Fix debug callback missing peeked byte and reading past end (#15169) --- esphome/components/uart/uart_component_esp_idf.cpp | 6 ++++-- esphome/components/uart/uart_component_host.cpp | 6 ++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index bd2f915d3a..cd77cd1189 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -324,6 +324,9 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) { } bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { + if (len == 0) { + return false; + } size_t length_to_read = len; int32_t read_len = 0; if (!this->check_read_timeout_(len)) @@ -331,11 +334,10 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { if (this->has_peek_) { length_to_read--; *data = this->peek_byte_; - data++; this->has_peek_ = false; } if (length_to_read > 0) - read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS); + read_len = uart_read_bytes(this->uart_num_, data + (len - length_to_read), length_to_read, 20 / portTICK_PERIOD_MS); #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { this->debug_callback_.call(UART_DIRECTION_RX, data[i]); diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp index 0042ffae23..085610a983 100644 --- a/esphome/components/uart/uart_component_host.cpp +++ b/esphome/components/uart/uart_component_host.cpp @@ -235,16 +235,14 @@ bool HostUartComponent::read_array(uint8_t *data, size_t len) { } if (!this->check_read_timeout_(len)) return false; - uint8_t *data_ptr = data; size_t length_to_read = len; if (this->has_peek_) { length_to_read--; - *data_ptr = this->peek_byte_; - data_ptr++; + *data = this->peek_byte_; this->has_peek_ = false; } if (length_to_read > 0) { - int sz = ::read(this->file_descriptor_, data_ptr, length_to_read); + int sz = ::read(this->file_descriptor_, data + (len - length_to_read), length_to_read); if (sz == -1) { this->update_error_(strerror(errno)); return false; From 29e263ad7d42fc90d54e809e41709f03d2e7f006 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Mar 2026 13:43:01 -1000 Subject: [PATCH 048/115] [esp32] Wrap vfprintf to fix printf stub on picolibc (IDF 6) (#15172) --- esphome/components/esp32/__init__.py | 2 +- esphome/components/esp32/printf_stubs.cpp | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0e216485ac..91eb913e3d 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1587,7 +1587,7 @@ async def to_code(config): if conf[CONF_ADVANCED][CONF_ENABLE_FULL_PRINTF]: cg.add_define("USE_FULL_PRINTF") else: - for symbol in ("vprintf", "printf", "fprintf"): + for symbol in ("vprintf", "printf", "fprintf", "vfprintf"): cg.add_build_flag(f"-Wl,--wrap={symbol}") else: cg.add_build_flag("-DUSE_ARDUINO") diff --git a/esphome/components/esp32/printf_stubs.cpp b/esphome/components/esp32/printf_stubs.cpp index c6f03bc363..386fbbd79d 100644 --- a/esphome/components/esp32/printf_stubs.cpp +++ b/esphome/components/esp32/printf_stubs.cpp @@ -2,10 +2,11 @@ * Linker wrap stubs for FILE*-based printf functions. * * ESP-IDF SDK components (gpio driver, ringbuf, log_write) reference - * fprintf(), printf(), and vprintf() which pull in newlib's _vfprintf_r - * (~11 KB). This is a separate implementation from _svfprintf_r (used by - * snprintf/vsnprintf) that handles FILE* stream I/O with buffering and - * locking. + * fprintf(), printf(), vprintf(), and vfprintf() which pull in the full + * printf implementation (~11 KB on newlib's _vfprintf_r, ~2.8 KB on + * picolibc's vfprintf). This is a separate implementation from the one + * used by snprintf/vsnprintf that handles FILE* stream I/O with buffering + * and locking. * * ESPHome replaces the ESP-IDF log handler via esp_log_set_vprintf_(), * so the SDK's vprintf() path is dead code at runtime. The fprintf() @@ -70,11 +71,15 @@ int __wrap_printf(const char *fmt, ...) { return len; } +int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) { + char buf[PRINTF_BUFFER_SIZE]; + return write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); +} + int __wrap_fprintf(FILE *stream, const char *fmt, ...) { va_list ap; va_start(ap, fmt); - char buf[PRINTF_BUFFER_SIZE]; - int len = write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); + int len = __wrap_vfprintf(stream, fmt, ap); va_end(ap); return len; } From 676ac9d8b876044b0278f48fbd70100cf8594141 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 25 Mar 2026 21:30:46 -0500 Subject: [PATCH 049/115] [infrared][ir_rf_proxy] Add `receiver_frequency` config for IR receiver demodulation frequency (#15156) Co-authored-by: J. Nick Koston --- esphome/components/api/api.proto | 1 + esphome/components/api/api_connection.cpp | 1 + esphome/components/api/api_pb2.cpp | 2 ++ esphome/components/api/api_pb2.h | 3 ++- esphome/components/api/api_pb2_dump.cpp | 1 + esphome/components/const/__init__.py | 1 + esphome/components/infrared/infrared.h | 4 ++++ esphome/components/ir_rf_proxy/infrared.py | 15 ++++++++++++++- esphome/components/ir_rf_proxy/ir_rf_proxy.h | 3 +++ tests/components/ir_rf_proxy/common-rx.yaml | 1 + 10 files changed, 30 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 86daa9a2bf..96ee2fb920 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -2512,6 +2512,7 @@ message ListEntitiesInfraredResponse { EntityCategory entity_category = 6; uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"]; uint32 capabilities = 8; // Bitfield of InfraredCapabilityFlags + uint32 receiver_frequency = 9; // Demodulation frequency of the IR receiver in Hz (0 = unspecified) } // Command to transmit infrared/RF data using raw timings diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index d023cd21a8..0a99adcacf 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1549,6 +1549,7 @@ uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection auto *infrared = static_cast(entity); ListEntitiesInfraredResponse msg; msg.capabilities = infrared->get_capability_flags(); + msg.receiver_frequency = infrared->get_traits().get_receiver_frequency_hz(); return fill_and_encode_entity_info(infrared, msg, conn, remaining_size); } #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index f77f4df545..ae2cd2bae8 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3657,6 +3657,7 @@ void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(7, this->device_id); #endif buffer.encode_uint32(8, this->capabilities); + buffer.encode_uint32(9, this->receiver_frequency); } uint32_t ListEntitiesInfraredResponse::calculate_size() const { uint32_t size = 0; @@ -3672,6 +3673,7 @@ uint32_t ListEntitiesInfraredResponse::calculate_size() const { size += ProtoSize::calc_uint32(1, this->device_id); #endif size += ProtoSize::calc_uint32(1, this->capabilities); + size += ProtoSize::calc_uint32(1, this->receiver_frequency); return size; } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 16586e6e9a..14f6c704ae 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -3041,11 +3041,12 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 135; - static constexpr uint8_t ESTIMATED_SIZE = 44; + static constexpr uint8_t ESTIMATED_SIZE = 48; #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("list_entities_infrared_response"); } #endif uint32_t capabilities{0}; + uint32_t receiver_frequency{0}; void encode(ProtoWriteBuffer &buffer) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index a11f3b231e..640c347371 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -2572,6 +2572,7 @@ const char *ListEntitiesInfraredResponse::dump_to(DumpBuffer &out) const { dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif dump_field(out, ESPHOME_PSTR("capabilities"), this->capabilities); + dump_field(out, ESPHOME_PSTR("receiver_frequency"), this->receiver_frequency); return out.c_str(); } #endif diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 1fbf88c276..0eb37e3029 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -18,6 +18,7 @@ CONF_ON_PACKET = "on_packet" CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" CONF_PARITY = "parity" +CONF_RECEIVER_FREQUENCY = "receiver_frequency" CONF_REQUEST_HEADERS = "request_headers" CONF_ROWS = "rows" CONF_STOP_BITS = "stop_bits" diff --git a/esphome/components/infrared/infrared.h b/esphome/components/infrared/infrared.h index 59535f499a..6d91c97cce 100644 --- a/esphome/components/infrared/infrared.h +++ b/esphome/components/infrared/infrared.h @@ -101,9 +101,13 @@ class InfraredTraits { bool get_supports_receiver() const { return this->supports_receiver_; } void set_supports_receiver(bool supports) { this->supports_receiver_ = supports; } + uint32_t get_receiver_frequency_hz() const { return this->receiver_frequency_hz_; } + void set_receiver_frequency_hz(uint32_t freq) { this->receiver_frequency_hz_ = freq; } + protected: bool supports_transmitter_{false}; bool supports_receiver_{false}; + uint32_t receiver_frequency_hz_{0}; // Demodulation frequency of the IR receiver in Hz (0 = unspecified) }; /// Infrared - Base class for infrared remote control implementations diff --git a/esphome/components/ir_rf_proxy/infrared.py b/esphome/components/ir_rf_proxy/infrared.py index 4a4d9fa860..3218889721 100644 --- a/esphome/components/ir_rf_proxy/infrared.py +++ b/esphome/components/ir_rf_proxy/infrared.py @@ -4,6 +4,7 @@ from typing import Any import esphome.codegen as cg from esphome.components import infrared, remote_receiver, remote_transmitter +from esphome.components.const import CONF_RECEIVER_FREQUENCY import esphome.config_validation as cv from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY import esphome.final_validate as fv @@ -19,6 +20,7 @@ CONFIG_SCHEMA = cv.All( infrared.infrared_schema(IrRfProxy).extend( { cv.Optional(CONF_FREQUENCY, default=0): cv.frequency, + cv.Optional(CONF_RECEIVER_FREQUENCY): cv.frequency, cv.Optional(CONF_REMOTE_RECEIVER_ID): cv.use_id( remote_receiver.RemoteReceiverComponent ), @@ -33,7 +35,14 @@ CONFIG_SCHEMA = cv.All( def _final_validate(config: dict[str, Any]) -> None: """Validate that transmitters have a proper carrier duty cycle.""" - # Only validate if this is an infrared (not RF) configuration with a transmitter + # receiver_frequency is only meaningful for receiver configurations + if CONF_RECEIVER_FREQUENCY in config and CONF_REMOTE_RECEIVER_ID not in config: + raise cv.Invalid( + f"'{CONF_RECEIVER_FREQUENCY}' can only be used with '{CONF_REMOTE_RECEIVER_ID}', " + "not with a transmitter" + ) + + # Only validate duty cycle if this is an infrared (not RF) configuration with a transmitter if config.get(CONF_FREQUENCY, 0) != 0 or CONF_REMOTE_TRANSMITTER_ID not in config: return @@ -75,3 +84,7 @@ async def to_code(config: dict[str, Any]) -> None: if CONF_REMOTE_RECEIVER_ID in config: receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID]) cg.add(var.set_receiver(receiver)) + + # Set receiver demodulation frequency if specified (metadata only, no hardware effect) + if CONF_RECEIVER_FREQUENCY in config: + cg.add(var.set_receiver_frequency(config[CONF_RECEIVER_FREQUENCY])) diff --git a/esphome/components/ir_rf_proxy/ir_rf_proxy.h b/esphome/components/ir_rf_proxy/ir_rf_proxy.h index f067a6e17a..05b988f287 100644 --- a/esphome/components/ir_rf_proxy/ir_rf_proxy.h +++ b/esphome/components/ir_rf_proxy/ir_rf_proxy.h @@ -22,6 +22,9 @@ class IrRfProxy : public infrared::Infrared { /// Check if this is RF mode (non-zero frequency) bool is_rf() const { return this->frequency_khz_ > 0; } + /// Set the receiver's hardware demodulation frequency in Hz (metadata only, does not affect hardware) + void set_receiver_frequency(uint32_t frequency_hz) { this->get_traits().set_receiver_frequency_hz(frequency_hz); } + protected: // RF frequency in kHz (Hz / 1000); 0 = infrared, non-zero = RF uint32_t frequency_khz_{0}; diff --git a/tests/components/ir_rf_proxy/common-rx.yaml b/tests/components/ir_rf_proxy/common-rx.yaml index 0f758f832d..37033a128e 100644 --- a/tests/components/ir_rf_proxy/common-rx.yaml +++ b/tests/components/ir_rf_proxy/common-rx.yaml @@ -8,6 +8,7 @@ infrared: - platform: ir_rf_proxy id: ir_rx name: "IR Receiver" + receiver_frequency: 38kHz remote_receiver_id: ir_receiver # RF 900MHz receiver From 8a6b009173a8373df76b3a4d07040c8852162b8e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Mar 2026 16:53:33 -1000 Subject: [PATCH 050/115] [light] Move normal state logging to VERBOSE (#15177) --- esphome/components/light/light_call.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 41bd98de7b..7c936b51b7 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -385,7 +385,7 @@ void LightCall::transform_parameters_() { !(this->color_mode_ & ColorCapability::WHITE) && // !(this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && // min_mireds > 0.0f && max_mireds > 0.0f) { - ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values", + ESP_LOGV(TAG, "'%s': setting cold/warm white channels using white/color temperature values", this->parent_->get_name().c_str()); // Only compute cold_white/warm_white from color_temperature if they're not already explicitly set. // This is important for state restoration, where both color_temperature and cold_white/warm_white @@ -432,7 +432,7 @@ ColorMode LightCall::compute_color_mode_() { // Don't change if the current mode is in the intersection (suitable AND supported) if (ColorModeMask::mask_contains(intersection, current_mode)) { - ESP_LOGI(TAG, "'%s': color mode not specified; retaining %s", this->parent_->get_name().c_str(), + ESP_LOGV(TAG, "'%s': color mode not specified; retaining %s", this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(current_mode))); return current_mode; } @@ -440,7 +440,7 @@ ColorMode LightCall::compute_color_mode_() { // Use the preferred suitable mode. if (intersection != 0) { ColorMode mode = ColorModeMask::first_value_from_mask(intersection); - ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(), + ESP_LOGV(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(mode))); return mode; } From 92604017471dd0dbcfbb90f6991f62160545b2e8 Mon Sep 17 00:00:00 2001 From: Daniel Kent <129895318+danielkent-net@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:11:46 -0400 Subject: [PATCH 051/115] [bmp581] Add SPI support for BMP581 (#13124) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + .../components/bmp581_base/bmp581_base.cpp | 11 ++- esphome/components/bmp581_base/bmp581_base.h | 3 + esphome/components/bmp581_spi/__init__.py | 0 esphome/components/bmp581_spi/bmp581_spi.cpp | 73 +++++++++++++++++++ esphome/components/bmp581_spi/bmp581_spi.h | 24 ++++++ esphome/components/bmp581_spi/sensor.py | 48 ++++++++++++ tests/components/bmp581_spi/common.yaml | 9 +++ .../components/bmp581_spi/test.esp32-idf.yaml | 7 ++ .../bmp581_spi/test.esp8266-ard.yaml | 7 ++ .../bmp581_spi/test.rp2040-ard.yaml | 7 ++ 11 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 esphome/components/bmp581_spi/__init__.py create mode 100644 esphome/components/bmp581_spi/bmp581_spi.cpp create mode 100644 esphome/components/bmp581_spi/bmp581_spi.h create mode 100644 esphome/components/bmp581_spi/sensor.py create mode 100644 tests/components/bmp581_spi/common.yaml create mode 100644 tests/components/bmp581_spi/test.esp32-idf.yaml create mode 100644 tests/components/bmp581_spi/test.esp8266-ard.yaml create mode 100644 tests/components/bmp581_spi/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index afe4cdb871..8d297d7b07 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -92,6 +92,7 @@ esphome/components/bmp3xx_i2c/* @latonita esphome/components/bmp3xx_spi/* @latonita esphome/components/bmp581_base/* @danielkent-net @kahrendt esphome/components/bmp581_i2c/* @danielkent-net @kahrendt +esphome/components/bmp581_spi/* @danielkent-net @kahrendt esphome/components/bp1658cj/* @Cossid esphome/components/bp5758d/* @Cossid esphome/components/bthome_mithermometer/* @nagyrobi diff --git a/esphome/components/bmp581_base/bmp581_base.cpp b/esphome/components/bmp581_base/bmp581_base.cpp index 89a92de31d..c9d250545b 100644 --- a/esphome/components/bmp581_base/bmp581_base.cpp +++ b/esphome/components/bmp581_base/bmp581_base.cpp @@ -469,14 +469,18 @@ bool BMP581Component::read_temperature_and_pressure_(float &temperature, float & } bool BMP581Component::reset_() { + // - activates interface (only relevant for SPI mode) // - writes reset command to the command register // - waits for sensor to complete reset + // - activates interface (only relevant for SPI mode) // - returns the Power-On-Reboot interrupt status, which is asserted if successful + // activates communication interface (SPI only) + this->activate_interface(); + // writes reset command to BMP's command register if (!this->bmp_write_byte(BMP581_COMMAND, RESET_COMMAND)) { ESP_LOGE(TAG, "Failed to write reset command"); - return false; } @@ -484,6 +488,9 @@ bool BMP581Component::reset_() { // - round up to 3 ms delay(3); + // reactivates communication interface after reset (SPI only) + this->activate_interface(); + // read interrupt status register if (!this->bmp_read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) { ESP_LOGE(TAG, "Failed to read interrupt status register"); @@ -491,7 +498,7 @@ bool BMP581Component::reset_() { return false; } - // Power-On-Reboot bit is asserted if sensor successfully reset + // power-On-Reboot bit is asserted if sensor successfully reset return this->int_status_.bit.por; } diff --git a/esphome/components/bmp581_base/bmp581_base.h b/esphome/components/bmp581_base/bmp581_base.h index d99c420272..c3920512e0 100644 --- a/esphome/components/bmp581_base/bmp581_base.h +++ b/esphome/components/bmp581_base/bmp581_base.h @@ -87,6 +87,9 @@ class BMP581Component : public PollingComponent { virtual bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; virtual bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; + // Interface activation function. Only used for SPI interface; no-op for I2C. + virtual void activate_interface() {} + sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *pressure_sensor_{nullptr}; diff --git a/esphome/components/bmp581_spi/__init__.py b/esphome/components/bmp581_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/bmp581_spi/bmp581_spi.cpp b/esphome/components/bmp581_spi/bmp581_spi.cpp new file mode 100644 index 0000000000..01435880f0 --- /dev/null +++ b/esphome/components/bmp581_spi/bmp581_spi.cpp @@ -0,0 +1,73 @@ +#include +#include + +#include "bmp581_spi.h" +#include "esphome/components/bmp581_base/bmp581_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome::bmp581_spi { + +static const char *const TAG = "bmp581_spi"; + +// OR (|) register with BMP_SPI_READ for read +inline constexpr uint8_t BMP_SPI_READ = 0x80; + +// AND (&) register with BMP_SPI_WRITE for write +inline constexpr uint8_t BMP_SPI_WRITE = 0x7F; + +void BMP581SPIComponent::dump_config() { + BMP581Component::dump_config(); + LOG_SPI_DEVICE(this); +} + +void BMP581SPIComponent::setup() { + this->spi_setup(); + BMP581Component::setup(); +} + +void BMP581SPIComponent::activate_interface() { + // - forces the device into SPI mode using a dummy read + uint8_t dummy_read = 0; + this->bmp_read_byte(bmp581_base::BMP581_CHIP_ID, &dummy_read); +} + +// In SPI mode, only 7 bits of the register addresses are used; the MSB of register address is not used +// and replaced by a read/write bit (RW = ‘0’ for write and RW = ‘1’ for read). +// Example: address 0xF7 is accessed by using SPI register address 0x77. For write access, the byte +// 0x77 is transferred, for read access, the byte 0xF7 is transferred. +// The expressions BMP_SPI_READ (| with register) and BMP_SPI_WRITE (& with register) +// are defined for readability. +// https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp581-ds004.pdf + +bool BMP581SPIComponent::bmp_read_byte(uint8_t a_register, uint8_t *data) { + this->enable(); + this->transfer_byte(a_register | BMP_SPI_READ); + *data = this->transfer_byte(0); + this->disable(); + return true; +} + +bool BMP581SPIComponent::bmp_write_byte(uint8_t a_register, uint8_t data) { + this->enable(); + this->transfer_byte(a_register & BMP_SPI_WRITE); + this->transfer_byte(data); + this->disable(); + return true; +} + +bool BMP581SPIComponent::bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->transfer_byte(a_register | BMP_SPI_READ); + this->read_array(data, len); + this->disable(); + return true; +} + +bool BMP581SPIComponent::bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->transfer_byte(a_register & BMP_SPI_WRITE); + this->write_array(data, len); + this->disable(); + return true; +} +} // namespace esphome::bmp581_spi diff --git a/esphome/components/bmp581_spi/bmp581_spi.h b/esphome/components/bmp581_spi/bmp581_spi.h new file mode 100644 index 0000000000..57f75588d5 --- /dev/null +++ b/esphome/components/bmp581_spi/bmp581_spi.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/components/bmp581_base/bmp581_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome::bmp581_spi { + +// BMP581 is technically compatible with SPI Mode0 and Mode3. Default to Mode3. +class BMP581SPIComponent : public esphome::bmp581_base::BMP581Component, + public spi::SPIDevice { + public: + void setup() override; + bool bmp_read_byte(uint8_t a_register, uint8_t *data) override; + bool bmp_write_byte(uint8_t a_register, uint8_t data) override; + bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + void dump_config() override; + + protected: + void activate_interface() override; +}; + +} // namespace esphome::bmp581_spi diff --git a/esphome/components/bmp581_spi/sensor.py b/esphome/components/bmp581_spi/sensor.py new file mode 100644 index 0000000000..75f60b2460 --- /dev/null +++ b/esphome/components/bmp581_spi/sensor.py @@ -0,0 +1,48 @@ +import logging + +import esphome.codegen as cg +from esphome.components import spi +from esphome.components.spi import CONF_SPI_MODE +import esphome.config_validation as cv + +from ..bmp581_base import CONFIG_SCHEMA_BASE, to_code_base + +AUTO_LOAD = ["bmp581_base"] +CODEOWNERS = ["@kahrendt", "@danielkent-net"] +DEPENDENCIES = ["spi"] + +_LOGGER = logging.getLogger(__name__) + +VALID_SPI_MODES = { + 0: "MODE0", + "0": "MODE0", + "MODE0": "MODE0", + 3: "MODE3", + "3": "MODE3", + "MODE3": "MODE3", +} + +bmp581_ns = cg.esphome_ns.namespace("bmp581_spi") +BMP581SPIComponent = bmp581_ns.class_( + "BMP581SPIComponent", cg.PollingComponent, spi.SPIDevice +) + + +def check_spi_mode(config): + spi_mode = config.get(CONF_SPI_MODE) + if spi_mode not in VALID_SPI_MODES: + raise cv.Invalid("BMP581 only supports SPI mode 3") + return config + + +CONFIG_SCHEMA = cv.All( + CONFIG_SCHEMA_BASE.extend(spi.spi_device_schema(default_mode="mode3")).extend( + {cv.GenerateID(): cv.declare_id(BMP581SPIComponent)} + ), + check_spi_mode, +) + + +async def to_code(config): + var = await to_code_base(config) + await spi.register_spi_device(var, config) diff --git a/tests/components/bmp581_spi/common.yaml b/tests/components/bmp581_spi/common.yaml new file mode 100644 index 0000000000..f22074f867 --- /dev/null +++ b/tests/components/bmp581_spi/common.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: bmp581_spi + cs_pin: ${cs_pin} + temperature: + name: BMP581 Temperature + iir_filter: 2x + pressure: + name: BMP581 Pressure + oversampling: 128x diff --git a/tests/components/bmp581_spi/test.esp32-idf.yaml b/tests/components/bmp581_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..a3352cf880 --- /dev/null +++ b/tests/components/bmp581_spi/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + cs_pin: GPIO5 + +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/bmp581_spi/test.esp8266-ard.yaml b/tests/components/bmp581_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..595f31046a --- /dev/null +++ b/tests/components/bmp581_spi/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + cs_pin: GPIO15 + +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/bmp581_spi/test.rp2040-ard.yaml b/tests/components/bmp581_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..79ea6ce90b --- /dev/null +++ b/tests/components/bmp581_spi/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + cs_pin: GPIO5 + +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + +<<: !include common.yaml From f3a31be6d0f4d896db0356b1cabea72741a73921 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Mar 2026 07:32:39 -1000 Subject: [PATCH 052/115] [benchmark] Add climate publish_state and call benchmarks (#15180) --- .../benchmarks/components/climate/__init__.py | 5 + .../components/climate/bench_climate.cpp | 142 ++++++++++++++++++ .../components/climate/benchmark.yaml | 1 + 3 files changed, 148 insertions(+) create mode 100644 tests/benchmarks/components/climate/__init__.py create mode 100644 tests/benchmarks/components/climate/bench_climate.cpp create mode 100644 tests/benchmarks/components/climate/benchmark.yaml diff --git a/tests/benchmarks/components/climate/__init__.py b/tests/benchmarks/components/climate/__init__.py new file mode 100644 index 0000000000..b08f67a095 --- /dev/null +++ b/tests/benchmarks/components/climate/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/climate/bench_climate.cpp b/tests/benchmarks/components/climate/bench_climate.cpp new file mode 100644 index 0000000000..316a72b2b6 --- /dev/null +++ b/tests/benchmarks/components/climate/bench_climate.cpp @@ -0,0 +1,142 @@ +#include + +#include "esphome/components/climate/climate.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +static constexpr int kInnerIterations = 2000; + +// Minimal Climate for benchmarking — control() is a no-op. +class BenchClimate : public climate::Climate { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } + + climate::ClimateTraits traits() override { return this->traits_; } + + climate::ClimateTraits traits_; + + protected: + void control(const climate::ClimateCall & /*call*/) override {} +}; + +// Helper to create a typical HVAC climate device for benchmarks. +// Note: setup() is not called (no preferences backend), so save_state_() +// is effectively a no-op. This benchmarks the call/validation path, not persistence. +static void setup_hvac_climate(BenchClimate &climate) { + climate.configure("test_climate"); + climate.traits_.set_supported_modes({ + climate::CLIMATE_MODE_OFF, + climate::CLIMATE_MODE_HEAT_COOL, + climate::CLIMATE_MODE_COOL, + climate::CLIMATE_MODE_HEAT, + climate::CLIMATE_MODE_FAN_ONLY, + }); + climate.traits_.set_supported_fan_modes({ + climate::CLIMATE_FAN_AUTO, + climate::CLIMATE_FAN_LOW, + climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH, + }); + climate.traits_.set_supported_swing_modes({ + climate::CLIMATE_SWING_OFF, + climate::CLIMATE_SWING_BOTH, + climate::CLIMATE_SWING_VERTICAL, + climate::CLIMATE_SWING_HORIZONTAL, + }); + climate.traits_.set_supported_presets({ + climate::CLIMATE_PRESET_NONE, + climate::CLIMATE_PRESET_HOME, + climate::CLIMATE_PRESET_AWAY, + }); + climate.traits_.set_visual_min_temperature(16.0f); + climate.traits_.set_visual_max_temperature(30.0f); + climate.traits_.set_visual_target_temperature_step(0.5f); + climate.traits_.set_visual_current_temperature_step(0.1f); + climate.traits_.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION); +} + +// --- Climate::publish_state() with temperature update --- +// Measures the publish path for a thermostat reporting state — +// the hot path during HVAC operation. + +static void ClimatePublish_State(benchmark::State &state) { + BenchClimate climate; + setup_hvac_climate(climate); + climate.mode = climate::CLIMATE_MODE_HEAT; + climate.action = climate::CLIMATE_ACTION_HEATING; + climate.target_temperature = 22.0f; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + climate.current_temperature = 20.0f + static_cast(i % 100) / 10.0f; + climate.publish_state(); + } + benchmark::DoNotOptimize(climate.current_temperature); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ClimatePublish_State); + +// --- Climate::publish_state() with callback --- +// Measures callback dispatch overhead. + +static void ClimatePublish_WithCallback(benchmark::State &state) { + BenchClimate climate; + setup_hvac_climate(climate); + climate.mode = climate::CLIMATE_MODE_HEAT; + climate.target_temperature = 22.0f; + + uint64_t callback_count = 0; + climate.add_on_state_callback([&callback_count](climate::Climate & /*c*/) { callback_count++; }); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + climate.current_temperature = 20.0f + static_cast(i % 100) / 10.0f; + climate.publish_state(); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ClimatePublish_WithCallback); + +// --- ClimateCall::perform() set target temperature --- +// The most common climate call — adjusting the thermostat setpoint. + +static void ClimateCall_SetTemperature(benchmark::State &state) { + BenchClimate climate; + setup_hvac_climate(climate); + climate.mode = climate::CLIMATE_MODE_HEAT; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + float temp = 18.0f + static_cast(i % 25) * 0.5f; + climate.make_call().set_target_temperature(temp).perform(); + } + benchmark::DoNotOptimize(climate.target_temperature); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ClimateCall_SetTemperature); + +// --- ClimateCall::perform() mode change with fan --- +// Exercises the validation path with multiple fields set. + +static void ClimateCall_ModeChange(benchmark::State &state) { + BenchClimate climate; + setup_hvac_climate(climate); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + auto mode = (i % 2 == 0) ? climate::CLIMATE_MODE_HEAT : climate::CLIMATE_MODE_COOL; + auto fan = (i % 2 == 0) ? climate::CLIMATE_FAN_HIGH : climate::CLIMATE_FAN_LOW; + climate.make_call().set_mode(mode).set_fan_mode(fan).set_target_temperature(22.0f).perform(); + } + benchmark::DoNotOptimize(climate.mode); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ClimateCall_ModeChange); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/components/climate/benchmark.yaml b/tests/benchmarks/components/climate/benchmark.yaml new file mode 100644 index 0000000000..8e79ed0ae7 --- /dev/null +++ b/tests/benchmarks/components/climate/benchmark.yaml @@ -0,0 +1 @@ +climate: From 689828436107c797a0525dbf5f3b86f96189ec47 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Mar 2026 07:32:54 -1000 Subject: [PATCH 053/115] [benchmark] Add cover publish_state and call benchmarks (#15179) --- tests/benchmarks/components/cover/__init__.py | 5 + .../components/cover/bench_cover_publish.cpp | 107 ++++++++++++++++++ .../components/cover/benchmark.yaml | 1 + 3 files changed, 113 insertions(+) create mode 100644 tests/benchmarks/components/cover/__init__.py create mode 100644 tests/benchmarks/components/cover/bench_cover_publish.cpp create mode 100644 tests/benchmarks/components/cover/benchmark.yaml diff --git a/tests/benchmarks/components/cover/__init__.py b/tests/benchmarks/components/cover/__init__.py new file mode 100644 index 0000000000..b08f67a095 --- /dev/null +++ b/tests/benchmarks/components/cover/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/cover/bench_cover_publish.cpp b/tests/benchmarks/components/cover/bench_cover_publish.cpp new file mode 100644 index 0000000000..794d967edb --- /dev/null +++ b/tests/benchmarks/components/cover/bench_cover_publish.cpp @@ -0,0 +1,107 @@ +#include + +#include "esphome/components/cover/cover.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +static constexpr int kInnerIterations = 2000; + +// Minimal Cover for benchmarking — control() is a no-op. +class BenchCover : public cover::Cover { + public: + cover::CoverTraits get_traits() override { return this->traits_; } + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } + + cover::CoverTraits traits_; + + protected: + void control(const cover::CoverCall & /*call*/) override {} +}; + +// --- Cover::publish_state() with position updates --- +// Measures the publish path for a garage door reporting position +// during open/close — the hot path during movement. + +static void CoverPublish_Position(benchmark::State &state) { + BenchCover cover; + cover.configure("test_cover"); + cover.traits_.set_supports_position(true); + cover.traits_.set_supports_tilt(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + cover.position = static_cast(i % 101) / 100.0f; + cover.current_operation = (i % 2 == 0) ? cover::COVER_OPERATION_OPENING : cover::COVER_OPERATION_CLOSING; + cover.publish_state(false); + } + benchmark::DoNotOptimize(cover.position); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CoverPublish_Position); + +// --- Cover::publish_state() with callback --- +// Measures callback dispatch overhead. + +static void CoverPublish_WithCallback(benchmark::State &state) { + BenchCover cover; + cover.configure("test_cover"); + cover.traits_.set_supports_position(true); + + uint64_t callback_count = 0; + cover.add_on_state_callback([&callback_count]() { callback_count++; }); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + cover.position = static_cast(i % 101) / 100.0f; + cover.publish_state(false); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CoverPublish_WithCallback); + +// --- CoverCall::perform() open/close cycle --- +// Measures the full call path: validation + control delegation. + +static void CoverCall_OpenClose(benchmark::State &state) { + BenchCover cover; + cover.configure("test_cover"); + cover.traits_.set_supports_position(true); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + if (i % 2 == 0) { + cover.make_call().set_command_open().perform(); + } else { + cover.make_call().set_command_close().perform(); + } + } + benchmark::DoNotOptimize(cover.position); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CoverCall_OpenClose); + +// --- CoverCall::perform() set position --- +// Measures the position-setting call path. + +static void CoverCall_SetPosition(benchmark::State &state) { + BenchCover cover; + cover.configure("test_cover"); + cover.traits_.set_supports_position(true); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + float pos = static_cast(i % 101) / 100.0f; + cover.make_call().set_position(pos).perform(); + } + benchmark::DoNotOptimize(cover.position); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CoverCall_SetPosition); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/components/cover/benchmark.yaml b/tests/benchmarks/components/cover/benchmark.yaml new file mode 100644 index 0000000000..477724be5a --- /dev/null +++ b/tests/benchmarks/components/cover/benchmark.yaml @@ -0,0 +1 @@ +cover: From 02e23eb386bd3fde73c34a41cebdf2b2a08b41af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Mar 2026 07:33:10 -1000 Subject: [PATCH 054/115] [benchmark] Add light call and publish benchmarks (#15176) --- tests/benchmarks/components/light/__init__.py | 28 ++ .../components/light/bench_light_call.cpp | 253 ++++++++++++++++++ .../components/light/benchmark.yaml | 1 + 3 files changed, 282 insertions(+) create mode 100644 tests/benchmarks/components/light/__init__.py create mode 100644 tests/benchmarks/components/light/bench_light_call.cpp create mode 100644 tests/benchmarks/components/light/benchmark.yaml diff --git a/tests/benchmarks/components/light/__init__.py b/tests/benchmarks/components/light/__init__.py new file mode 100644 index 0000000000..233a3c246e --- /dev/null +++ b/tests/benchmarks/components/light/__init__.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +from esphome.components.light import generate_gamma_table +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # Light benchmarks need USE_LIGHT_GAMMA_LUT defined and a gamma table + # with external linkage that the benchmark .cpp can reference. + manifest.enable_codegen() + original_to_code = manifest.to_code + + async def to_code(config): + await original_to_code(config) + cg.add_define("USE_LIGHT_GAMMA_LUT") + # Use the light component's own generate_gamma_table() so the + # benchmark stays in sync with any formula changes. + forward = generate_gamma_table(2.8) + values = ", ".join(f"0x{int(v):04X}" for v in forward) + # Use extern-visible (non-static) array so the benchmark .cpp + # can reference it via extern declaration. + cg.add_global( + cg.RawStatement( + f"extern const uint16_t bench_gamma_2_8_fwd[256] PROGMEM = {{{values}}};" + ) + ) + + to_code.priority = original_to_code.priority + manifest.to_code = to_code diff --git a/tests/benchmarks/components/light/bench_light_call.cpp b/tests/benchmarks/components/light/bench_light_call.cpp new file mode 100644 index 0000000000..c1ef0c425e --- /dev/null +++ b/tests/benchmarks/components/light/bench_light_call.cpp @@ -0,0 +1,253 @@ +#include + +#include "esphome/components/light/light_output.h" +#include "esphome/components/light/light_state.h" + +// Gamma 2.8 forward LUT generated by the light component's Python codegen +// (see tests/benchmarks/components/light/__init__.py which calls generate_gamma_table()) +extern const uint16_t bench_gamma_2_8_fwd[256]; + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +static constexpr int kInnerIterations = 2000; + +// Minimal LightOutput for benchmarking — no real hardware interaction. +class BenchLightOutput : public light::LightOutput { + public: + light::LightTraits get_traits() override { return this->traits_; } + void write_state(light::LightState * /*state*/) override {} + + light::LightTraits traits_; +}; + +// Test subclass to access protected configure_entity_() for benchmark setup. +class TestLightState : public light::LightState { + public: + using LightState::LightState; + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } +}; + +// Helper to create a configured RGBWW light state for benchmarks. +// Note: setup() is not called (no preferences backend), so save_remote_values_() +// is effectively a no-op. This benchmarks the call/validation path, not persistence. +static void setup_rgbww_light(BenchLightOutput &output, TestLightState &light) { + output.traits_.set_supported_color_modes({light::ColorMode::RGB_COLD_WARM_WHITE}); + output.traits_.set_min_mireds(153.0f); + output.traits_.set_max_mireds(500.0f); + light.configure("test_light"); + light.set_default_transition_length(0); + light.set_gamma_correct(2.8f); + light.set_gamma_table(bench_gamma_2_8_fwd); + light.set_restore_mode(light::LIGHT_ALWAYS_OFF); +} + +// --- LightCall::perform() with instant RGB color change (Home Assistant API path) --- +// Measures the full call path: validation, set_immediately_, publish, and save. +// HA sends color_mode explicitly since API 1.6. + +static void LightCall_RGBInstant(benchmark::State &state) { + BenchLightOutput output; + TestLightState light(&output); + setup_rgbww_light(output, light); + + // Turn on first so subsequent calls are color changes + light.make_call().set_state(true).set_brightness(1.0f).set_color_brightness(1.0f).set_transition_length(0).perform(); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + float v = static_cast(i % 256) / 255.0f; + light.make_call() + .set_color_mode(light::ColorMode::RGB_COLD_WARM_WHITE) + .set_red(v) + .set_green(1.0f - v) + .set_blue(v * 0.5f) + .set_transition_length(0) + .perform(); + } + benchmark::DoNotOptimize(light.remote_values); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(LightCall_RGBInstant); + +// --- LightCall::perform() turn on/off cycle (Home Assistant API path) --- +// HA sends color_mode explicitly since API 1.6, skipping compute_color_mode_(). + +static void LightCall_ToggleOnOff(benchmark::State &state) { + BenchLightOutput output; + TestLightState light(&output); + setup_rgbww_light(output, light); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + light.make_call() + .set_state(i % 2 == 0) + .set_color_mode(light::ColorMode::RGB_COLD_WARM_WHITE) + .set_transition_length(0) + .perform(); + } + benchmark::DoNotOptimize(light.remote_values); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(LightCall_ToggleOnOff); + +// --- LightCall::perform() turn on/off via MQTT --- +// MQTT never sends color_mode, so compute_color_mode_() runs every call. + +static void LightCall_ToggleOnOff_MQTT(benchmark::State &state) { + BenchLightOutput output; + TestLightState light(&output); + setup_rgbww_light(output, light); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + light.make_call().set_state(i % 2 == 0).set_transition_length(0).perform(); + } + benchmark::DoNotOptimize(light.remote_values); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(LightCall_ToggleOnOff_MQTT); + +// --- LightCall::perform() with color temperature via MQTT --- +// Exercises the transform_parameters_() path that converts color_temperature +// to cold/warm white fractions. MQTT never sends color_mode, so this also +// hits compute_color_mode_() every call. Modern HA avoids this path entirely +// by converting color temp to CW/WW client-side. + +static void LightCall_ColorTemperature_MQTT(benchmark::State &state) { + BenchLightOutput output; + TestLightState light(&output); + setup_rgbww_light(output, light); + + light.make_call().set_state(true).set_brightness(1.0f).set_transition_length(0).perform(); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + // Sweep through color temperature range + float ct = 153.0f + static_cast(i % 348); + light.make_call().set_color_temperature(ct).set_transition_length(0).perform(); + } + benchmark::DoNotOptimize(light.remote_values); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(LightCall_ColorTemperature_MQTT); + +// --- LightCall::perform() with 1s transition (Home Assistant API path) --- +// Exercises start_transition_() which allocates a LightTransformer. +// This is the default HA path when transition_length > 0. + +static void LightCall_Transition(benchmark::State &state) { + BenchLightOutput output; + TestLightState light(&output); + setup_rgbww_light(output, light); + + light.make_call().set_state(true).set_brightness(1.0f).set_transition_length(0).perform(); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + float v = static_cast(i % 256) / 255.0f; + light.make_call() + .set_color_mode(light::ColorMode::RGB_COLD_WARM_WHITE) + .set_red(v) + .set_green(1.0f - v) + .set_blue(v * 0.5f) + .set_transition_length(1000) + .perform(); + } + benchmark::DoNotOptimize(light.remote_values); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(LightCall_Transition); + +// --- LightCall::perform() with cold/warm white (Home Assistant API path) --- +// Mirrors what modern HA sends: explicit color_mode with direct cold_white +// and warm_white values. HA converts color temp to CW/WW client-side for +// CWWW lights (API >= 1.6), so this is the primary HA path. + +static void LightCall_ColdWarmWhite(benchmark::State &state) { + BenchLightOutput output; + TestLightState light(&output); + setup_rgbww_light(output, light); + + light.make_call().set_state(true).set_brightness(1.0f).set_transition_length(0).perform(); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + float frac = static_cast(i % 256) / 255.0f; + light.make_call() + .set_color_mode(light::ColorMode::RGB_COLD_WARM_WHITE) + .set_cold_white(1.0f - frac) + .set_warm_white(frac) + .set_transition_length(0) + .perform(); + } + benchmark::DoNotOptimize(light.remote_values); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(LightCall_ColdWarmWhite); + +// --- LightState::publish_state() with a remote values listener --- +// Measures listener notification overhead. + +static void LightPublish_WithListener(benchmark::State &state) { + BenchLightOutput output; + TestLightState light(&output); + setup_rgbww_light(output, light); + + struct TestListener : public light::LightRemoteValuesListener { + void on_light_remote_values_update() override { count_++; } + uint64_t count_{0}; + } listener; + light.add_remote_values_listener(&listener); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + light.publish_state(); + } + benchmark::DoNotOptimize(listener.count_); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(LightPublish_WithListener); + +// --- current_values_as_rgbww output conversion with gamma LUT --- +// Measures the output conversion path that real light drivers call +// from write_state() to get hardware PWM values, including gamma +// table lookups via the LUT generated by Python codegen. + +static void LightOutput_RGBWW(benchmark::State &state) { + BenchLightOutput output; + TestLightState light(&output); + setup_rgbww_light(output, light); + + light.make_call() + .set_state(true) + .set_brightness(0.8f) + .set_color_brightness(0.6f) + .set_red(1.0f) + .set_green(0.5f) + .set_blue(0.2f) + .set_cold_white(0.7f) + .set_warm_white(0.3f) + .set_transition_length(0) + .perform(); + + float r, g, b, cw, ww; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + light.current_values_as_rgbww(&r, &g, &b, &cw, &ww); + } + benchmark::DoNotOptimize(r); + benchmark::DoNotOptimize(cw); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(LightOutput_RGBWW); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/components/light/benchmark.yaml b/tests/benchmarks/components/light/benchmark.yaml new file mode 100644 index 0000000000..2b7c938581 --- /dev/null +++ b/tests/benchmarks/components/light/benchmark.yaml @@ -0,0 +1 @@ +light: From c2456409bd4bde9b793a5c6c98bebbc18f62511d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:39:19 -0400 Subject: [PATCH 055/115] [core] Improve clean-all with no arguments (#15184) --- esphome/writer.py | 10 ++++++++ tests/unit_tests/test_writer.py | 41 +++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/esphome/writer.py b/esphome/writer.py index 4aac16ffd4..06a2230118 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -476,6 +476,16 @@ def clean_all(configuration: list[str]): data_dirs.append(Path(env_data_dir)) if env_build_path := os.environ.get("ESPHOME_BUILD_PATH"): data_dirs.append(Path(env_build_path)) + if not data_dirs: + # No config files or known data dirs, check current directory + cwd_esphome = Path.cwd() / ".esphome" + if cwd_esphome.is_dir(): + data_dirs.append(cwd_esphome) + else: + _LOGGER.warning( + "No configuration files specified and no .esphome directory found in current directory. " + "Pass YAML files or a configuration directory to clean build artifacts." + ) # Clean build dir for dir in data_dirs: diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index 6ace38a7d7..940a394c08 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -990,6 +990,47 @@ def test_clean_all_ignores_empty_env_vars( assert marker.exists() +@patch("esphome.writer.CORE") +def test_clean_all_no_args_with_esphome_dir( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all with no args cleans .esphome in cwd.""" + esphome_dir = tmp_path / ".esphome" + esphome_dir.mkdir() + (esphome_dir / "dummy.txt").write_text("x") + + from esphome.writer import clean_all + + with ( + caplog.at_level("INFO"), + patch("esphome.writer.Path.cwd", return_value=tmp_path), + ): + clean_all([]) + + assert esphome_dir.exists() + assert not (esphome_dir / "dummy.txt").exists() + + +@patch("esphome.writer.CORE") +def test_clean_all_no_args_no_esphome_dir( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all with no args and no .esphome dir warns.""" + from esphome.writer import clean_all + + with ( + caplog.at_level("WARNING"), + patch("esphome.writer.Path.cwd", return_value=tmp_path), + ): + clean_all([]) + + assert "No configuration files specified" in caplog.text + + @patch("esphome.writer.CORE") def test_clean_all( mock_core: MagicMock, From bf89a191f06a4fea3c3b9014f1d46200c89fa2df Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:39:35 -0400 Subject: [PATCH 056/115] [wifi] Guard coex_background_scan with CONFIG_SOC_WIFI_SUPPORTED (#15187) --- esphome/components/wifi/wifi_component_esp_idf.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 1b80adc82e..d8b3db9667 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -989,9 +989,11 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { } // When scanning while connected (roaming), return to home channel between // each scanned channel to maintain the connection (helps with BLE/WiFi coexistence) +#ifdef CONFIG_SOC_WIFI_SUPPORTED if (this->roaming_state_ == RoamingState::SCANNING) { config.coex_background_scan = true; } +#endif esp_err_t err = esp_wifi_scan_start(&config, false); if (err != ESP_OK) { From d9ada4536cbcaacdba36ea43806753841e06d515 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:58:12 +0100 Subject: [PATCH 057/115] [nextion] Fix leading space in pressed color string commands (#15190) --- esphome/components/nextion/nextion_commands.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 2adf314a2e..4ddbfbee6a 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -106,7 +106,7 @@ void Nextion::set_component_pressed_foreground_color(const char *component, uint } void Nextion::set_component_pressed_foreground_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", " %s.pco2=%s", component, color); + this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", "%s.pco2=%s", component, color); } void Nextion::set_component_pressed_foreground_color(const char *component, Color color) { @@ -134,7 +134,7 @@ void Nextion::set_component_pressed_font_color(const char *component, uint16_t c } void Nextion::set_component_pressed_font_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", " %s.pco2=%s", component, color); + this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%s", component, color); } void Nextion::set_component_pressed_font_color(const char *component, Color color) { From 1edf952ddacb7a0ad4d7ea1d106bd340642e6c99 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 27 Mar 2026 04:59:06 +1000 Subject: [PATCH 058/115] [font] Add unit tests verifying correct processing of glyphs (#15178) --- tests/component_tests/font/.gitattributes | 2 + .../component_tests/font/NotoSans-Regular.ttf | Bin 0 -> 455188 bytes tests/component_tests/font/__init__.py | 0 tests/component_tests/font/test_font.py | 337 ++++++++++++++++++ tests/components/font/.gitattributes | 3 +- 5 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 tests/component_tests/font/.gitattributes create mode 100644 tests/component_tests/font/NotoSans-Regular.ttf create mode 100644 tests/component_tests/font/__init__.py create mode 100644 tests/component_tests/font/test_font.py diff --git a/tests/component_tests/font/.gitattributes b/tests/component_tests/font/.gitattributes new file mode 100644 index 0000000000..4df6726184 --- /dev/null +++ b/tests/component_tests/font/.gitattributes @@ -0,0 +1,2 @@ +*.pcf -text +*.ttf -text diff --git a/tests/component_tests/font/NotoSans-Regular.ttf b/tests/component_tests/font/NotoSans-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a1b8994edeacd70067de843a4691b15a0ce5921b GIT binary patch literal 455188 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
dI>|@x^aDd?;!(oP_496KxGMr{O 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

&WAr92Eg5`rwCC}mWJg|9LrV;m!6{5%0^P7W!7 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 z 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�gj|olzjY)xy*7(9C$-oV|j}f$g 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 xDKjUtq!KIxvq85wFR`Eiv1%-R3| literal 0 HcmV?d00001 diff --git a/tests/component_tests/font/__init__.py b/tests/component_tests/font/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/font/test_font.py b/tests/component_tests/font/test_font.py new file mode 100644 index 0000000000..55e27ae84c --- /dev/null +++ b/tests/component_tests/font/test_font.py @@ -0,0 +1,337 @@ +"""Tests for the font component. + +Focuses on verifying that long multi-byte (Chinese/CJK) glyph strings +are correctly processed through the font configuration pipeline. +""" + +import functools +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from esphome.components.font import ( + CONF_BPP, + CONF_EXTRAS, + CONF_GLYPHSETS, + CONF_IGNORE_MISSING_GLYPHS, + CONF_RAW_GLYPH_ID, + FONT_CACHE, + flatten, + glyph_comparator, + to_code, + validate_font_config, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_FILE, + CONF_GLYPHS, + CONF_ID, + CONF_PATH, + CONF_RAW_DATA_ID, + CONF_SIZE, + CONF_TYPE, +) + +FONT_DIR = Path(__file__).parent +FONT_PATH = FONT_DIR / "NotoSans-Regular.ttf" + +# 200 unique CJK Unified Ideograph characters (U+4E00..U+4EC7) +CHINESE_200 = "".join(chr(cp) for cp in range(0x4E00, 0x4EC8)) + + +def _file_conf() -> dict: + return {CONF_PATH: str(FONT_PATH), CONF_TYPE: "local"} + + +def _make_config( + glyphs: list[str], + *, + ignore_missing: bool = False, + size: int = 20, + bpp: int = 1, + extras: list | None = None, + glyphsets: list | None = None, +) -> dict: + """Build a config dict matching what FONT_SCHEMA produces.""" + return { + CONF_FILE: _file_conf(), + CONF_GLYPHS: glyphs, + CONF_GLYPHSETS: glyphsets or [], + CONF_IGNORE_MISSING_GLYPHS: ignore_missing, + CONF_SIZE: size, + CONF_BPP: bpp, + CONF_EXTRAS: extras or [], + } + + +@pytest.fixture(autouse=True) +def _load_font(): + """Load the test font into FONT_CACHE and clean up afterwards.""" + fc = _file_conf() + FONT_CACHE[fc] = FONT_PATH + yield + FONT_CACHE.store.clear() + + +# ---------- flatten / glyph_comparator helpers ---------- + + +def test_flatten_splits_chinese_string_into_chars(): + """A single string of 200 Chinese characters must become 200 individual chars.""" + result = flatten([CHINESE_200]) + assert len(result) == 200 + assert all(len(c) == 1 for c in result) + assert result[0] == "\u4e00" + assert result[-1] == "\u4ec7" + + +def test_flatten_multiple_chinese_strings(): + """Multiple glyph strings are concatenated then split correctly.""" + s1 = CHINESE_200[:100] + s2 = CHINESE_200[100:] + result = flatten([list(s1), list(s2)]) + assert len(result) == 200 + + +def test_glyph_comparator_orders_chinese_by_utf8(): + """glyph_comparator must order CJK characters by their UTF-8 byte sequence.""" + chars = list(CHINESE_200[:10]) + sorted_chars = sorted(chars, key=functools.cmp_to_key(glyph_comparator)) + # CJK block is contiguous and UTF-8 order matches codepoint order here + assert sorted_chars == chars + + +def test_glyph_comparator_mixed_ascii_and_chinese(): + """ASCII characters sort before CJK characters (lower UTF-8 bytes).""" + assert glyph_comparator("A", "\u4e00") == -1 + assert glyph_comparator("\u4e00", "A") == 1 + assert glyph_comparator("\u4e00", "\u4e00") == 0 + + +# ---------- validate_font_config ---------- + + +def test_long_chinese_glyphs_raises_missing_error(): + """200 Chinese chars not present in NotoSans must raise Invalid with the correct count.""" + config = _make_config([CHINESE_200]) + with pytest.raises(cv.Invalid, match=r"missing 200 glyphs"): + validate_font_config(config) + + +def test_long_chinese_glyphs_error_mentions_overflow(): + """When more than 10 glyphs are missing the error should mention the remainder.""" + config = _make_config([CHINESE_200]) + with pytest.raises(cv.Invalid, match=r"and 190 more"): + validate_font_config(config) + + +def test_duplicate_chinese_glyphs_detected(): + """Duplicate CJK characters within a single glyph string must be caught.""" + duped = "\u4e00\u4e01\u4e00" # first char repeated + config = _make_config([duped]) + with pytest.raises(cv.Invalid, match="duplicate"): + validate_font_config(config) + + +def test_duplicate_chinese_across_strings(): + """Duplicates across separate glyph strings are also caught.""" + config = _make_config(["\u4e00\u4e01", "\u4e01\u4e02"]) + with pytest.raises(cv.Invalid, match="duplicate"): + validate_font_config(config) + + +def test_no_false_duplicates_in_200_unique_chinese(): + """200 unique CJK characters must not trigger the duplicate check.""" + config = _make_config([CHINESE_200]) + # Should not raise duplicate error — it should reach the missing-glyph check instead + with pytest.raises(cv.Invalid, match="missing"): + validate_font_config(config) + + +def test_valid_latin_glyphs_pass_validation(): + """Latin characters present in NotoSans-Regular pass validation without error.""" + config = _make_config(["ABCabc123"]) + result = validate_font_config(config) + assert result is not None + assert result[CONF_SIZE] == 20 + + +def test_long_latin_glyphs_pass_validation(): + """A long string of supported Latin glyphs passes validation.""" + # 95 printable ASCII characters that NotoSans supports + latin = "".join(chr(cp) for cp in range(0x21, 0x7F)) + config = _make_config([latin]) + result = validate_font_config(config) + assert result is not None + + +def test_mixed_latin_and_chinese_glyphs_error(): + """Mixing valid Latin and invalid Chinese chars reports missing Chinese glyphs.""" + chinese_10 = CHINESE_200[:10] + config = _make_config(["ABC", chinese_10]) + with pytest.raises(cv.Invalid, match=r"missing 10 glyphs"): + validate_font_config(config) + + +def test_single_chinese_char_glyph(): + """A single Chinese character is correctly handled as one glyph.""" + config = _make_config(["\u4e00"]) + with pytest.raises(cv.Invalid, match=r"missing 1 glyph[^s]"): + validate_font_config(config) + + +def test_chinese_glyphs_as_individual_list_items(): + """Chinese chars provided as separate list items are handled the same as a single string.""" + chars_as_list = list(CHINESE_200[:50]) + config = _make_config(chars_as_list) + with pytest.raises(cv.Invalid, match=r"missing 50 glyphs"): + validate_font_config(config) + + +# ---------- YAML parsing ---------- + + +def test_yaml_long_latin_glyphs_parsed_and_validated(tmp_path): + """200 Latin Extended chars on a single YAML line are parsed intact and pass validation.""" + from esphome.yaml_util import load_yaml + + latin_long = "".join(chr(cp) for cp in range(0x100, 0x1C8)) + yaml_file = tmp_path / "font_test.yaml" + yaml_file.write_text( + f'font:\n - file: "NotoSans-Regular.ttf"\n glyphs: "{latin_long}"\n', + encoding="utf-8", + ) + + parsed = load_yaml(yaml_file) + raw_glyphs = parsed["font"][0]["glyphs"] + + # YAML must preserve every Unicode character on the single line + assert raw_glyphs == latin_long + assert len(raw_glyphs) == 200 + + # Feed through validate_font_config to confirm all glyphs are accepted + config = _make_config([raw_glyphs]) + result = validate_font_config(config) + assert result is not None + + +@pytest.mark.parametrize( + "glyphs_str", + [ + " ABC", # space at start + "AB CD", # space in middle + "ABC ", # space at end + ], + ids=["start", "middle", "end"], +) +def test_yaml_space_in_glyphs_preserved(tmp_path, glyphs_str): + """A space character in a glyphs string must survive YAML round-trip and validation.""" + from esphome.yaml_util import load_yaml + + yaml_file = tmp_path / "font_test.yaml" + yaml_file.write_text( + f'font:\n - file: "NotoSans-Regular.ttf"\n glyphs: "{glyphs_str}"\n', + encoding="utf-8", + ) + + parsed = load_yaml(yaml_file) + raw_glyphs = parsed["font"][0]["glyphs"] + + assert raw_glyphs == glyphs_str + assert " " in raw_glyphs + + # Space and ASCII letters are all in NotoSans — validation must pass + config = _make_config([raw_glyphs]) + result = validate_font_config(config) + assert result is not None + + +# ---------- to_code generation ---------- + + +# 200 unique Latin Extended characters (U+0100..U+01C7), all present in NotoSans +LATIN_LONG = "".join(chr(cp) for cp in range(0x100, 0x1C8)) + + +@pytest.fixture +def mock_cg(): + """Mock all cg codegen functions used by to_code.""" + with ( + patch("esphome.components.font.cg.add_define") as mock_define, + patch("esphome.components.font.cg.progmem_array") as mock_progmem, + patch("esphome.components.font.cg.static_const_array") as mock_static, + patch("esphome.components.font.cg.new_Pvariable") as mock_new_pvar, + ): + mock_progmem.return_value = MagicMock() + mock_static.return_value = MagicMock() + yield { + "add_define": mock_define, + "progmem_array": mock_progmem, + "static_const_array": mock_static, + "new_Pvariable": mock_new_pvar, + } + + +@pytest.mark.asyncio +async def test_to_code_long_latin_generates_all_glyphs(mock_cg): + """to_code must generate glyph data for every character in a long Latin string.""" + glyph_count = len(LATIN_LONG) # 200 + config = _make_config([LATIN_LONG]) + config[CONF_ID] = MagicMock() + config[CONF_RAW_DATA_ID] = MagicMock() + config[CONF_RAW_GLYPH_ID] = MagicMock() + + await to_code(config) + + # USE_FONT define must be emitted + mock_cg["add_define"].assert_any_call("USE_FONT") + + # progmem_array receives the combined bitmap data (non-empty) + mock_cg["progmem_array"].assert_called_once() + bitmap_data = mock_cg["progmem_array"].call_args.args[1] + assert len(bitmap_data) > 0 + + # static_const_array receives one entry per unique glyph + mock_cg["static_const_array"].assert_called_once() + glyph_initializer = mock_cg["static_const_array"].call_args.args[1] + assert len(glyph_initializer) == glyph_count + + # new_Pvariable is called with the correct glyph count + mock_cg["new_Pvariable"].assert_called_once() + pvar_args = mock_cg["new_Pvariable"].call_args.args + assert pvar_args[2] == glyph_count # len(glyph_initializer) + assert pvar_args[8] == 1 # bpp + + +@pytest.mark.asyncio +async def test_to_code_glyph_entries_contain_expected_fields(mock_cg): + """Each glyph initializer entry must have 7 fields: codepoint, data ptr, advance, offset_x, offset_y, w, h.""" + config = _make_config([LATIN_LONG]) + config[CONF_ID] = MagicMock() + config[CONF_RAW_DATA_ID] = MagicMock() + config[CONF_RAW_GLYPH_ID] = MagicMock() + + await to_code(config) + + glyph_initializer = mock_cg["static_const_array"].call_args.args[1] + for entry in glyph_initializer: + assert len(entry) == 7, f"Glyph entry should have 7 fields, got {len(entry)}" + codepoint = entry[0] + assert isinstance(codepoint, int) + assert 0x100 <= codepoint <= 0x1C7 + + +@pytest.mark.asyncio +async def test_to_code_glyphs_sorted_by_utf8(mock_cg): + """Glyphs in the initializer must be sorted by UTF-8 byte order.""" + config = _make_config([LATIN_LONG]) + config[CONF_ID] = MagicMock() + config[CONF_RAW_DATA_ID] = MagicMock() + config[CONF_RAW_GLYPH_ID] = MagicMock() + + await to_code(config) + + glyph_initializer = mock_cg["static_const_array"].call_args.args[1] + codepoints = [entry[0] for entry in glyph_initializer] + assert codepoints == sorted(codepoints) diff --git a/tests/components/font/.gitattributes b/tests/components/font/.gitattributes index 63ab00e9f2..4df6726184 100644 --- a/tests/components/font/.gitattributes +++ b/tests/components/font/.gitattributes @@ -1 +1,2 @@ -*.pcf -text +*.pcf -text +*.ttf -text From a008c27fcfc8e1f4fd1241ce4597eef4ea0be15b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Mar 2026 09:01:08 -1000 Subject: [PATCH 059/115] [climate] Avoid duplicate get_traits() in publish_state (#15181) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/climate/climate.cpp | 5 ++--- esphome/components/climate/climate.h | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 5cbe9a5daf..32cac0961c 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -367,7 +367,7 @@ optional Climate::restore_state_() { return recovered; } -void Climate::save_state_() { +void Climate::save_state_(const ClimateTraits &traits) { #if (defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \ !defined(CLANG_TIDY) #pragma GCC diagnostic ignored "-Wclass-memaccess" @@ -382,7 +382,6 @@ void Climate::save_state_() { #endif state.mode = this->mode; - auto traits = this->get_traits(); if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { state.target_temperature_low = this->target_temperature_low; @@ -480,7 +479,7 @@ void Climate::publish_state() { ControllerRegistry::notify_climate_update(this); #endif // Save state - this->save_state_(); + this->save_state_(traits); } ClimateTraits Climate::get_traits() { diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index e2cb743c0a..0251365dd8 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -335,7 +335,8 @@ class Climate : public EntityBase { /** Internal method to save the state of the climate device to recover memory. This is automatically * called from publish_state() */ - void save_state_(); + void save_state_(const ClimateTraits &traits); + void save_state_() { this->save_state_(this->traits()); } void dump_traits_(const char *tag); From 1e2c410abfae7a1c1a78cfff62c9507f307b582f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 09:47:18 -1000 Subject: [PATCH 060/115] Bump cryptography from 46.0.5 to 46.0.6 (#15193) 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 ce735f398a..c74dd265c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -cryptography==46.0.5 +cryptography==46.0.6 voluptuous==0.16.0 PyYAML==6.0.3 paho-mqtt==1.6.1 From 3152642571a32108c87e90390722808c65533b4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 09:48:06 -1000 Subject: [PATCH 061/115] Bump codecov/codecov-action from 5.5.3 to 6.0.0 (#15194) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 965e23870d..ab7a750388 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -154,7 +154,7 @@ jobs: . venv/bin/activate pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ - name: Upload coverage to Codecov - uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} - name: Save Python virtual environment cache From 81f0aa1168b8b993451b3673f28bd57763148cbe Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:54:50 +0100 Subject: [PATCH 062/115] [nextion] Replace `or`/`and` operators and missing `this->` (#15191) --- esphome/components/nextion/nextion.cpp | 2 +- .../components/nextion/nextion_upload_arduino.cpp | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index ac17e14312..612bfbc968 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -90,7 +90,7 @@ bool Nextion::check_connect_() { #endif // NEXTION_PROTOCOL_LOG ESP_LOGW(TAG, "Not connected"); - comok_sent_ = 0; + this->comok_sent_ = 0; return false; } diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index 6c454ab745..f59b708002 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -22,9 +22,9 @@ static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16; int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { uint32_t range_size = this->tft_size_ - range_start; ESP_LOGV(TAG, "Heap: %" PRIu32, EspClass::getFreeHeap()); - uint32_t range_end = ((upload_first_chunk_sent_ or this->tft_size_ < 4096) ? this->tft_size_ : 4096) - 1; + uint32_t range_end = ((this->upload_first_chunk_sent_ || this->tft_size_ < 4096) ? this->tft_size_ : 4096) - 1; ESP_LOGD(TAG, "Range start: %" PRIu32, range_start); - if (range_size <= 0 or range_end <= range_start) { + if (range_size <= 0 || range_end <= range_start) { ESP_LOGE(TAG, "Invalid range end: %" PRIu32 ", size: %" PRIu32, range_end, range_size); return -1; } @@ -34,7 +34,7 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { ESP_LOGV(TAG, "Range: %s", range_header); http_client.addHeader("Range", range_header); int code = http_client.GET(); - if (code != HTTP_CODE_OK and code != HTTP_CODE_PARTIAL_CONTENT) { + if (code != HTTP_CODE_OK && code != HTTP_CODE_PARTIAL_CONTENT) { ESP_LOGW(TAG, "HTTP failed: %s", HTTPClient::errorToString(code).c_str()); return -1; } @@ -80,12 +80,12 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { recv_string.clear(); this->write_array(buffer, buffer_size); App.feed_wdt(); - this->recv_ret_string_(recv_string, upload_first_chunk_sent_ ? 500 : 5000, true); + this->recv_ret_string_(recv_string, this->upload_first_chunk_sent_ ? 500 : 5000, true); this->content_length_ -= read_len; const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_; ESP_LOGD(TAG, "Upload: %0.2f%% (%" PRIu32 " left, heap: %" PRIu32 ")", upload_percentage, this->content_length_, EspClass::getFreeHeap()); - upload_first_chunk_sent_ = true; + this->upload_first_chunk_sent_ = true; if (recv_string.empty()) { ESP_LOGW(TAG, "No response from display during upload"); allocator.deallocate(buffer, 4096); @@ -112,7 +112,7 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { allocator.deallocate(buffer, 4096); buffer = nullptr; return range_end + 1; - } else if (recv_string[0] != 0x05 and recv_string[0] != 0x08) { // 0x05 == "ok" + } else if (recv_string[0] != 0x05 && recv_string[0] != 0x08) { // 0x05 == "ok" char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)]; ESP_LOGE( TAG, "Invalid response: [%s]", @@ -214,7 +214,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ++tries; } - if (code != 200 and code != 206) { + if (code != 200 && code != 206) { ESP_LOGE(TAG, "HTTP request failed with status %d", code); return this->upload_end_(false); } From 6aafb521c15bf9a873f71392a445b1f7983234ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:59:21 +0000 Subject: [PATCH 063/115] Bump ruff from 0.15.7 to 0.15.8 (#15192) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5e2bfe09ce..f4729f211c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.6 + rev: v0.15.8 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 1440b20333..3b277e214d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.7 # also change in .pre-commit-config.yaml when updating +ruff==0.15.8 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From fa8a609bcc1939ec4f9d7b54dc89b54bfc8ff23a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Mar 2026 13:50:50 -1000 Subject: [PATCH 064/115] [automation] Eliminate trigger trampolines with deduplicated forwarder structs (#15174) --- esphome/automation.py | 44 +++++++++++ esphome/components/binary_sensor/__init__.py | 61 +++++---------- esphome/components/button/__init__.py | 16 +--- esphome/components/event/__init__.py | 14 +--- esphome/components/number/__init__.py | 14 +--- esphome/components/sensor/__init__.py | 34 +++----- esphome/components/switch/__init__.py | 48 +++--------- esphome/components/text_sensor/__init__.py | 38 +++------ esphome/core/automation.h | 42 +++++++++- tests/unit_tests/test_automation.py | 81 +++++++++++++++++++- 10 files changed, 226 insertions(+), 166 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index 17966dc782..7b1d6ceca1 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -137,6 +137,9 @@ UpdateComponentAction = cg.esphome_ns.class_("UpdateComponentAction", Action) SuspendComponentAction = cg.esphome_ns.class_("SuspendComponentAction", Action) ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action) Automation = cg.esphome_ns.class_("Automation") +TriggerForwarder = cg.esphome_ns.class_("TriggerForwarder") +TriggerOnTrueForwarder = cg.esphome_ns.class_("TriggerOnTrueForwarder") +TriggerOnFalseForwarder = cg.esphome_ns.class_("TriggerOnFalseForwarder") LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition) StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition) @@ -661,3 +664,44 @@ async def build_automation( actions = await build_action_list(config[CONF_THEN], templ, args) cg.add(obj.add_actions(actions)) return obj + + +async def build_callback_automation( + parent: MockObj, + callback_method: str, + args: TemplateArgsType, + config: ConfigType, + forwarder: MockObj | MockObjClass | None = None, +) -> None: + """Build an Automation and register it as a callback on the parent. + + Eliminates the need for a Trigger wrapper object by registering the + automation's trigger() directly as a callback on the parent component. + + Uses template forwarder structs so the compiler deduplicates the operator() + body across all call sites with the same signature. The forwarder must be + pointer-sized (single Automation* field) to fit inline in Callback::ctx_ + and avoid heap allocation. + + :param parent: The component object (e.g., button, sensor). + :param callback_method: Name of the callback method (e.g., "add_on_press_callback"). + :param args: Automation template args as list of (type, name) tuples. + :param config: The automation config dict. + :param forwarder: Optional forwarder type to use instead of the default + TriggerForwarder. Pass any struct type whose aggregate init takes + a single Automation pointer (e.g., TriggerOnTrueForwarder). + """ + arg_types = [arg[0] for arg in args] + templ = cg.TemplateArguments(*arg_types) + obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ) + actions = await build_action_list(config[CONF_THEN], templ, args) + cg.add(obj.add_actions(actions)) + # Use template forwarder structs for deduplication. The compiler generates + # one operator() per forwarder type; different automation pointers are just + # data in the struct. + if forwarder is None: + forwarder = TriggerForwarder.template(templ) + # RawExpression for aggregate init — both forwarder and obj are codegen + # MockObjs (not user input), and there's no Expression type for positional + # aggregate initialization (StructInitializer uses named fields). + cg.add(getattr(parent, callback_method)(cg.RawExpression(f"{forwarder}{{{obj}}}"))) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 37cccc01be..4705f1675d 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -120,10 +120,6 @@ BinarySensorInitiallyOff = binary_sensor_ns.class_( BinarySensorPtr = BinarySensor.operator("ptr") # Triggers -PressTrigger = binary_sensor_ns.class_("PressTrigger", automation.Trigger.template()) -ReleaseTrigger = binary_sensor_ns.class_( - "ReleaseTrigger", automation.Trigger.template() -) ClickTrigger = binary_sensor_ns.class_("ClickTrigger", automation.Trigger.template()) DoubleClickTrigger = binary_sensor_ns.class_( "DoubleClickTrigger", automation.Trigger.template() @@ -132,13 +128,6 @@ MultiClickTrigger = binary_sensor_ns.class_( "MultiClickTrigger", automation.Trigger.template(), cg.Component ) MultiClickTriggerEvent = binary_sensor_ns.struct("MultiClickTriggerEvent") -StateTrigger = binary_sensor_ns.class_( - "StateTrigger", automation.Trigger.template(bool) -) -StateChangeTrigger = binary_sensor_ns.class_( - "StateChangeTrigger", - automation.Trigger.template(cg.optional.template(bool), cg.optional.template(bool)), -) BinarySensorPublishAction = binary_sensor_ns.class_( "BinarySensorPublishAction", automation.Action @@ -458,16 +447,8 @@ _BINARY_SENSOR_SCHEMA = ( ): cv.boolean, cv.Optional(CONF_DEVICE_CLASS): validate_device_class, cv.Optional(CONF_FILTERS): validate_filters, - cv.Optional(CONF_ON_PRESS): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PressTrigger), - } - ), - cv.Optional(CONF_ON_RELEASE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReleaseTrigger), - } - ), + cv.Optional(CONF_ON_PRESS): automation.validate_automation({}), + cv.Optional(CONF_ON_RELEASE): automation.validate_automation({}), cv.Optional(CONF_ON_CLICK): cv.All( automation.validate_automation( { @@ -509,16 +490,8 @@ _BINARY_SENSOR_SCHEMA = ( ): cv.positive_time_period_milliseconds, } ), - cv.Optional(CONF_ON_STATE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), - } - ), - cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateChangeTrigger), - } - ), + cv.Optional(CONF_ON_STATE): automation.validate_automation({}), + cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation({}), } ) ) @@ -556,13 +529,14 @@ def binary_sensor_schema( @coroutine_with_priority(CoroPriority.AUTOMATION) async def _build_binary_sensor_automations(var, config): - for conf in config.get(CONF_ON_PRESS, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - - for conf in config.get(CONF_ON_RELEASE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + for conf_key, forwarder in ( + (CONF_ON_PRESS, automation.TriggerOnTrueForwarder), + (CONF_ON_RELEASE, automation.TriggerOnFalseForwarder), + ): + for conf in config.get(conf_key, []): + await automation.build_callback_automation( + var, "add_on_state_callback", [], conf, forwarder=forwarder + ) for conf in config.get(CONF_ON_CLICK, []): trigger = cg.new_Pvariable( @@ -593,13 +567,14 @@ async def _build_binary_sensor_automations(var, config): await automation.build_automation(trigger, [], conf) for conf in config.get(CONF_ON_STATE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(bool, "x")], conf) + await automation.build_callback_automation( + var, "add_on_state_callback", [(bool, "x")], conf + ) for conf in config.get(CONF_ON_STATE_CHANGE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, + await automation.build_callback_automation( + var, + "add_full_state_callback", [ (cg.optional.template(bool), "x_previous"), (cg.optional.template(bool), "x"), diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index 12d9ebaba6..f279b6ffe3 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -10,7 +10,6 @@ from esphome.const import ( CONF_ID, CONF_MQTT_ID, CONF_ON_PRESS, - CONF_TRIGGER_ID, CONF_WEB_SERVER, DEVICE_CLASS_EMPTY, DEVICE_CLASS_IDENTIFY, @@ -41,10 +40,6 @@ ButtonPtr = Button.operator("ptr") PressAction = button_ns.class_("PressAction", automation.Action) -ButtonPressTrigger = button_ns.class_( - "ButtonPressTrigger", automation.Trigger.template() -) - validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") @@ -55,11 +50,7 @@ _BUTTON_SCHEMA = ( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTButtonComponent), cv.Optional(CONF_DEVICE_CLASS): validate_device_class, - cv.Optional(CONF_ON_PRESS): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ButtonPressTrigger), - } - ), + cv.Optional(CONF_ON_PRESS): automation.validate_automation({}), } ) ) @@ -91,8 +82,9 @@ def button_schema( @setup_entity("button") async def setup_button_core_(var, config): for conf in config.get(CONF_ON_PRESS, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_press_callback", [], conf + ) setup_device_class(config) diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index 300902b8ca..527bb4ebba 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -10,7 +10,6 @@ from esphome.const import ( CONF_ID, CONF_MQTT_ID, CONF_ON_EVENT, - CONF_TRIGGER_ID, CONF_WEB_SERVER, DEVICE_CLASS_BUTTON, DEVICE_CLASS_DOORBELL, @@ -41,8 +40,6 @@ EventPtr = Event.operator("ptr") TriggerEventAction = event_ns.class_("TriggerEventAction", automation.Action) -EventTrigger = event_ns.class_("EventTrigger", automation.Trigger.template()) - validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") _EVENT_SCHEMA = ( @@ -53,11 +50,7 @@ _EVENT_SCHEMA = ( cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTEventComponent), cv.GenerateID(): cv.declare_id(Event), cv.Optional(CONF_DEVICE_CLASS): validate_device_class, - cv.Optional(CONF_ON_EVENT): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(EventTrigger), - } - ), + cv.Optional(CONF_ON_EVENT): automation.validate_automation({}), } ) ) @@ -92,8 +85,9 @@ def event_schema( @setup_entity("event") async def setup_event_core_(var, config, *, event_types: list[str]): for conf in config.get(CONF_ON_EVENT, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.StringRef, "event_type")], conf) + await automation.build_callback_automation( + var, "add_on_event_callback", [(cg.StringRef, "event_type")], conf + ) cg.add(var.set_event_types(event_types)) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 0570ac0b1e..90f9fe1835 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -155,9 +155,6 @@ Number = number_ns.class_("Number", cg.EntityBase) NumberPtr = Number.operator("ptr") # Triggers -NumberStateTrigger = number_ns.class_( - "NumberStateTrigger", automation.Trigger.template(cg.float_) -) ValueRangeTrigger = number_ns.class_( "ValueRangeTrigger", automation.Trigger.template(cg.float_), cg.Component ) @@ -198,11 +195,7 @@ _NUMBER_SCHEMA = ( .extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent), - cv.Optional(CONF_ON_VALUE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(NumberStateTrigger), - } - ), + cv.Optional(CONF_ON_VALUE): automation.validate_automation({}), cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger), @@ -248,8 +241,9 @@ def number_schema( @coroutine_with_priority(CoroPriority.AUTOMATION) async def _build_number_automations(var, config): for conf in config.get(CONF_ON_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "x")], conf) + await automation.build_callback_automation( + var, "add_on_state_callback", [(float, "x")], conf + ) for conf in config.get(CONF_ON_VALUE_RANGE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await cg.register_component(trigger, conf) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 9f3c1484b0..19d03a0afc 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -238,12 +238,6 @@ Sensor = sensor_ns.class_("Sensor", cg.EntityBase) SensorPtr = Sensor.operator("ptr") # Triggers -SensorStateTrigger = sensor_ns.class_( - "SensorStateTrigger", automation.Trigger.template(cg.float_) -) -SensorRawStateTrigger = sensor_ns.class_( - "SensorRawStateTrigger", automation.Trigger.template(cg.float_) -) ValueRangeTrigger = sensor_ns.class_( "ValueRangeTrigger", automation.Trigger.template(cg.float_), cg.Component ) @@ -316,18 +310,8 @@ _SENSOR_SCHEMA = ( cv.Any(None, cv.positive_time_period_milliseconds), ), cv.Optional(CONF_FILTERS): validate_filters, - cv.Optional(CONF_ON_VALUE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SensorStateTrigger), - } - ), - cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - SensorRawStateTrigger - ), - } - ), + cv.Optional(CONF_ON_VALUE): automation.validate_automation({}), + cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation({}), cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger), @@ -897,12 +881,14 @@ async def build_filters(config): @coroutine_with_priority(CoroPriority.AUTOMATION) async def _build_sensor_automations(var, config): - for conf in config.get(CONF_ON_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "x")], conf) - for conf in config.get(CONF_ON_RAW_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "x")], conf) + for conf_key, callback in ( + (CONF_ON_VALUE, "add_on_state_callback"), + (CONF_ON_RAW_VALUE, "add_on_raw_state_callback"), + ): + for conf in config.get(conf_key, []): + await automation.build_callback_automation( + var, callback, [(float, "x")], conf + ) for conf in config.get(CONF_ON_VALUE_RANGE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await cg.register_component(trigger, conf) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index bbafc54bd1..c4dd4856e3 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -15,7 +15,6 @@ from esphome.const import ( CONF_ON_TURN_ON, CONF_RESTORE_MODE, CONF_STATE, - CONF_TRIGGER_ID, CONF_WEB_SERVER, DEVICE_CLASS_EMPTY, DEVICE_CLASS_OUTLET, @@ -61,17 +60,6 @@ TurnOnAction = switch_ns.class_("TurnOnAction", automation.Action) SwitchPublishAction = switch_ns.class_("SwitchPublishAction", automation.Action) SwitchCondition = switch_ns.class_("SwitchCondition", Condition) -SwitchStateTrigger = switch_ns.class_( - "SwitchStateTrigger", automation.Trigger.template(bool) -) -SwitchTurnOnTrigger = switch_ns.class_( - "SwitchTurnOnTrigger", automation.Trigger.template() -) -SwitchTurnOffTrigger = switch_ns.class_( - "SwitchTurnOffTrigger", automation.Trigger.template() -) - - validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True) @@ -86,21 +74,9 @@ _SWITCH_SCHEMA = ( cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum( RESTORE_MODES, upper=True, space="_" ), - cv.Optional(CONF_ON_STATE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchStateTrigger), - } - ), - cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOnTrigger), - } - ), - cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOffTrigger), - } - ), + cv.Optional(CONF_ON_STATE): automation.validate_automation({}), + cv.Optional(CONF_ON_TURN_ON): automation.validate_automation({}), + cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation({}), cv.Optional(CONF_DEVICE_CLASS): validate_device_class, } ) @@ -147,15 +123,15 @@ def switch_schema( @coroutine_with_priority(CoroPriority.AUTOMATION) async def _build_switch_automations(var, config): - for conf in config.get(CONF_ON_STATE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(bool, "x")], 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, args, forwarder in ( + (CONF_ON_STATE, [(bool, "x")], None), + (CONF_ON_TURN_ON, [], automation.TriggerOnTrueForwarder), + (CONF_ON_TURN_OFF, [], automation.TriggerOnFalseForwarder), + ): + for conf in config.get(conf_key, []): + await automation.build_callback_automation( + var, "add_on_state_callback", args, conf, forwarder=forwarder + ) @setup_entity("switch") diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 97f394ecf7..51eedf9a95 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -14,7 +14,6 @@ from esphome.const import ( CONF_ON_VALUE, CONF_STATE, CONF_TO, - CONF_TRIGGER_ID, CONF_WEB_SERVER, DEVICE_CLASS_DATE, DEVICE_CLASS_EMPTY, @@ -42,12 +41,6 @@ text_sensor_ns = cg.esphome_ns.namespace("text_sensor") TextSensor = text_sensor_ns.class_("TextSensor", cg.EntityBase) TextSensorPtr = TextSensor.operator("ptr") -TextSensorStateTrigger = text_sensor_ns.class_( - "TextSensorStateTrigger", automation.Trigger.template(cg.std_string) -) -TextSensorStateRawTrigger = text_sensor_ns.class_( - "TextSensorStateRawTrigger", automation.Trigger.template(cg.std_string) -) TextSensorPublishAction = text_sensor_ns.class_( "TextSensorPublishAction", automation.Action ) @@ -150,20 +143,8 @@ _TEXT_SENSOR_SCHEMA = ( cv.GenerateID(): cv.declare_id(TextSensor), cv.Optional(CONF_DEVICE_CLASS): validate_device_class, cv.Optional(CONF_FILTERS): validate_filters, - cv.Optional(CONF_ON_VALUE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - TextSensorStateTrigger - ), - } - ), - cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - TextSensorStateRawTrigger - ), - } - ), + cv.Optional(CONF_ON_VALUE): automation.validate_automation({}), + cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation({}), } ) ) @@ -203,13 +184,14 @@ async def build_filters(config): @coroutine_with_priority(CoroPriority.AUTOMATION) async def _build_text_sensor_automations(var, config): - for conf in config.get(CONF_ON_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) - - for conf in config.get(CONF_ON_RAW_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + for conf_key, callback in ( + (CONF_ON_VALUE, "add_on_state_callback"), + (CONF_ON_RAW_VALUE, "add_on_raw_state_callback"), + ): + for conf in config.get(conf_key, []): + await automation.build_callback_automation( + var, callback, [(cg.std_string, "x")], conf + ) @setup_entity("text_sensor") diff --git a/esphome/core/automation.h b/esphome/core/automation.h index ca4a2c8b6b..fc2cad99be 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -470,7 +470,9 @@ template class ActionList { template class Automation { public: - explicit Automation(Trigger *trigger) : trigger_(trigger) { this->trigger_->set_automation_parent(this); } + /// Default constructor for use with TriggerForwarder (no Trigger object needed). + Automation() = default; + explicit Automation(Trigger *trigger) { trigger->set_automation_parent(this); } void add_action(Action *action) { this->actions_.add_action(action); } void add_actions(const std::initializer_list *> &actions) { this->actions_.add_actions(actions); } @@ -487,8 +489,44 @@ template class Automation { int num_running() { return this->actions_.num_running(); } protected: - Trigger *trigger_; ActionList actions_; }; +/// Callback forwarder that triggers an Automation directly. +/// One operator() instantiation per Automation signature, shared across all call sites. +/// Must stay pointer-sized to fit inline in Callback::ctx_ without heap allocation. +template struct TriggerForwarder { + Automation *automation; + void operator()(const Ts &...args) const { this->automation->trigger(args...); } +}; + +/// Callback forwarder that triggers an Automation<> only when the bool arg is true. +/// Must stay pointer-sized to fit inline in Callback::ctx_ without heap allocation. +struct TriggerOnTrueForwarder { + Automation<> *automation; + void operator()(bool state) const { + if (state) + this->automation->trigger(); + } +}; + +/// Callback forwarder that triggers an Automation<> only when the bool arg is false. +/// Must stay pointer-sized to fit inline in Callback::ctx_ without heap allocation. +struct TriggerOnFalseForwarder { + Automation<> *automation; + void operator()(bool state) const { + if (!state) + this->automation->trigger(); + } +}; + +// Ensure forwarders fit in Callback::ctx_ (pointer-sized inline storage). +// If these fail, the forwarder would heap-allocate in Callback::create(). +static_assert(sizeof(TriggerForwarder<>) <= sizeof(void *)); +static_assert(sizeof(TriggerOnTrueForwarder) <= sizeof(void *)); +static_assert(sizeof(TriggerOnFalseForwarder) <= sizeof(void *)); +static_assert(std::is_trivially_copyable_v>); +static_assert(std::is_trivially_copyable_v); +static_assert(std::is_trivially_copyable_v); + } // namespace esphome diff --git a/tests/unit_tests/test_automation.py b/tests/unit_tests/test_automation.py index 61fef8201d..37779f23e6 100644 --- a/tests/unit_tests/test_automation.py +++ b/tests/unit_tests/test_automation.py @@ -5,7 +5,13 @@ from unittest.mock import patch import pytest -from esphome.automation import has_non_synchronous_actions +from esphome.automation import ( + TriggerForwarder, + TriggerOnFalseForwarder, + TriggerOnTrueForwarder, + has_non_synchronous_actions, +) +from esphome.cpp_generator import MockObj, RawExpression from esphome.util import RegistryEntry @@ -175,3 +181,76 @@ def test_has_non_synchronous_actions_dict_input( """Direct dict input (single action).""" assert has_non_synchronous_actions({"delay": "1s"}) is True assert has_non_synchronous_actions({"logger.log": "hello"}) is False + + +def _build_forwarder( + automation_name: str, + args: list[tuple[str, str]], + forwarder: MockObj | None = None, +) -> str: + """Build a trigger forwarder expression the same way build_callback_automation does. + + Mirrors the forwarder selection logic in automation.build_callback_automation. + """ + import esphome.codegen as cg + + obj = MockObj(automation_name, "->") + if forwarder is None: + arg_types = [RawExpression(t) for t, _ in args] + templ = ( + cg.TemplateArguments(*arg_types) if arg_types else cg.TemplateArguments() + ) + forwarder = TriggerForwarder.template(templ) + return f"{forwarder}{{{obj}}}" + + +def test_trigger_forwarder_no_args() -> None: + """Button on_press: TriggerForwarder<> with no args.""" + result = _build_forwarder("auto_1", []) + assert result == "TriggerForwarder<>{auto_1}" + + +def test_trigger_forwarder_single_float_arg() -> None: + """Sensor on_value: TriggerForwarder.""" + result = _build_forwarder("auto_1", [("float", "x")]) + assert result == "TriggerForwarder{auto_1}" + + +def test_trigger_forwarder_single_bool_arg() -> None: + """Switch on_state: TriggerForwarder.""" + result = _build_forwarder("auto_1", [("bool", "x")]) + assert result == "TriggerForwarder{auto_1}" + + +def test_trigger_forwarder_on_true() -> None: + """Binary_sensor on_press / switch on_turn_on: TriggerOnTrueForwarder.""" + result = _build_forwarder("auto_1", [], forwarder=TriggerOnTrueForwarder) + assert result == "TriggerOnTrueForwarder{auto_1}" + + +def test_trigger_forwarder_on_false() -> None: + """Binary_sensor on_release / switch on_turn_off: TriggerOnFalseForwarder.""" + result = _build_forwarder("auto_1", [], forwarder=TriggerOnFalseForwarder) + assert result == "TriggerOnFalseForwarder{auto_1}" + + +def test_trigger_forwarder_multiple_args() -> None: + """Binary_sensor on_state_change: TriggerForwarder with two args.""" + result = _build_forwarder( + "auto_1", + [("optional", "x_previous"), ("optional", "x")], + ) + assert result == "TriggerForwarder, optional>{auto_1}" + + +def test_trigger_forwarder_string_arg() -> None: + """Text_sensor on_value: TriggerForwarder.""" + result = _build_forwarder("auto_1", [("std::string", "x")]) + assert result == "TriggerForwarder{auto_1}" + + +def test_trigger_forwarder_custom_type() -> None: + """Custom forwarder type passed directly.""" + custom = MockObj("MyForwarder", "") + result = _build_forwarder("auto_1", [], forwarder=custom) + assert result == "MyForwarder{auto_1}" From 240e53afce87155d12fe72a4f31117abe7ee4a13 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Mar 2026 14:35:09 -1000 Subject: [PATCH 065/115] [fan] Add benchmarks for fan component (#15210) --- tests/benchmarks/components/fan/__init__.py | 5 + tests/benchmarks/components/fan/bench_fan.cpp | 122 ++++++++++++++++++ .../benchmarks/components/fan/benchmark.yaml | 1 + 3 files changed, 128 insertions(+) create mode 100644 tests/benchmarks/components/fan/__init__.py create mode 100644 tests/benchmarks/components/fan/bench_fan.cpp create mode 100644 tests/benchmarks/components/fan/benchmark.yaml diff --git a/tests/benchmarks/components/fan/__init__.py b/tests/benchmarks/components/fan/__init__.py new file mode 100644 index 0000000000..b08f67a095 --- /dev/null +++ b/tests/benchmarks/components/fan/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/fan/bench_fan.cpp b/tests/benchmarks/components/fan/bench_fan.cpp new file mode 100644 index 0000000000..c7966c7886 --- /dev/null +++ b/tests/benchmarks/components/fan/bench_fan.cpp @@ -0,0 +1,122 @@ +#include + +#include "esphome/components/fan/fan.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +static constexpr int kInnerIterations = 2000; + +// Minimal Fan for benchmarking — control() is a no-op. +class BenchFan : public fan::Fan { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } + + fan::FanTraits get_traits() override { return this->traits_; } + + fan::FanTraits traits_; + + protected: + void control(const fan::FanCall & /*call*/) override {} +}; + +// Helper to create a typical fan device for benchmarks. +// Note: setup() is not called (no preferences backend), so save_state_() +// is effectively a no-op. This benchmarks the call/validation path, not persistence. +static void setup_fan(BenchFan &fan) { + fan.configure("test_fan"); + fan.traits_.set_oscillation(true); + fan.traits_.set_speed(true); + fan.traits_.set_supported_speed_count(6); + fan.traits_.set_direction(true); + fan.set_restore_mode(fan::FanRestoreMode::NO_RESTORE); + fan.traits_.set_supported_preset_modes({ + "auto", + "sleep", + "nature", + "turbo", + }); +} + +// --- Fan::publish_state() with speed update --- +// Measures the publish path for a fan reporting state — +// the hot path during fan operation. + +static void FanPublish_State(benchmark::State &state) { + BenchFan fan; + setup_fan(fan); + fan.state = true; + fan.direction = fan::FanDirection::FORWARD; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + fan.speed = (i % 6) + 1; + fan.publish_state(); + } + benchmark::DoNotOptimize(fan.speed); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(FanPublish_State); + +// --- Fan::publish_state() with callback --- +// Measures callback dispatch overhead. + +static void FanPublish_WithCallback(benchmark::State &state) { + BenchFan fan; + setup_fan(fan); + fan.state = true; + + uint64_t callback_count = 0; + fan.add_on_state_callback([&callback_count]() { callback_count++; }); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + fan.speed = (i % 6) + 1; + fan.publish_state(); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(FanPublish_WithCallback); + +// --- FanCall::perform() set speed --- +// The most common fan call — adjusting the speed level. + +static void FanCall_SetSpeed(benchmark::State &state) { + BenchFan fan; + setup_fan(fan); + fan.state = true; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + int speed = (i % 6) + 1; + fan.make_call().set_speed(speed).perform(); + } + benchmark::DoNotOptimize(fan.speed); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(FanCall_SetSpeed); + +// --- FanCall::perform() with multiple fields --- +// Exercises the validation path with state, speed, oscillation, and direction. + +static void FanCall_MultiField(benchmark::State &state) { + BenchFan fan; + setup_fan(fan); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + auto dir = (i % 2 == 0) ? fan::FanDirection::FORWARD : fan::FanDirection::REVERSE; + int speed = (i % 6) + 1; + fan.make_call().set_state(true).set_speed(speed).set_oscillating(i % 2 == 0).set_direction(dir).perform(); + } + benchmark::DoNotOptimize(fan.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(FanCall_MultiField); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/components/fan/benchmark.yaml b/tests/benchmarks/components/fan/benchmark.yaml new file mode 100644 index 0000000000..e9d59c12b2 --- /dev/null +++ b/tests/benchmarks/components/fan/benchmark.yaml @@ -0,0 +1 @@ +fan: From 90e6c0d7c7b2174309cd5309e0e082b6526b37e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Mar 2026 15:09:16 -1000 Subject: [PATCH 066/115] [core] Remove indirection from ControllerRegistry dispatch (#15173) --- esphome/core/controller_registry.cpp | 22 +++++++++++----------- esphome/core/controller_registry.h | 19 ++----------------- 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/esphome/core/controller_registry.cpp b/esphome/core/controller_registry.cpp index 255efa86ba..dd69de47d4 100644 --- a/esphome/core/controller_registry.cpp +++ b/esphome/core/controller_registry.cpp @@ -10,24 +10,24 @@ StaticVector ControllerRegistry::controll void ControllerRegistry::register_controller(Controller *controller) { controllers.push_back(controller); } -void ControllerRegistry::notify(void *obj, DispatchFunc dispatch) { - for (auto *controller : controllers) { - dispatch(controller, obj); - } -} - -// Macro for standard registry notification dispatch - calls on__update() -// Each wrapper passes a small trampoline lambda that calls the correct virtual method. +// Each notify method directly iterates controllers and calls the virtual method. +// This avoids the overhead of a shared noinline dispatch loop with function pointer +// indirection. The loop is tiny (~20 bytes per entity type) so the flash cost of +// duplicating it is negligible compared to eliminating two levels of indirection +// (noinline call + function pointer) from every state publish. // NOLINTBEGIN(bugprone-macro-parentheses) #define CONTROLLER_REGISTRY_NOTIFY(entity_type, entity_name) \ void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { \ - notify(obj, [](Controller *c, void *o) { c->on_##entity_name##_update(static_cast(o)); }); \ + for (auto *controller : controllers) { \ + controller->on_##entity_name##_update(obj); \ + } \ } -// Macro for entities where controller method has no "_update" suffix (Event, Update) #define CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(entity_type, entity_name) \ void ControllerRegistry::notify_##entity_name(entity_type *obj) { \ - notify(obj, [](Controller *c, void *o) { c->on_##entity_name(static_cast(o)); }); \ + for (auto *controller : controllers) { \ + controller->on_##entity_name(obj); \ + } \ } // NOLINTEND(bugprone-macro-parentheses) diff --git a/esphome/core/controller_registry.h b/esphome/core/controller_registry.h index 15e3b4ba83..89b3069bcb 100644 --- a/esphome/core/controller_registry.h +++ b/esphome/core/controller_registry.h @@ -146,8 +146,8 @@ class UpdateEntity; * entities call ControllerRegistry::notify_*_update() which iterates the small list * of registered controllers (typically 2: API and WebServer). * - * Controllers read state directly from entities using existing accessors (obj->state, etc.) - * rather than receiving it as callback parameters that were being ignored anyway. + * Each notify method directly iterates controllers and calls the virtual method, + * avoiding function pointer indirection for minimal dispatch overhead. * * Memory savings: 32 bytes per entity (2 controllers × 16 bytes std::function overhead) * Typical config (25 entities): ~780 bytes saved @@ -247,21 +247,6 @@ class ControllerRegistry { #endif protected: - /** Type-erased dispatch function pointer. - * - * Each notify method passes a small trampoline that calls the - * correct virtual method on Controller. The shared notify() loop - * iterates controllers once, calling the trampoline for each. - */ - using DispatchFunc = void (*)(Controller *, void *); - - /** Shared dispatch loop - iterates controllers and calls dispatch for each. - * - * Marked noinline to ensure only one copy of the loop exists in flash, - * rather than being duplicated into each notify_*_update wrapper. - */ - static void __attribute__((noinline)) notify(void *obj, DispatchFunc dispatch); - static StaticVector controllers; }; From e77cdb59710c5db3a904a6054ebc63035954d40e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Mar 2026 15:13:44 -1000 Subject: [PATCH 067/115] [light] Validate effect names during config validation instead of codegen (#15107) --- esphome/components/light/__init__.py | 75 +++++ esphome/components/light/automation.py | 49 ++- tests/component_tests/light/__init__.py | 0 .../light/test_effect_validation.py | 280 ++++++++++++++++++ 4 files changed, 395 insertions(+), 9 deletions(-) create mode 100644 tests/component_tests/light/__init__.py create mode 100644 tests/component_tests/light/test_effect_validation.py diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 4090ca57c2..5925afb472 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -24,6 +24,7 @@ from esphome.const import ( CONF_ID, CONF_INITIAL_STATE, CONF_MQTT_ID, + CONF_NAME, CONF_ON_STATE, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, @@ -41,6 +42,8 @@ from esphome.const import ( from esphome.core import CORE, ID, CoroPriority, HexInt, Lambda, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass +import esphome.final_validate as fv +from esphome.types import ConfigType from .automation import LIGHT_STATE_SCHEMA from .effects import ( @@ -70,9 +73,19 @@ IS_PLATFORM_COMPONENT = True DOMAIN = "light" +@dataclass +class EffectRef: + """A pending effect name reference from a light action to validate.""" + + light_id: ID + effect_name: str + component_path: list[str | int] # path_context when the action was validated + + @dataclass class LightData: gamma_tables: dict = field(default_factory=dict) # gamma_value -> fwd_arr + effect_refs: list[EffectRef] = field(default_factory=list) def _get_data() -> LightData: @@ -115,6 +128,68 @@ def _get_or_create_gamma_table(gamma_correct): return fwd_arr +def find_effect_index(effects: list, effect_name: str) -> int | None: + """Find the 1-based index of an effect by name (case-insensitive). + + Returns the 1-based index if found, or None if not found. + """ + effect_name_lower = effect_name.lower() + for i, effect_conf in enumerate(effects): + key = next(iter(effect_conf)) + if effect_conf[key][CONF_NAME].lower() == effect_name_lower: + return i + 1 + return None + + +def available_effects_str(effects: list) -> str: + """Return a comma-separated string of available effect names.""" + available = [ + effect_conf[next(iter(effect_conf))][CONF_NAME] for effect_conf in effects + ] + return ", ".join(f"'{name}'" for name in available) if available else "none" + + +def _final_validate(config: ConfigType) -> ConfigType: + """Validate all recorded effect name references against their target lights. + + This runs once per light platform instance. If no light platform is configured, + this never runs — but the ID validator will catch the missing light ID separately. + """ + data = _get_data() + if not data.effect_refs: + return config + + # Drain the list so we only validate once even though + # FINAL_VALIDATE_SCHEMA runs for each light platform instance. + refs = data.effect_refs + data.effect_refs = [] + + fconf = fv.full_config.get() + + for ref in refs: + try: + light_path = fconf.get_path_for_id(ref.light_id)[:-1] + light_config = fconf.get_config_for_path(light_path) + except KeyError: + # Light ID not found — ID validation will have already reported this + continue + + effects = light_config.get(CONF_EFFECTS, []) + + if find_effect_index(effects, ref.effect_name) is None: + raise cv.FinalExternalInvalid( + f"Effect '{ref.effect_name}' not found for light " + f"'{ref.light_id}'. " + f"Available effects: {available_effects_str(effects)}", + path=[cv.ROOT_CONFIG_PATH] + ref.component_path, + ) + + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + LightRestoreMode = light_ns.enum("LightRestoreMode") RESTORE_MODES = { "RESTORE_DEFAULT_OFF": LightRestoreMode.LIGHT_RESTORE_DEFAULT_OFF, diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 55273003b9..16e7d72f6b 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -1,5 +1,6 @@ from esphome import automation import esphome.codegen as cg +from esphome.config import path_context import esphome.config_validation as cv from esphome.const import ( CONF_BLUE, @@ -17,7 +18,6 @@ from esphome.const import ( CONF_LIMIT_MODE, CONF_MAX_BRIGHTNESS, CONF_MIN_BRIGHTNESS, - CONF_NAME, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_RED, @@ -26,7 +26,7 @@ from esphome.const import ( CONF_WARM_WHITE, CONF_WHITE, ) -from esphome.core import CORE, Lambda +from esphome.core import CORE, EsphomeError, Lambda from esphome.cpp_generator import LambdaExpression from esphome.types import ConfigType @@ -98,6 +98,31 @@ LIGHT_CONTROL_ACTION_SCHEMA = LIGHT_STATE_SCHEMA.extend( } ) + +def _record_effect_ref(config: ConfigType) -> ConfigType: + """Record a static effect name reference for later cross-component validation.""" + if CONF_EFFECT not in config: + return config + effect = config[CONF_EFFECT] + if isinstance(effect, Lambda): + return config # Lambda effects resolved at runtime + if effect.lower() == "none": + return config # "None" is always valid + + from . import EffectRef, _get_data + + _get_data().effect_refs.append( + EffectRef( + light_id=config[CONF_ID], + effect_name=effect, + component_path=path_context.get(), + ) + ) + return config + + +LIGHT_CONTROL_ACTION_SCHEMA.add_extra(_record_effect_ref) + LIGHT_TURN_OFF_ACTION_SCHEMA = automation.maybe_simple_id( { cv.Required(CONF_ID): cv.use_id(LightState), @@ -122,18 +147,24 @@ def _resolve_effect_index(config: ConfigType) -> int: Effect index 0 means "None" (no effect). Effects are 1-indexed matching the C++ convention in LightState. """ + from . import available_effects_str, find_effect_index + original_name = config[CONF_EFFECT] - effect_name = original_name.lower() - if effect_name == "none": + if original_name.lower() == "none": return 0 light_id = config[CONF_ID] light_path = CORE.config.get_path_for_id(light_id)[:-1] light_config = CORE.config.get_config_for_path(light_path) - for i, effect_conf in enumerate(light_config.get(CONF_EFFECTS, [])): - key = next(iter(effect_conf)) - if effect_conf[key][CONF_NAME].lower() == effect_name: - return i + 1 - raise ValueError(f"Effect '{original_name}' not found in light '{light_id}'") + effects = light_config.get(CONF_EFFECTS, []) + index = find_effect_index(effects, original_name) + if index is not None: + return index + # Should never reach here — effect names are validated during config + # validation in FINAL_VALIDATE_SCHEMA. This is a safety net. + raise EsphomeError( + f"Effect '{original_name}' not found for light '{light_id}'. " + f"Available effects: {available_effects_str(effects)}" + ) @automation.register_action( diff --git a/tests/component_tests/light/__init__.py b/tests/component_tests/light/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/light/test_effect_validation.py b/tests/component_tests/light/test_effect_validation.py new file mode 100644 index 0000000000..579e92c62a --- /dev/null +++ b/tests/component_tests/light/test_effect_validation.py @@ -0,0 +1,280 @@ +"""Tests for light effect name validation.""" + +from __future__ import annotations + +from collections.abc import Generator +from contextvars import Token + +import pytest + +from esphome import config_validation as cv +from esphome.components.light import ( + EffectRef, + _final_validate, + _get_data, + available_effects_str, + find_effect_index, +) +from esphome.components.light.automation import _record_effect_ref +from esphome.config import Config, path_context +from esphome.const import CONF_EFFECT, CONF_EFFECTS, CONF_ID, CONF_NAME +from esphome.core import ID, Lambda +import esphome.final_validate as fv +from esphome.types import ConfigType + + +def _make_effects(*names: str) -> list[dict[str, dict[str, str]]]: + """Create a list of effect config dicts from names.""" + return [{f"effect_{i}": {CONF_NAME: name}} for i, name in enumerate(names)] + + +# --- find_effect_index --- + + +def test_find_effect_index_found() -> None: + effects = _make_effects("Fast Pulse", "Slow Pulse") + assert find_effect_index(effects, "Fast Pulse") == 1 + assert find_effect_index(effects, "Slow Pulse") == 2 + + +def test_find_effect_index_case_insensitive() -> None: + effects = _make_effects("Fast Pulse") + assert find_effect_index(effects, "fast pulse") == 1 + assert find_effect_index(effects, "FAST PULSE") == 1 + + +def test_find_effect_index_not_found() -> None: + effects = _make_effects("Fast Pulse", "Slow Pulse") + assert find_effect_index(effects, "Missing") is None + + +def test_find_effect_index_empty() -> None: + assert find_effect_index([], "anything") is None + + +# --- available_effects_str --- + + +def test_available_effects_str_multiple() -> None: + effects = _make_effects("Fast Pulse", "Slow Pulse") + assert available_effects_str(effects) == "'Fast Pulse', 'Slow Pulse'" + + +def test_available_effects_str_single() -> None: + effects = _make_effects("Fast Pulse") + assert available_effects_str(effects) == "'Fast Pulse'" + + +def test_available_effects_str_empty() -> None: + assert available_effects_str([]) == "none" + + +# --- _final_validate --- + + +def _setup_final_validate( + effect_refs: list[EffectRef], + light_configs: list[ConfigType], + declare_ids: list[tuple[ID, list[str | int]]], +) -> Token: + """Set up CORE.data and fv.full_config for _final_validate tests.""" + data = _get_data() + data.effect_refs = effect_refs + + full_conf = Config() + full_conf["light"] = light_configs + for id_, path in declare_ids: + full_conf.declare_ids.append((id_, path)) + + return fv.full_config.set(full_conf) + + +def test_final_validate_valid_effect() -> None: + """Valid effect name should not raise.""" + light_id = ID("led1", is_declaration=True) + token = _setup_final_validate( + effect_refs=[ + EffectRef( + light_id=light_id, effect_name="Fast Pulse", component_path=["esphome"] + ), + ], + light_configs=[ + {CONF_ID: light_id, CONF_EFFECTS: _make_effects("Fast Pulse", "Slow Pulse")} + ], + declare_ids=[(light_id, ["light", 0, CONF_ID])], + ) + try: + _final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_final_validate_invalid_effect_raises() -> None: + """Invalid effect name should raise FinalExternalInvalid.""" + light_id = ID("led1", is_declaration=True) + token = _setup_final_validate( + effect_refs=[ + EffectRef( + light_id=light_id, effect_name="Nonexistent", component_path=["esphome"] + ), + ], + light_configs=[ + {CONF_ID: light_id, CONF_EFFECTS: _make_effects("Fast Pulse", "Slow Pulse")} + ], + declare_ids=[(light_id, ["light", 0, CONF_ID])], + ) + try: + with pytest.raises(cv.FinalExternalInvalid, match="Nonexistent"): + _final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_final_validate_lists_available_effects() -> None: + """Error message should list available effects.""" + light_id = ID("led1", is_declaration=True) + token = _setup_final_validate( + effect_refs=[ + EffectRef( + light_id=light_id, effect_name="Missing", component_path=["esphome"] + ), + ], + light_configs=[ + {CONF_ID: light_id, CONF_EFFECTS: _make_effects("Fast Pulse", "Slow Pulse")} + ], + declare_ids=[(light_id, ["light", 0, CONF_ID])], + ) + try: + with pytest.raises(cv.FinalExternalInvalid, match="'Fast Pulse', 'Slow Pulse'"): + _final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_final_validate_no_effects_on_light() -> None: + """Light with no effects should report 'none' as available.""" + light_id = ID("led1", is_declaration=True) + token = _setup_final_validate( + effect_refs=[ + EffectRef( + light_id=light_id, effect_name="Missing", component_path=["esphome"] + ), + ], + light_configs=[{CONF_ID: light_id}], + declare_ids=[(light_id, ["light", 0, CONF_ID])], + ) + try: + with pytest.raises(cv.FinalExternalInvalid, match="Available effects: none"): + _final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_final_validate_no_refs_is_noop() -> None: + """No stored refs should pass without error.""" + data = _get_data() + data.effect_refs = [] + _final_validate({}) + + +def test_final_validate_unknown_light_id_skipped() -> None: + """Refs to unknown light IDs should be silently skipped.""" + data = _get_data() + data.effect_refs = [ + EffectRef( + light_id=ID("nonexistent", is_declaration=True), + effect_name="Missing", + component_path=["esphome"], + ) + ] + + full_conf = Config() + token = fv.full_config.set(full_conf) + try: + _final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_final_validate_drains_refs() -> None: + """Refs should be drained after validation to avoid redundant runs.""" + light_id = ID("led1", is_declaration=True) + token = _setup_final_validate( + effect_refs=[ + EffectRef( + light_id=light_id, effect_name="Fast Pulse", component_path=["esphome"] + ), + ], + light_configs=[{CONF_ID: light_id, CONF_EFFECTS: _make_effects("Fast Pulse")}], + declare_ids=[(light_id, ["light", 0, CONF_ID])], + ) + try: + _final_validate({}) + assert _get_data().effect_refs == [] + finally: + fv.full_config.reset(token) + + +# --- _record_effect_ref --- + + +@pytest.fixture +def _path_ctx() -> Generator[None]: + """Set path_context for _record_effect_ref tests.""" + token = path_context.set(["esphome"]) + yield + path_context.reset(token) + + +@pytest.mark.usefixtures("_path_ctx") +def test_record_effect_ref_static() -> None: + """Static effect name should be recorded.""" + light_id = ID("led1", is_declaration=True) + config: ConfigType = {CONF_ID: light_id, CONF_EFFECT: "Fast Pulse"} + result = _record_effect_ref(config) + assert result is config + data = _get_data() + assert len(data.effect_refs) == 1 + assert data.effect_refs[0].effect_name == "Fast Pulse" + assert data.effect_refs[0].light_id is light_id + assert data.effect_refs[0].component_path == ["esphome"] + + +@pytest.mark.usefixtures("_path_ctx") +def test_record_effect_ref_skips_lambda() -> None: + """Lambda effect should not be recorded.""" + config: ConfigType = { + CONF_ID: ID("led1", is_declaration=True), + CONF_EFFECT: Lambda("return effect;"), + } + _record_effect_ref(config) + assert _get_data().effect_refs == [] + + +@pytest.mark.usefixtures("_path_ctx") +def test_record_effect_ref_skips_none() -> None: + """Effect 'None' should not be recorded.""" + config: ConfigType = { + CONF_ID: ID("led1", is_declaration=True), + CONF_EFFECT: "None", + } + _record_effect_ref(config) + assert _get_data().effect_refs == [] + + +@pytest.mark.usefixtures("_path_ctx") +def test_record_effect_ref_skips_none_case_insensitive() -> None: + """Effect 'none' (lowercase) should not be recorded.""" + config: ConfigType = { + CONF_ID: ID("led1", is_declaration=True), + CONF_EFFECT: "none", + } + _record_effect_ref(config) + assert _get_data().effect_refs == [] + + +def test_record_effect_ref_skips_no_effect_key() -> None: + """Config without effect key should be a no-op.""" + config: ConfigType = {CONF_ID: ID("led1", is_declaration=True)} + _record_effect_ref(config) + assert _get_data().effect_refs == [] From 90dafa3fa45bc5b279136f069030e1ea4edde780 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Mar 2026 15:59:58 -1000 Subject: [PATCH 068/115] [logger] Warn when VERBOSE/VERY_VERBOSE logging is active (#15189) --- esphome/components/logger/logger.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index cd6543bfb8..23b69c36c6 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -243,6 +243,16 @@ void Logger::dump_config() { #endif #ifdef USE_ZEPHYR dump_crash_(); +#endif + // Warn users that VERBOSE/VERY_VERBOSE logging impacts performance. + // Only the compiled log level matters — all log calls up to this level + // are in the binary and will be formatted (vsnprintf) and block UART. +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + ESP_LOGW(TAG, "VERY_VERBOSE logging is active — significant performance impact, short-term debugging only\n" + " May cause connection instability. Set log level to DEBUG or lower for long-term use."); +#elif ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGI(TAG, "VERBOSE logging is active — performance impact, short-term debugging only\n" + " Set log level to DEBUG or lower for long-term use."); #endif } From 6feb2d04dfd61c3fe1d03980fe1516b846eacf6b Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Fri, 27 Mar 2026 04:36:35 +0100 Subject: [PATCH 069/115] [nextion] Replace `static std::string COMMAND_DELIMITER` with `constexpr` (#15195) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/nextion/nextion.cpp | 13 +++++++++---- esphome/components/nextion/nextion.h | 2 -- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 612bfbc968..fa1582c209 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -10,6 +10,10 @@ namespace nextion { static const char *const TAG = "nextion"; +// Nextion command terminator: three consecutive 0xFF bytes (per Nextion Instruction Set v1.1). +static constexpr uint8_t COMMAND_DELIMITER[3] = {0xFF, 0xFF, 0xFF}; +static constexpr size_t DELIMITER_SIZE = sizeof(COMMAND_DELIMITER); + void Nextion::setup() { this->is_setup_ = false; this->connection_state_.ignore_is_setup_ = true; @@ -415,7 +419,8 @@ void Nextion::process_nextion_commands_() { #ifdef NEXTION_PROTOCOL_LOG this->print_queue_members_(); #endif - while ((to_process_length = this->command_data_.find(COMMAND_DELIMITER)) != std::string::npos) { + while ((to_process_length = this->command_data_.find(reinterpret_cast(COMMAND_DELIMITER), 0, + DELIMITER_SIZE)) != std::string::npos) { #ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP if (++commands_processed > this->max_commands_per_loop_) { ESP_LOGW(TAG, "Command processing limit exceeded"); @@ -423,8 +428,8 @@ void Nextion::process_nextion_commands_() { } #endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP ESP_LOGN(TAG, "queue size: %zu", this->nextion_queue_.size()); - while (to_process_length + COMMAND_DELIMITER.length() < this->command_data_.length() && - static_cast(this->command_data_[to_process_length + COMMAND_DELIMITER.length()]) == 0xFF) { + while (to_process_length + DELIMITER_SIZE < this->command_data_.length() && + static_cast(this->command_data_[to_process_length + DELIMITER_SIZE]) == 0xFF) { ++to_process_length; ESP_LOGN(TAG, "Add 0xFF"); } @@ -829,7 +834,7 @@ void Nextion::process_nextion_commands_() { break; } - this->command_data_.erase(0, to_process_length + COMMAND_DELIMITER.length() + 1); + this->command_data_.erase(0, to_process_length + DELIMITER_SIZE + 1); } const uint32_t ms = App.get_loop_component_start_time(); diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index bb5998cf5d..217d2e605d 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -29,8 +29,6 @@ class NextionComponentBase; using nextion_writer_t = display::DisplayWriter; -static const std::string COMMAND_DELIMITER{static_cast(255), static_cast(255), static_cast(255)}; - #ifdef USE_NEXTION_COMMAND_SPACING class NextionCommandPacer { public: From 2d9922496cd94ed43a0a5eec7193ff9f581c48a8 Mon Sep 17 00:00:00 2001 From: Diorcet Yann Date: Fri, 27 Mar 2026 17:02:45 +0100 Subject: [PATCH 070/115] [git] Add support for subpath to computed destination directory (#15135) --- esphome/git.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/git.py b/esphome/git.py index a45768b5cd..096ff483a7 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -102,6 +102,7 @@ def clone_or_update( username: str = None, password: str = None, submodules: list[str] | None = None, + subpath: Path | None = None, _recover_broken: bool = True, ) -> tuple[Path, Callable[[], None] | None]: key = f"{url}@{ref}" @@ -112,6 +113,9 @@ def clone_or_update( ) repo_dir = _compute_destination_path(key, domain) + if subpath: + repo_dir = repo_dir / subpath + if not repo_dir.is_dir(): _LOGGER.info("Cloning %s", key) _LOGGER.debug("Location: %s", repo_dir) From 73e939ffb5fa41be407812fa069be76f45d968e6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:13:24 -0400 Subject: [PATCH 071/115] [sgp4x] Fix NOx index_offset default (should be 1, not 100) (#15212) --- esphome/components/sgp4x/sensor.py | 39 ++++++++++++++++++------------ 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/esphome/components/sgp4x/sensor.py b/esphome/components/sgp4x/sensor.py index ab78ab59d9..8d52ffb4f2 100644 --- a/esphome/components/sgp4x/sensor.py +++ b/esphome/components/sgp4x/sensor.py @@ -44,20 +44,27 @@ def validate_sensors(config): return config -GAS_SENSOR = cv.Schema( - { - cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema( - { - cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_, - cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_, - cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_, - cv.Optional(CONF_GATING_MAX_DURATION_MINUTES, default=720): cv.int_, - cv.Optional(CONF_STD_INITIAL, default=50): cv.int_, - cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_, - } - ) - } -) +def _gas_sensor_schema(index_offset_default: int): + return cv.Schema( + { + cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema( + { + cv.Optional( + CONF_INDEX_OFFSET, default=index_offset_default + ): cv.int_, + cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_, + cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_, + cv.Optional(CONF_GATING_MAX_DURATION_MINUTES, default=720): cv.int_, + cv.Optional(CONF_STD_INITIAL, default=50): cv.int_, + cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_, + } + ) + } + ) + + +VOC_SENSOR = _gas_sensor_schema(100) +NOX_SENSOR = _gas_sensor_schema(1) CONFIG_SCHEMA = cv.All( cv.Schema( @@ -68,13 +75,13 @@ CONFIG_SCHEMA = cv.All( accuracy_decimals=0, device_class=DEVICE_CLASS_AQI, state_class=STATE_CLASS_MEASUREMENT, - ).extend(GAS_SENSOR), + ).extend(VOC_SENSOR), cv.Optional(CONF_NOX): sensor.sensor_schema( icon=ICON_RADIATOR, accuracy_decimals=0, device_class=DEVICE_CLASS_AQI, state_class=STATE_CLASS_MEASUREMENT, - ).extend(GAS_SENSOR), + ).extend(NOX_SENSOR), cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, cv.Optional(CONF_COMPENSATION): cv.Schema( From 1e65165e48274acfd30a8cfd18867608b6dff414 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:19:58 -1000 Subject: [PATCH 072/115] [safe_mode] Migrate SafeModeTrigger to callback automation (#15197) --- esphome/components/safe_mode/__init__.py | 13 ++++--------- esphome/components/safe_mode/automation.h | 10 ---------- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index e868985054..da36d21eb7 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_NUM_ATTEMPTS, CONF_REBOOT_TIMEOUT, CONF_SAFE_MODE, - CONF_TRIGGER_ID, KEY_PAST_SAFE_MODE, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -20,7 +19,6 @@ CONF_ON_SAFE_MODE = "on_safe_mode" safe_mode_ns = cg.esphome_ns.namespace("safe_mode") SafeModeComponent = safe_mode_ns.class_("SafeModeComponent", cg.Component) -SafeModeTrigger = safe_mode_ns.class_("SafeModeTrigger", automation.Trigger.template()) MarkSuccessfulAction = safe_mode_ns.class_("MarkSuccessfulAction", automation.Action) @@ -43,11 +41,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_REBOOT_TIMEOUT, default="5min" ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_ON_SAFE_MODE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SafeModeTrigger), - } - ), + cv.Optional(CONF_ON_SAFE_MODE): automation.validate_automation({}), } ).extend(cv.COMPONENT_SCHEMA), _remove_id_if_disabled, @@ -80,8 +74,9 @@ async def to_code(config): if on_safe_mode_config := config.get(CONF_ON_SAFE_MODE): cg.add_define("USE_SAFE_MODE_CALLBACK") for conf in on_safe_mode_config: - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_safe_mode_callback", [], conf + ) condition = var.should_enter_safe_mode( config[CONF_NUM_ATTEMPTS], diff --git a/esphome/components/safe_mode/automation.h b/esphome/components/safe_mode/automation.h index dee02c64a0..79b53c0881 100644 --- a/esphome/components/safe_mode/automation.h +++ b/esphome/components/safe_mode/automation.h @@ -1,19 +1,9 @@ #pragma once -#include "esphome/core/defines.h" #include "esphome/core/automation.h" #include "safe_mode.h" namespace esphome::safe_mode { -#ifdef USE_SAFE_MODE_CALLBACK -class SafeModeTrigger final : public Trigger<> { - public: - explicit SafeModeTrigger(SafeModeComponent *parent) { - parent->add_on_safe_mode_callback([this]() { trigger(); }); - } -}; -#endif // USE_SAFE_MODE_CALLBACK - template class MarkSuccessfulAction : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->mark_successful(); } From b0f6a94df51a40c163627aa7e12a1c3947369573 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:20:11 -1000 Subject: [PATCH 073/115] [sml] Migrate DataTrigger to callback automation (#15233) --- esphome/components/sml/__init__.py | 23 +++++------------------ esphome/components/sml/automation.h | 19 ------------------- 2 files changed, 5 insertions(+), 37 deletions(-) delete mode 100644 esphome/components/sml/automation.h diff --git a/esphome/components/sml/__init__.py b/esphome/components/sml/__init__.py index eaeddce390..1bf0d97d65 100644 --- a/esphome/components/sml/__init__.py +++ b/esphome/components/sml/__init__.py @@ -4,7 +4,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_ON_DATA, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_ON_DATA CODEOWNERS = ["@alengwenus"] @@ -18,24 +18,11 @@ CONF_SML_ID = "sml_id" CONF_OBIS_CODE = "obis_code" CONF_SERVER_ID = "server_id" -sml_ns = cg.esphome_ns.namespace("sml") - -DataTrigger = sml_ns.class_( - "DataTrigger", - automation.Trigger.template( - cg.std_vector.template(cg.uint8).operator("ref"), cg.bool_ - ), -) - CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(Sml), - cv.Optional(CONF_ON_DATA): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DataTrigger), - } - ), + cv.Optional(CONF_ON_DATA): automation.validate_automation({}), } ).extend(uart.UART_DEVICE_SCHEMA) @@ -45,9 +32,9 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) for conf in config.get(CONF_ON_DATA, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, + await automation.build_callback_automation( + var, + "add_on_data_callback", [ ( cg.std_vector.template(cg.uint8).operator("ref").operator("const"), diff --git a/esphome/components/sml/automation.h b/esphome/components/sml/automation.h deleted file mode 100644 index d51063065d..0000000000 --- a/esphome/components/sml/automation.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "esphome/core/automation.h" -#include "sml.h" - -#include - -namespace esphome { -namespace sml { - -class DataTrigger : public Trigger &, bool> { - public: - explicit DataTrigger(Sml *sml) { - sml->add_on_data_callback([this](const std::vector &data, bool valid) { this->trigger(data, valid); }); - } -}; - -} // namespace sml -} // namespace esphome From b41634e19af272b8c146d3912edf2cba3191b2eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:20:24 -1000 Subject: [PATCH 074/115] [alarm_control_panel] Migrate triggers to callback automation (#15198) --- .../alarm_control_panel/__init__.py | 162 +++++------------- .../alarm_control_panel.cpp | 4 +- .../alarm_control_panel/alarm_control_panel.h | 4 +- .../alarm_control_panel/automation.h | 69 ++------ .../mqtt/mqtt_alarm_control_panel.cpp | 3 +- 5 files changed, 68 insertions(+), 174 deletions(-) diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index aefb18d25c..4ee073a15b 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -10,7 +10,6 @@ from esphome.const import ( CONF_ID, CONF_MQTT_ID, CONF_ON_STATE, - CONF_TRIGGER_ID, CONF_WEB_SERVER, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -34,39 +33,9 @@ CONF_ON_READY = "on_ready" alarm_control_panel_ns = cg.esphome_ns.namespace("alarm_control_panel") AlarmControlPanel = alarm_control_panel_ns.class_("AlarmControlPanel", cg.EntityBase) -StateTrigger = alarm_control_panel_ns.class_( - "StateTrigger", automation.Trigger.template() -) -TriggeredTrigger = alarm_control_panel_ns.class_( - "TriggeredTrigger", automation.Trigger.template() -) -ClearedTrigger = alarm_control_panel_ns.class_( - "ClearedTrigger", automation.Trigger.template() -) -ArmingTrigger = alarm_control_panel_ns.class_( - "ArmingTrigger", automation.Trigger.template() -) -PendingTrigger = alarm_control_panel_ns.class_( - "PendingTrigger", automation.Trigger.template() -) -ArmedHomeTrigger = alarm_control_panel_ns.class_( - "ArmedHomeTrigger", automation.Trigger.template() -) -ArmedNightTrigger = alarm_control_panel_ns.class_( - "ArmedNightTrigger", automation.Trigger.template() -) -ArmedAwayTrigger = alarm_control_panel_ns.class_( - "ArmedAwayTrigger", automation.Trigger.template() -) -DisarmedTrigger = alarm_control_panel_ns.class_( - "DisarmedTrigger", automation.Trigger.template() -) -ChimeTrigger = alarm_control_panel_ns.class_( - "ChimeTrigger", automation.Trigger.template() -) -ReadyTrigger = alarm_control_panel_ns.class_( - "ReadyTrigger", automation.Trigger.template() -) +StateAnyForwarder = alarm_control_panel_ns.class_("StateAnyForwarder") +StateEnterForwarder = alarm_control_panel_ns.class_("StateEnterForwarder") +AlarmControlPanelState = alarm_control_panel_ns.enum("AlarmControlPanelState") ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action) ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action) @@ -89,61 +58,17 @@ _ALARM_CONTROL_PANEL_SCHEMA = ( cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( mqtt.MQTTAlarmControlPanelComponent ), - cv.Optional(CONF_ON_STATE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), - } - ), - cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger), - } - ), - cv.Optional(CONF_ON_ARMING): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmingTrigger), - } - ), - cv.Optional(CONF_ON_PENDING): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PendingTrigger), - } - ), - cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedHomeTrigger), - } - ), - cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedNightTrigger), - } - ), - cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedAwayTrigger), - } - ), - cv.Optional(CONF_ON_DISARMED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisarmedTrigger), - } - ), - cv.Optional(CONF_ON_CLEARED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger), - } - ), - cv.Optional(CONF_ON_CHIME): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger), - } - ), - cv.Optional(CONF_ON_READY): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger), - } - ), + cv.Optional(CONF_ON_STATE): automation.validate_automation({}), + cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation({}), + cv.Optional(CONF_ON_ARMING): automation.validate_automation({}), + cv.Optional(CONF_ON_PENDING): automation.validate_automation({}), + cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation({}), + cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation({}), + cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation({}), + cv.Optional(CONF_ON_DISARMED): automation.validate_automation({}), + cv.Optional(CONF_ON_CLEARED): automation.validate_automation({}), + cv.Optional(CONF_ON_CHIME): automation.validate_automation({}), + cv.Optional(CONF_ON_READY): automation.validate_automation({}), } ) ) @@ -189,38 +114,39 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id( @setup_entity("alarm_control_panel") async def setup_alarm_control_panel_core_(var, config): 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_TRIGGERED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_ARMING, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_PENDING, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_ARMED_HOME, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_ARMED_NIGHT, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_ARMED_AWAY, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_DISARMED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_state_callback", [], conf, forwarder=StateAnyForwarder + ) + _STATE_ENTER_MAP = { + CONF_ON_TRIGGERED: AlarmControlPanelState.ACP_STATE_TRIGGERED, + CONF_ON_ARMING: AlarmControlPanelState.ACP_STATE_ARMING, + CONF_ON_PENDING: AlarmControlPanelState.ACP_STATE_PENDING, + CONF_ON_ARMED_HOME: AlarmControlPanelState.ACP_STATE_ARMED_HOME, + CONF_ON_ARMED_NIGHT: AlarmControlPanelState.ACP_STATE_ARMED_NIGHT, + CONF_ON_ARMED_AWAY: AlarmControlPanelState.ACP_STATE_ARMED_AWAY, + CONF_ON_DISARMED: AlarmControlPanelState.ACP_STATE_DISARMED, + } + for conf_key, state_enum in _STATE_ENTER_MAP.items(): + for conf in config.get(conf_key, []): + await automation.build_callback_automation( + var, + "add_on_state_callback", + [], + conf, + forwarder=StateEnterForwarder.template(state_enum), + ) for conf in config.get(CONF_ON_CLEARED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_cleared_callback", [], conf + ) for conf in config.get(CONF_ON_CHIME, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_chime_callback", [], conf + ) for conf in config.get(CONF_ON_READY, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_ready_callback", [], conf + ) if web_server_config := config.get(CONF_WEB_SERVER): await web_server.add_entity_config(var, web_server_config) if mqtt_id := config.get(CONF_MQTT_ID): diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index 623241851a..fc72c13ce3 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -35,8 +35,8 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { LOG_STR_ARG(alarm_control_panel_state_to_string(state)), LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); this->current_state_ = state; - // Single state callback - triggers check get_state() for specific states - this->state_callback_.call(); + // Single state callback - listeners receive the new state as an argument + this->state_callback_.call(state); #if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_alarm_control_panel_update(this); #endif diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.h b/esphome/components/alarm_control_panel/alarm_control_panel.h index cf99d359e7..e748b8621b 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel.h @@ -145,8 +145,8 @@ class AlarmControlPanel : public EntityBase { uint32_t last_update_; // the call control function virtual void control(const AlarmControlPanelCall &call) = 0; - // state callback - triggers check get_state() for specific state - LazyCallbackManager state_callback_{}; + // state callback - passes the new state to listeners + LazyCallbackManager state_callback_{}; // clear callback - fires when leaving TRIGGERED state LazyCallbackManager cleared_callback_{}; // chime callback diff --git a/esphome/components/alarm_control_panel/automation.h b/esphome/components/alarm_control_panel/automation.h index 4ff34de0d5..022d2650d2 100644 --- a/esphome/components/alarm_control_panel/automation.h +++ b/esphome/components/alarm_control_panel/automation.h @@ -5,60 +5,27 @@ namespace esphome::alarm_control_panel { -/// Trigger on any state change -class StateTrigger : public Trigger<> { - public: - explicit StateTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_state_callback([this]() { this->trigger(); }); +/// Callback forwarder that triggers an Automation<> on any state change. +/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_. +struct StateAnyForwarder { + Automation<> *automation; + void operator()(AlarmControlPanelState /*state*/) const { this->automation->trigger(); } +}; + +/// Callback forwarder that triggers an Automation<> only when the alarm enters a specific state. +/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_. +template struct StateEnterForwarder { + Automation<> *automation; + void operator()(AlarmControlPanelState state) const { + if (state == State) + this->automation->trigger(); } }; -/// Template trigger that fires when entering a specific state -template class StateEnterTrigger : public Trigger<> { - public: - explicit StateEnterTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { - alarm_control_panel->add_on_state_callback([this]() { - if (this->alarm_control_panel_->get_state() == State) - this->trigger(); - }); - } - - protected: - AlarmControlPanel *alarm_control_panel_; -}; - -// Type aliases for state-specific triggers -using TriggeredTrigger = StateEnterTrigger; -using ArmingTrigger = StateEnterTrigger; -using PendingTrigger = StateEnterTrigger; -using ArmedHomeTrigger = StateEnterTrigger; -using ArmedNightTrigger = StateEnterTrigger; -using ArmedAwayTrigger = StateEnterTrigger; -using DisarmedTrigger = StateEnterTrigger; - -/// Trigger when leaving TRIGGERED state (alarm cleared) -class ClearedTrigger : public Trigger<> { - public: - explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_cleared_callback([this]() { this->trigger(); }); - } -}; - -/// Trigger on chime event (zone opened while disarmed) -class ChimeTrigger : public Trigger<> { - public: - explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_chime_callback([this]() { this->trigger(); }); - } -}; - -/// Trigger on ready state change -class ReadyTrigger : public Trigger<> { - public: - explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_ready_callback([this]() { this->trigger(); }); - } -}; +static_assert(sizeof(StateAnyForwarder) <= sizeof(void *)); +static_assert(std::is_trivially_copyable_v); +static_assert(sizeof(StateEnterForwarder) <= sizeof(void *)); +static_assert(std::is_trivially_copyable_v>); template class ArmAwayAction : public Action { public: diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index 74a60b3624..f059360e23 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -48,7 +48,8 @@ static bool apply_command(AlarmControlPanelCall &call, const char *state) { } void MQTTAlarmControlPanelComponent::setup() { - this->alarm_control_panel_->add_on_state_callback([this]() { this->publish_state(); }); + this->alarm_control_panel_->add_on_state_callback( + [this](AlarmControlPanelState /*state*/) { this->publish_state(); }); this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { auto call = this->alarm_control_panel_->make_call(); if (!payload.empty() && payload[0] == '{') { From dea8fdd906a7fc79648d05dfeb74d6aaadad55e8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:20:35 -1000 Subject: [PATCH 075/115] [lock] Migrate LockStateTrigger to callback automation (#15199) --- esphome/components/copy/lock/copy_lock.cpp | 2 +- esphome/components/lock/__init__.py | 34 ++++++++++------------ esphome/components/lock/automation.h | 22 ++++++-------- esphome/components/lock/lock.cpp | 2 +- esphome/components/lock/lock.h | 4 +-- esphome/components/mqtt/mqtt_lock.cpp | 3 +- 6 files changed, 30 insertions(+), 37 deletions(-) diff --git a/esphome/components/copy/lock/copy_lock.cpp b/esphome/components/copy/lock/copy_lock.cpp index 25bd8c33ef..c846954510 100644 --- a/esphome/components/copy/lock/copy_lock.cpp +++ b/esphome/components/copy/lock/copy_lock.cpp @@ -7,7 +7,7 @@ namespace copy { static const char *const TAG = "copy.lock"; void CopyLock::setup() { - source_->add_on_state_callback([this]() { this->publish_state(source_->state); }); + source_->add_on_state_callback([this](lock::LockState state) { this->publish_state(state); }); traits.set_assumed_state(source_->traits.get_assumed_state()); traits.set_requires_code(source_->traits.get_requires_code()); diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index fe4db23ae3..0df4b20cba 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -10,7 +10,6 @@ from esphome.const import ( CONF_MQTT_ID, CONF_ON_LOCK, CONF_ON_UNLOCK, - CONF_TRIGGER_ID, CONF_WEB_SERVER, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -31,8 +30,7 @@ OpenAction = lock_ns.class_("OpenAction", automation.Action) LockPublishAction = lock_ns.class_("LockPublishAction", automation.Action) LockCondition = lock_ns.class_("LockCondition", Condition) -LockLockTrigger = lock_ns.class_("LockLockTrigger", automation.Trigger.template()) -LockUnlockTrigger = lock_ns.class_("LockUnlockTrigger", automation.Trigger.template()) +LockStateForwarder = lock_ns.class_("LockStateForwarder") LockState = lock_ns.enum("LockState") @@ -52,16 +50,8 @@ _LOCK_SCHEMA = ( .extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTLockComponent), - cv.Optional(CONF_ON_LOCK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LockLockTrigger), - } - ), - cv.Optional(CONF_ON_UNLOCK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LockUnlockTrigger), - } - ), + cv.Optional(CONF_ON_LOCK): automation.validate_automation({}), + cv.Optional(CONF_ON_UNLOCK): automation.validate_automation({}), } ) ) @@ -93,12 +83,18 @@ def lock_schema( @setup_entity("lock") async def _setup_lock_core(var, config): - for conf in config.get(CONF_ON_LOCK, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_UNLOCK, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + for conf_key, state_enum in ( + (CONF_ON_LOCK, LockState.LOCK_STATE_LOCKED), + (CONF_ON_UNLOCK, LockState.LOCK_STATE_UNLOCKED), + ): + for conf in config.get(conf_key, []): + await automation.build_callback_automation( + var, + "add_on_state_callback", + [], + conf, + forwarder=LockStateForwarder.template(state_enum), + ) if mqtt_id := config.get(CONF_MQTT_ID): mqtt_ = cg.new_Pvariable(mqtt_id, var) diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h index 6f3c422693..c140bc568f 100644 --- a/esphome/components/lock/automation.h +++ b/esphome/components/lock/automation.h @@ -49,21 +49,17 @@ template class LockCondition : public Condition { bool state_; }; -template class LockStateTrigger : public Trigger<> { - public: - explicit LockStateTrigger(Lock *a_lock) : lock_(a_lock) { - a_lock->add_on_state_callback([this]() { - if (this->lock_->state == State) { - this->trigger(); - } - }); +/// Callback forwarder that triggers an Automation<> only when a specific lock state is entered. +/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_. +template struct LockStateForwarder { + Automation<> *automation; + void operator()(LockState state) const { + if (state == State) + this->automation->trigger(); } - - protected: - Lock *lock_; }; -using LockLockTrigger = LockStateTrigger; -using LockUnlockTrigger = LockStateTrigger; +static_assert(sizeof(LockStateForwarder) <= sizeof(void *)); +static_assert(std::is_trivially_copyable_v>); } // namespace esphome::lock diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 90937485b9..3ff131af3d 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -42,7 +42,7 @@ void Lock::publish_state(LockState state) { this->state = state; this->rtc_.save(&this->state); ESP_LOGV(TAG, "'%s' >> %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state))); - this->state_callback_.call(); + this->state_callback_.call(state); #if defined(USE_LOCK) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_lock_update(this); #endif diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index 707431d543..543a4b51a8 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -148,7 +148,7 @@ class Lock : public EntityBase { /** Set callback for state changes. * - * @param callback The void(bool) callback. + * @param callback The void(LockState) callback. */ template void add_on_state_callback(F &&callback) { this->state_callback_.add(std::forward(callback)); @@ -178,7 +178,7 @@ class Lock : public EntityBase { */ virtual void control(const LockCall &call) = 0; - LazyCallbackManager state_callback_{}; + LazyCallbackManager state_callback_{}; Deduplicator publish_dedup_; ESPPreferenceObject rtc_; }; diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index 45d8e4698f..7920187f92 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -28,7 +28,8 @@ void MQTTLockComponent::setup() { this->status_momentary_warning("state", 5000); } }); - this->lock_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); }); + this->lock_->add_on_state_callback( + [this](LockState /*state*/) { this->defer("send", [this]() { this->publish_state(); }); }); } void MQTTLockComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Lock '%s': ", this->lock_->get_name().c_str()); From 2e42547d32edce07150f925e7bc8fd0c03f7b814 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:20:46 -1000 Subject: [PATCH 076/115] [media_player] Migrate triggers to callback automation (#15200) Co-authored-by: Claude Opus 4.6 (1M context) --- esphome/components/media_player/__init__.py | 42 ++++++++++--------- esphome/components/media_player/automation.h | 41 ++++++++---------- .../components/media_player/media_player.cpp | 2 +- .../components/media_player/media_player.h | 2 +- .../voice_assistant/voice_assistant.cpp | 4 +- 5 files changed, 44 insertions(+), 47 deletions(-) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index a5baca2994..767916ad88 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -9,7 +9,6 @@ from esphome.const import ( CONF_ON_STATE, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, - CONF_TRIGGER_ID, CONF_VOLUME, ) from esphome.core import CORE @@ -65,15 +64,19 @@ _COMMAND_ACTIONS = [ "clear_playlist", ] -# State triggers: (config_key, C++ class name) +StateAnyForwarder = media_player_ns.class_("StateAnyForwarder") +StateEnterForwarder = media_player_ns.class_("StateEnterForwarder") +MediaPlayerState = media_player_ns.enum("MediaPlayerState") + +# State triggers: (config_key, state enum or None for any-state) _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"), + (CONF_ON_STATE, None), + (CONF_ON_IDLE, MediaPlayerState.MEDIA_PLAYER_STATE_IDLE), + (CONF_ON_PLAY, MediaPlayerState.MEDIA_PLAYER_STATE_PLAYING), + (CONF_ON_PAUSE, MediaPlayerState.MEDIA_PLAYER_STATE_PAUSED), + (CONF_ON_ANNOUNCEMENT, MediaPlayerState.MEDIA_PLAYER_STATE_ANNOUNCING), + (CONF_ON_TURN_ON, MediaPlayerState.MEDIA_PLAYER_STATE_ON), + (CONF_ON_TURN_OFF, MediaPlayerState.MEDIA_PLAYER_STATE_OFF), ] # State conditions that all share the same schema and codegen handler @@ -98,10 +101,15 @@ VolumeSetAction = media_player_ns.class_( @setup_entity("media_player") async def setup_media_player_core_(var, config): - for conf_key, _ in _STATE_TRIGGERS: + for conf_key, state_enum 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) + if state_enum is None: + forwarder = StateAnyForwarder + else: + forwarder = StateEnterForwarder.template(state_enum) + await automation.build_callback_automation( + var, "add_on_state_callback", [], conf, forwarder=forwarder + ) async def register_media_player(var, config): @@ -120,14 +128,8 @@ async def new_media_player(config, *args): _MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( { - cv.Optional(conf_key): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - media_player_ns.class_(class_name, automation.Trigger.template()) - ), - } - ) - for conf_key, class_name in _STATE_TRIGGERS + cv.Optional(conf_key): automation.validate_automation({}) + for conf_key, _ in _STATE_TRIGGERS } ) diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index 031f6657f4..658381ef90 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -71,32 +71,27 @@ template class VolumeSetAction : public Action, public Pa void play(const Ts &...x) override { this->parent_->make_call().set_volume(this->volume_.value(x...)).perform(); } }; -class StateTrigger : public Trigger<> { - public: - explicit StateTrigger(MediaPlayer *player) { - player->add_on_state_callback([this]() { this->trigger(); }); +/// Callback forwarder that triggers an Automation<> on any state change. +/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_. +struct StateAnyForwarder { + Automation<> *automation; + void operator()(MediaPlayerState /*state*/) const { this->automation->trigger(); } +}; + +/// Callback forwarder that triggers an Automation<> only when a specific media player state is entered. +/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_. +template struct StateEnterForwarder { + Automation<> *automation; + void operator()(MediaPlayerState state) const { + if (state == State) + this->automation->trigger(); } }; -template class MediaPlayerStateTrigger : public Trigger<> { - public: - explicit MediaPlayerStateTrigger(MediaPlayer *player) : player_(player) { - player->add_on_state_callback([this]() { - if (this->player_->state == State) - this->trigger(); - }); - } - - protected: - MediaPlayer *player_; -}; - -using IdleTrigger = MediaPlayerStateTrigger; -using PlayTrigger = MediaPlayerStateTrigger; -using PauseTrigger = MediaPlayerStateTrigger; -using AnnouncementTrigger = MediaPlayerStateTrigger; -using OnTrigger = MediaPlayerStateTrigger; -using OffTrigger = MediaPlayerStateTrigger; +static_assert(sizeof(StateAnyForwarder) <= sizeof(void *)); +static_assert(std::is_trivially_copyable_v); +static_assert(sizeof(StateEnterForwarder) <= sizeof(void *)); +static_assert(std::is_trivially_copyable_v>); template class IsIdleCondition : public Condition, public Parented { public: diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index a0eb7b5500..48d23fa0b1 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -199,7 +199,7 @@ MediaPlayerCall &MediaPlayerCall::set_announcement(bool announce) { } void MediaPlayer::publish_state() { - this->state_callback_.call(); + this->state_callback_.call(this->state); #if defined(USE_MEDIA_PLAYER) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_media_player_update(this); #endif diff --git a/esphome/components/media_player/media_player.h b/esphome/components/media_player/media_player.h index 26eca469e7..d5d0020797 100644 --- a/esphome/components/media_player/media_player.h +++ b/esphome/components/media_player/media_player.h @@ -168,7 +168,7 @@ class MediaPlayer : public EntityBase { virtual void control(const MediaPlayerCall &call) = 0; - LazyCallbackManager state_callback_{}; + LazyCallbackManager state_callback_{}; }; } // namespace media_player diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 15124e422f..ddce606b2c 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -39,8 +39,8 @@ void VoiceAssistant::setup() { #ifdef USE_MEDIA_PLAYER if (this->media_player_ != nullptr) { - this->media_player_->add_on_state_callback([this]() { - switch (this->media_player_->state) { + this->media_player_->add_on_state_callback([this](media_player::MediaPlayerState state) { + switch (state) { case media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING: if (this->media_player_response_state_ == MediaPlayerResponseState::URL_SENT) { // State changed to announcing after receiving the url From a2d452684a0cc6620e0a3e1bce746b0ef6a80ff3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:21:03 -1000 Subject: [PATCH 077/115] [ld2450] Migrate LD2450DataTrigger to callback automation (#15201) Co-authored-by: Claude Opus 4.6 (1M context) --- esphome/components/ld2450/__init__.py | 14 +++++--------- esphome/components/ld2450/ld2450.h | 8 -------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/esphome/components/ld2450/__init__.py b/esphome/components/ld2450/__init__.py index 5854a5794c..37bf12bafc 100644 --- a/esphome/components/ld2450/__init__.py +++ b/esphome/components/ld2450/__init__.py @@ -2,7 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE AUTO_LOAD = ["ld24xx"] DEPENDENCIES = ["uart"] @@ -12,7 +12,6 @@ MULTI_CONF = True ld2450_ns = cg.esphome_ns.namespace("ld2450") LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice) -LD2450DataTrigger = ld2450_ns.class_("LD2450DataTrigger", automation.Trigger.template()) CONF_LD2450_ID = "ld2450_id" @@ -23,11 +22,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_THROTTLE): cv.invalid( f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead" ), - cv.Optional(CONF_ON_DATA): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LD2450DataTrigger), - } - ), + cv.Optional(CONF_ON_DATA): automation.validate_automation({}), } ) .extend(uart.UART_DEVICE_SCHEMA) @@ -54,5 +49,6 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) for conf in config.get(CONF_ON_DATA, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_data_callback", [], conf + ) diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index e774dd9c75..cbcdec10b3 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -1,6 +1,5 @@ #pragma once -#include "esphome/core/automation.h" #include "esphome/core/defines.h" #include "esphome/core/component.h" #ifdef USE_SENSOR @@ -201,11 +200,4 @@ class LD2450Component : public Component, public uart::UARTDevice { LazyCallbackManager data_callback_; }; -class LD2450DataTrigger : public Trigger<> { - public: - explicit LD2450DataTrigger(LD2450Component *parent) { - parent->add_on_data_callback([this]() { this->trigger(); }); - } -}; - } // namespace esphome::ld2450 From 83b3187126be87ac2d7a97db9dd340d8679180e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:21:16 -1000 Subject: [PATCH 078/115] [rtttl] Migrate FinishedPlaybackTrigger to callback automation (#15202) --- esphome/components/rtttl/__init__.py | 25 +++++-------------------- esphome/components/rtttl/rtttl.h | 7 ------- 2 files changed, 5 insertions(+), 27 deletions(-) diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py index 3566734200..638e950ba6 100644 --- a/esphome/components/rtttl/__init__.py +++ b/esphome/components/rtttl/__init__.py @@ -5,14 +5,7 @@ import esphome.codegen as cg from esphome.components.output import FloatOutput from esphome.components.speaker import Speaker import esphome.config_validation as cv -from esphome.const import ( - CONF_GAIN, - CONF_ID, - CONF_OUTPUT, - CONF_PLATFORM, - CONF_SPEAKER, - CONF_TRIGGER_ID, -) +from esphome.const import CONF_GAIN, CONF_ID, CONF_OUTPUT, CONF_PLATFORM, CONF_SPEAKER import esphome.final_validate as fv _LOGGER = logging.getLogger(__name__) @@ -26,9 +19,6 @@ rtttl_ns = cg.esphome_ns.namespace("rtttl") Rtttl = rtttl_ns.class_("Rtttl", cg.Component) PlayAction = rtttl_ns.class_("PlayAction", automation.Action) StopAction = rtttl_ns.class_("StopAction", automation.Action) -FinishedPlaybackTrigger = rtttl_ns.class_( - "FinishedPlaybackTrigger", automation.Trigger.template() -) IsPlayingCondition = rtttl_ns.class_("IsPlayingCondition", automation.Condition) MULTI_CONF = True @@ -40,13 +30,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_OUTPUT): cv.use_id(FloatOutput), cv.Optional(CONF_SPEAKER): cv.use_id(Speaker), cv.Optional(CONF_GAIN, default="0.6"): cv.percentage, - cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FinishedPlaybackTrigger - ), - } - ), + cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation({}), } ).extend(cv.COMPONENT_SCHEMA), cv.has_exactly_one_key(CONF_OUTPUT, CONF_SPEAKER), @@ -103,8 +87,9 @@ async def to_code(config): cg.add(var.set_gain(config[CONF_GAIN])) for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_finished_playback_callback", [], conf + ) @automation.register_action( diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index bff43d2edd..98ed9ba1bf 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -131,11 +131,4 @@ template class IsPlayingCondition : public Condition, pub bool check(const Ts &...x) override { return this->parent_->is_playing(); } }; -class FinishedPlaybackTrigger : public Trigger<> { - public: - explicit FinishedPlaybackTrigger(Rtttl *parent) { - parent->add_on_finished_playback_callback([this]() { this->trigger(); }); - } -}; - } // namespace esphome::rtttl From 4493d2efb6582f020b6ed73879ab56566c088779 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:21:27 -1000 Subject: [PATCH 079/115] [online_image] Migrate triggers to callback automation (#15216) --- esphome/components/online_image/__init__.py | 41 ++++--------------- .../components/online_image/online_image.h | 14 ------- 2 files changed, 9 insertions(+), 46 deletions(-) diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index 292e2bb3bb..5b8294c70e 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -7,14 +7,7 @@ from esphome.components.const import CONF_REQUEST_HEADERS from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent from esphome.components.image import CONF_TRANSPARENCY, add_metadata import esphome.config_validation as cv -from esphome.const import ( - CONF_BUFFER_SIZE, - CONF_ID, - CONF_ON_ERROR, - CONF_TRIGGER_ID, - CONF_TYPE, - CONF_URL, -) +from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_ON_ERROR, CONF_TYPE, CONF_URL from esphome.core import Lambda AUTO_LOAD = ["image", "runtime_image"] @@ -41,14 +34,6 @@ ReleaseImageAction = online_image_ns.class_( "OnlineImageReleaseAction", automation.Action, cg.Parented.template(OnlineImage) ) -# Triggers -DownloadFinishedTrigger = online_image_ns.class_( - "DownloadFinishedTrigger", automation.Trigger.template() -) -DownloadErrorTrigger = online_image_ns.class_( - "DownloadErrorTrigger", automation.Trigger.template() -) - ONLINE_IMAGE_SCHEMA = ( runtime_image.runtime_image_schema(OnlineImage) @@ -61,18 +46,8 @@ ONLINE_IMAGE_SCHEMA = ( cv.Optional(CONF_REQUEST_HEADERS): cv.All( cv.Schema({cv.string: cv.templatable(cv.string)}) ), - cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - DownloadFinishedTrigger - ), - } - ), - cv.Optional(CONF_ON_ERROR): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadErrorTrigger), - } - ), + cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation({}), + cv.Optional(CONF_ON_ERROR): automation.validate_automation({}), } ) .extend(cv.polling_component_schema("never")) @@ -165,9 +140,11 @@ async def to_code(config): cg.add(var.add_request_header(key, value)) for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(bool, "cached")], conf) + await automation.build_callback_automation( + var, "add_on_finished_callback", [(bool, "cached")], conf + ) for conf in config.get(CONF_ON_ERROR, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_error_callback", [], conf + ) diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 3a348cbb07..816d6525ea 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -129,18 +129,4 @@ template class OnlineImageReleaseAction : public Action { OnlineImage *parent_; }; -class DownloadFinishedTrigger : public Trigger { - public: - explicit DownloadFinishedTrigger(OnlineImage *parent) { - parent->add_on_finished_callback([this](bool cached) { this->trigger(cached); }); - } -}; - -class DownloadErrorTrigger : public Trigger<> { - public: - explicit DownloadErrorTrigger(OnlineImage *parent) { - parent->add_on_error_callback([this]() { this->trigger(); }); - } -}; - } // namespace esphome::online_image From 54283a2599cf5f6958bbb329745afd0dbb800f2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:21:41 -1000 Subject: [PATCH 080/115] [rotary_encoder] Migrate triggers to callback automation (#15217) --- .../rotary_encoder/rotary_encoder.h | 14 -------- esphome/components/rotary_encoder/sensor.py | 34 +++++-------------- 2 files changed, 8 insertions(+), 40 deletions(-) diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index 4b776fe55e..6f4a4fd83c 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -118,19 +118,5 @@ template class RotaryEncoderSetValueAction : public Action { - public: - explicit RotaryEncoderClockwiseTrigger(RotaryEncoderSensor *parent) { - parent->add_on_clockwise_callback([this]() { this->trigger(); }); - } -}; - -class RotaryEncoderAnticlockwiseTrigger : public Trigger<> { - public: - explicit RotaryEncoderAnticlockwiseTrigger(RotaryEncoderSensor *parent) { - parent->add_on_anticlockwise_callback([this]() { this->trigger(); }); - } -}; - } // namespace rotary_encoder } // namespace esphome diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index be315db55d..e64e44f7c1 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -10,7 +10,6 @@ from esphome.const import ( CONF_PIN_B, CONF_RESOLUTION, CONF_RESTORE_MODE, - CONF_TRIGGER_ID, CONF_VALUE, ICON_ROTATE_RIGHT, UNIT_STEPS, @@ -43,13 +42,6 @@ RotaryEncoderSetValueAction = rotary_encoder_ns.class_( "RotaryEncoderSetValueAction", automation.Action ) -RotaryEncoderClockwiseTrigger = rotary_encoder_ns.class_( - "RotaryEncoderClockwiseTrigger", automation.Trigger -) -RotaryEncoderAnticlockwiseTrigger = rotary_encoder_ns.class_( - "RotaryEncoderAnticlockwiseTrigger", automation.Trigger -) - def validate_min_max_value(config): if CONF_MIN_VALUE in config and CONF_MAX_VALUE in config: @@ -81,20 +73,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_RESTORE_MODE, default="RESTORE_DEFAULT_ZERO"): cv.enum( RESTORE_MODES, upper=True, space="_" ), - cv.Optional(CONF_ON_CLOCKWISE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - RotaryEncoderClockwiseTrigger - ), - } - ), - cv.Optional(CONF_ON_ANTICLOCKWISE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - RotaryEncoderAnticlockwiseTrigger - ), - } - ), + cv.Optional(CONF_ON_CLOCKWISE): automation.validate_automation({}), + cv.Optional(CONF_ON_ANTICLOCKWISE): automation.validate_automation({}), } ) .extend(cv.COMPONENT_SCHEMA), @@ -123,11 +103,13 @@ async def to_code(config): cg.add(var.set_max_value(config[CONF_MAX_VALUE])) for conf in config.get(CONF_ON_CLOCKWISE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_clockwise_callback", [], conf + ) for conf in config.get(CONF_ON_ANTICLOCKWISE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_anticlockwise_callback", [], conf + ) @automation.register_action( From 514df6c99af94915523d26e45ad489d4a5ff60d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:21:52 -1000 Subject: [PATCH 081/115] [dfplayer] Migrate FinishedPlaybackTrigger to callback automation (#15218) --- esphome/components/dfplayer/__init__.py | 18 +++++------------- esphome/components/dfplayer/dfplayer.h | 7 ------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index 9df108c9c0..c49420f060 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -2,16 +2,13 @@ from esphome import automation import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv -from esphome.const import CONF_DEVICE, CONF_FILE, CONF_ID, CONF_TRIGGER_ID, CONF_VOLUME +from esphome.const import CONF_DEVICE, CONF_FILE, CONF_ID, CONF_VOLUME DEPENDENCIES = ["uart"] CODEOWNERS = ["@glmnet"] dfplayer_ns = cg.esphome_ns.namespace("dfplayer") DFPlayer = dfplayer_ns.class_("DFPlayer", cg.Component) -DFPlayerFinishedPlaybackTrigger = dfplayer_ns.class_( - "DFPlayerFinishedPlaybackTrigger", automation.Trigger.template() -) DFPlayerIsPlayingCondition = dfplayer_ns.class_( "DFPlayerIsPlayingCondition", automation.Condition ) @@ -58,13 +55,7 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(DFPlayer), - cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - DFPlayerFinishedPlaybackTrigger - ), - } - ), + cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation({}), } ).extend(uart.UART_DEVICE_SCHEMA) ) @@ -79,8 +70,9 @@ async def to_code(config): await uart.register_uart_device(var, config) for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_finished_playback_callback", [], conf + ) @automation.register_action( diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index 2c4ee03470..0d240566c3 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -171,12 +171,5 @@ template class DFPlayerIsPlayingCondition : public Conditionparent_->is_playing(); } }; -class DFPlayerFinishedPlaybackTrigger : public Trigger<> { - public: - explicit DFPlayerFinishedPlaybackTrigger(DFPlayer *parent) { - parent->add_on_finished_playback_callback([this]() { this->trigger(); }); - } -}; - } // namespace dfplayer } // namespace esphome From 623408bbfe2bff8c41964b73afa2a23fbd208e10 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:22:02 -1000 Subject: [PATCH 082/115] [hlk_fm22x] Migrate triggers to callback automation (#15219) --- esphome/components/hlk_fm22x/__init__.py | 109 ++++++----------------- esphome/components/hlk_fm22x/hlk_fm22x.h | 46 ---------- 2 files changed, 28 insertions(+), 127 deletions(-) diff --git a/esphome/components/hlk_fm22x/__init__.py b/esphome/components/hlk_fm22x/__init__.py index cb6d5cdfd6..c0349319d1 100644 --- a/esphome/components/hlk_fm22x/__init__.py +++ b/esphome/components/hlk_fm22x/__init__.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_NAME, CONF_ON_ENROLLMENT_DONE, CONF_ON_ENROLLMENT_FAILED, - CONF_TRIGGER_ID, ) CODEOWNERS = ["@OnFreund"] @@ -28,33 +27,6 @@ HlkFm22xComponent = hlk_fm22x_ns.class_( "HlkFm22xComponent", cg.PollingComponent, uart.UARTDevice ) -FaceScanMatchedTrigger = hlk_fm22x_ns.class_( - "FaceScanMatchedTrigger", automation.Trigger.template(cg.int16, cg.std_string) -) - -FaceScanUnmatchedTrigger = hlk_fm22x_ns.class_( - "FaceScanUnmatchedTrigger", automation.Trigger.template() -) - -FaceScanInvalidTrigger = hlk_fm22x_ns.class_( - "FaceScanInvalidTrigger", automation.Trigger.template(cg.uint8) -) - -FaceInfoTrigger = hlk_fm22x_ns.class_( - "FaceInfoTrigger", - automation.Trigger.template( - cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16 - ), -) - -EnrollmentDoneTrigger = hlk_fm22x_ns.class_( - "EnrollmentDoneTrigger", automation.Trigger.template(cg.int16, cg.uint8) -) - -EnrollmentFailedTrigger = hlk_fm22x_ns.class_( - "EnrollmentFailedTrigger", automation.Trigger.template(cg.uint8) -) - EnrollmentAction = hlk_fm22x_ns.class_("EnrollmentAction", automation.Action) DeleteAction = hlk_fm22x_ns.class_("DeleteAction", automation.Action) DeleteAllAction = hlk_fm22x_ns.class_("DeleteAllAction", automation.Action) @@ -65,46 +37,14 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(HlkFm22xComponent), - cv.Optional(CONF_ON_FACE_SCAN_MATCHED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FaceScanMatchedTrigger - ), - } - ), + cv.Optional(CONF_ON_FACE_SCAN_MATCHED): automation.validate_automation({}), cv.Optional(CONF_ON_FACE_SCAN_UNMATCHED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FaceScanUnmatchedTrigger - ), - } - ), - cv.Optional(CONF_ON_FACE_SCAN_INVALID): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FaceScanInvalidTrigger - ), - } - ), - cv.Optional(CONF_ON_FACE_INFO): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FaceInfoTrigger), - } - ), - cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - EnrollmentDoneTrigger - ), - } - ), - cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - EnrollmentFailedTrigger - ), - } + {} ), + cv.Optional(CONF_ON_FACE_SCAN_INVALID): automation.validate_automation({}), + cv.Optional(CONF_ON_FACE_INFO): automation.validate_automation({}), + cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation({}), + cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation({}), } ) .extend(cv.polling_component_schema("50ms")) @@ -118,23 +58,27 @@ async def to_code(config): await uart.register_uart_device(var, config) for conf in config.get(CONF_ON_FACE_SCAN_MATCHED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.int16, "face_id"), (cg.std_string, "name")], conf + await automation.build_callback_automation( + var, + "add_on_face_scan_matched_callback", + [(cg.int16, "face_id"), (cg.std_string, "name")], + conf, ) for conf in config.get(CONF_ON_FACE_SCAN_UNMATCHED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_face_scan_unmatched_callback", [], conf + ) for conf in config.get(CONF_ON_FACE_SCAN_INVALID, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.uint8, "error")], conf) + await automation.build_callback_automation( + var, "add_on_face_scan_invalid_callback", [(cg.uint8, "error")], conf + ) for conf in config.get(CONF_ON_FACE_INFO, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, + await automation.build_callback_automation( + var, + "add_on_face_info_callback", [ (cg.int16, "status"), (cg.int16, "left"), @@ -149,14 +93,17 @@ async def to_code(config): ) for conf in config.get(CONF_ON_ENROLLMENT_DONE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.int16, "face_id"), (cg.uint8, "direction")], conf + await automation.build_callback_automation( + var, + "add_on_enrollment_done_callback", + [(cg.int16, "face_id"), (cg.uint8, "direction")], + conf, ) for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.uint8, "error")], conf) + await automation.build_callback_automation( + var, "add_on_enrollment_failed_callback", [(cg.uint8, "error")], conf + ) @automation.register_action( diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.h b/esphome/components/hlk_fm22x/hlk_fm22x.h index d897d51881..fd8257b435 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.h +++ b/esphome/components/hlk_fm22x/hlk_fm22x.h @@ -141,52 +141,6 @@ class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice { CallbackManager enrollment_failed_callback_; }; -class FaceScanMatchedTrigger : public Trigger { - public: - explicit FaceScanMatchedTrigger(HlkFm22xComponent *parent) { - parent->add_on_face_scan_matched_callback( - [this](int16_t face_id, const std::string &name) { this->trigger(face_id, name); }); - } -}; - -class FaceScanUnmatchedTrigger : public Trigger<> { - public: - explicit FaceScanUnmatchedTrigger(HlkFm22xComponent *parent) { - parent->add_on_face_scan_unmatched_callback([this]() { this->trigger(); }); - } -}; - -class FaceScanInvalidTrigger : public Trigger { - public: - explicit FaceScanInvalidTrigger(HlkFm22xComponent *parent) { - parent->add_on_face_scan_invalid_callback([this](uint8_t error) { this->trigger(error); }); - } -}; - -class FaceInfoTrigger : public Trigger { - public: - explicit FaceInfoTrigger(HlkFm22xComponent *parent) { - parent->add_on_face_info_callback( - [this](int16_t status, int16_t left, int16_t top, int16_t right, int16_t bottom, int16_t yaw, int16_t pitch, - int16_t roll) { this->trigger(status, left, top, right, bottom, yaw, pitch, roll); }); - } -}; - -class EnrollmentDoneTrigger : public Trigger { - public: - explicit EnrollmentDoneTrigger(HlkFm22xComponent *parent) { - parent->add_on_enrollment_done_callback( - [this](int16_t face_id, uint8_t direction) { this->trigger(face_id, direction); }); - } -}; - -class EnrollmentFailedTrigger : public Trigger { - public: - explicit EnrollmentFailedTrigger(HlkFm22xComponent *parent) { - parent->add_on_enrollment_failed_callback([this](uint8_t error) { this->trigger(error); }); - } -}; - template class EnrollmentAction : public Action, public Parented { public: TEMPLATABLE_VALUE(std::string, name) From a4a8fa3027088c2a9c3ef8cc96c004cfabfe36b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:22:14 -1000 Subject: [PATCH 083/115] [pn532] Migrate PN532OnFinishedWriteTrigger to callback automation (#15220) --- esphome/components/pn532/__init__.py | 17 ++++------------- esphome/components/pn532/pn532.h | 7 ------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/esphome/components/pn532/__init__.py b/esphome/components/pn532/__init__.py index 6f679ed10a..4ccda49a72 100644 --- a/esphome/components/pn532/__init__.py +++ b/esphome/components/pn532/__init__.py @@ -19,10 +19,6 @@ CONF_PN532_ID = "pn532_id" pn532_ns = cg.esphome_ns.namespace("pn532") PN532 = pn532_ns.class_("PN532", cg.PollingComponent) -PN532OnFinishedWriteTrigger = pn532_ns.class_( - "PN532OnFinishedWriteTrigger", automation.Trigger.template() -) - PN532IsWritingCondition = pn532_ns.class_( "PN532IsWritingCondition", automation.Condition ) @@ -35,13 +31,7 @@ PN532_SCHEMA = cv.Schema( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), } ), - cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - PN532OnFinishedWriteTrigger - ), - } - ), + cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation({}), cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), @@ -77,8 +67,9 @@ async def setup_pn532(var, config): ) for conf in config.get(CONF_ON_FINISHED_WRITE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_finished_write_callback", [], conf + ) @automation.register_condition( diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index 1f6a6b3bc3..b76cbb1946 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -133,13 +133,6 @@ class PN532BinarySensor : public binary_sensor::BinarySensor { bool found_{false}; }; -class PN532OnFinishedWriteTrigger : public Trigger<> { - public: - explicit PN532OnFinishedWriteTrigger(PN532 *parent) { - parent->add_on_finished_write_callback([this]() { this->trigger(); }); - } -}; - template class PN532IsWritingCondition : public Condition, public Parented { public: bool check(const Ts &...x) override { return this->parent_->is_writing(); } From 985477f2cfa40cc31a97f3169364b73db4d34a91 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:22:25 -1000 Subject: [PATCH 084/115] [pn7150][pn7160] Migrate triggers to callback automation (#15221) --- esphome/components/pn7150/__init__.py | 34 ++++++-------------------- esphome/components/pn7150/automation.h | 14 ----------- esphome/components/pn7160/__init__.py | 34 ++++++-------------------- esphome/components/pn7160/automation.h | 14 ----------- 4 files changed, 16 insertions(+), 80 deletions(-) diff --git a/esphome/components/pn7150/__init__.py b/esphome/components/pn7150/__init__.py index 6af1412881..c8723dc31c 100644 --- a/esphome/components/pn7150/__init__.py +++ b/esphome/components/pn7150/__init__.py @@ -50,14 +50,6 @@ SetWriteMessageAction = pn7150_ns.class_("SetWriteMessageAction", automation.Act SetWriteModeAction = pn7150_ns.class_("SetWriteModeAction", automation.Action) -PN7150OnEmulatedTagScanTrigger = pn7150_ns.class_( - "PN7150OnEmulatedTagScanTrigger", automation.Trigger.template() -) - -PN7150OnFinishedWriteTrigger = pn7150_ns.class_( - "PN7150OnFinishedWriteTrigger", automation.Trigger.template() -) - PN7150IsWritingCondition = pn7150_ns.class_( "PN7150IsWritingCondition", automation.Condition ) @@ -83,20 +75,8 @@ SET_MESSAGE_ACTION_SCHEMA = cv.Schema( PN7150_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(PN7150), - cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - PN7150OnEmulatedTagScanTrigger - ), - } - ), - cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - PN7150OnFinishedWriteTrigger - ), - } - ), + cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation({}), + cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation({}), cv.Optional(CONF_ON_TAG): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), @@ -215,12 +195,14 @@ async def setup_pn7150(var, config): ) for conf in config.get(CONF_ON_EMULATED_TAG_SCAN, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_emulated_tag_scan_callback", [], conf + ) for conf in config.get(CONF_ON_FINISHED_WRITE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_finished_write_callback", [], conf + ) @automation.register_condition( diff --git a/esphome/components/pn7150/automation.h b/esphome/components/pn7150/automation.h index 21329a998a..a8c65ae633 100644 --- a/esphome/components/pn7150/automation.h +++ b/esphome/components/pn7150/automation.h @@ -7,20 +7,6 @@ namespace esphome { namespace pn7150 { -class PN7150OnEmulatedTagScanTrigger : public Trigger<> { - public: - explicit PN7150OnEmulatedTagScanTrigger(PN7150 *parent) { - parent->add_on_emulated_tag_scan_callback([this]() { this->trigger(); }); - } -}; - -class PN7150OnFinishedWriteTrigger : public Trigger<> { - public: - explicit PN7150OnFinishedWriteTrigger(PN7150 *parent) { - parent->add_on_finished_write_callback([this]() { this->trigger(); }); - } -}; - template class PN7150IsWritingCondition : public Condition, public Parented { public: bool check(const Ts &...x) override { return this->parent_->is_writing(); } diff --git a/esphome/components/pn7160/__init__.py b/esphome/components/pn7160/__init__.py index 54e4b74796..e382594b93 100644 --- a/esphome/components/pn7160/__init__.py +++ b/esphome/components/pn7160/__init__.py @@ -52,14 +52,6 @@ SetWriteMessageAction = pn7160_ns.class_("SetWriteMessageAction", automation.Act SetWriteModeAction = pn7160_ns.class_("SetWriteModeAction", automation.Action) -PN7160OnEmulatedTagScanTrigger = pn7160_ns.class_( - "PN7160OnEmulatedTagScanTrigger", automation.Trigger.template() -) - -PN7160OnFinishedWriteTrigger = pn7160_ns.class_( - "PN7160OnFinishedWriteTrigger", automation.Trigger.template() -) - PN7160IsWritingCondition = pn7160_ns.class_( "PN7160IsWritingCondition", automation.Condition ) @@ -85,20 +77,8 @@ SET_MESSAGE_ACTION_SCHEMA = cv.Schema( PN7160_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(PN7160), - cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - PN7160OnEmulatedTagScanTrigger - ), - } - ), - cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - PN7160OnFinishedWriteTrigger - ), - } - ), + cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation({}), + cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation({}), cv.Optional(CONF_ON_TAG): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), @@ -227,12 +207,14 @@ async def setup_pn7160(var, config): ) for conf in config.get(CONF_ON_EMULATED_TAG_SCAN, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_emulated_tag_scan_callback", [], conf + ) for conf in config.get(CONF_ON_FINISHED_WRITE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_finished_write_callback", [], conf + ) @automation.register_condition( diff --git a/esphome/components/pn7160/automation.h b/esphome/components/pn7160/automation.h index 08148c2311..7759da8f53 100644 --- a/esphome/components/pn7160/automation.h +++ b/esphome/components/pn7160/automation.h @@ -7,20 +7,6 @@ namespace esphome { namespace pn7160 { -class PN7160OnEmulatedTagScanTrigger : public Trigger<> { - public: - explicit PN7160OnEmulatedTagScanTrigger(PN7160 *parent) { - parent->add_on_emulated_tag_scan_callback([this]() { this->trigger(); }); - } -}; - -class PN7160OnFinishedWriteTrigger : public Trigger<> { - public: - explicit PN7160OnFinishedWriteTrigger(PN7160 *parent) { - parent->add_on_finished_write_callback([this]() { this->trigger(); }); - } -}; - template class PN7160IsWritingCondition : public Condition, public Parented { public: bool check(const Ts &...x) override { return this->parent_->is_writing(); } From a5416df6155172ff80869caa8e183cd58e552e18 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:22:36 -1000 Subject: [PATCH 085/115] [sim800l] Migrate triggers to callback automation (#15222) --- esphome/components/sim800l/__init__.py | 93 +++++++------------------- esphome/components/sim800l/sim800l.h | 35 ---------- 2 files changed, 23 insertions(+), 105 deletions(-) diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index ebb74302a9..91771047e1 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -2,7 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_MESSAGE, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_MESSAGE DEPENDENCIES = ["uart"] CODEOWNERS = ["@glmnet"] @@ -11,28 +11,6 @@ MULTI_CONF = True sim800l_ns = cg.esphome_ns.namespace("sim800l") Sim800LComponent = sim800l_ns.class_("Sim800LComponent", cg.Component) -Sim800LReceivedMessageTrigger = sim800l_ns.class_( - "Sim800LReceivedMessageTrigger", - automation.Trigger.template(cg.std_string, cg.std_string), -) -Sim800LIncomingCallTrigger = sim800l_ns.class_( - "Sim800LIncomingCallTrigger", - automation.Trigger.template(cg.std_string), -) -Sim800LCallConnectedTrigger = sim800l_ns.class_( - "Sim800LCallConnectedTrigger", - automation.Trigger.template(), -) -Sim800LCallDisconnectedTrigger = sim800l_ns.class_( - "Sim800LCallDisconnectedTrigger", - automation.Trigger.template(), -) - -Sim800LReceivedUssdTrigger = sim800l_ns.class_( - "Sim800LReceivedUssdTrigger", - automation.Trigger.template(cg.std_string), -) - # Actions Sim800LSendSmsAction = sim800l_ns.class_("Sim800LSendSmsAction", automation.Action) Sim800LSendUssdAction = sim800l_ns.class_("Sim800LSendUssdAction", automation.Action) @@ -55,41 +33,11 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(Sim800LComponent), - cv.Optional(CONF_ON_SMS_RECEIVED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Sim800LReceivedMessageTrigger - ), - } - ), - cv.Optional(CONF_ON_INCOMING_CALL): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Sim800LIncomingCallTrigger - ), - } - ), - cv.Optional(CONF_ON_CALL_CONNECTED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Sim800LCallConnectedTrigger - ), - } - ), - cv.Optional(CONF_ON_CALL_DISCONNECTED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Sim800LCallDisconnectedTrigger - ), - } - ), - cv.Optional(CONF_ON_USSD_RECEIVED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Sim800LReceivedUssdTrigger - ), - } - ), + cv.Optional(CONF_ON_SMS_RECEIVED): automation.validate_automation({}), + cv.Optional(CONF_ON_INCOMING_CALL): automation.validate_automation({}), + cv.Optional(CONF_ON_CALL_CONNECTED): automation.validate_automation({}), + cv.Optional(CONF_ON_CALL_DISCONNECTED): automation.validate_automation({}), + cv.Optional(CONF_ON_USSD_RECEIVED): automation.validate_automation({}), } ) .extend(cv.polling_component_schema("5s")) @@ -106,23 +54,28 @@ async def to_code(config): await uart.register_uart_device(var, config) for conf in config.get(CONF_ON_SMS_RECEIVED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.std_string, "message"), (cg.std_string, "sender")], conf + await automation.build_callback_automation( + var, + "add_on_sms_received_callback", + [(cg.std_string, "message"), (cg.std_string, "sender")], + conf, ) for conf in config.get(CONF_ON_INCOMING_CALL, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "caller_id")], conf) + await automation.build_callback_automation( + var, "add_on_incoming_call_callback", [(cg.std_string, "caller_id")], conf + ) for conf in config.get(CONF_ON_CALL_CONNECTED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_call_connected_callback", [], conf + ) for conf in config.get(CONF_ON_CALL_DISCONNECTED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_on_call_disconnected_callback", [], conf + ) for conf in config.get(CONF_ON_USSD_RECEIVED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "ussd")], conf) + await automation.build_callback_automation( + var, "add_on_ussd_received_callback", [(cg.std_string, "ussd")], conf + ) SIM800L_SEND_SMS_SCHEMA = cv.Schema( diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h index d79279ea72..d0da123039 100644 --- a/esphome/components/sim800l/sim800l.h +++ b/esphome/components/sim800l/sim800l.h @@ -121,41 +121,6 @@ class Sim800LComponent : public uart::UARTDevice, public PollingComponent { CallbackManager ussd_received_callback_; }; -class Sim800LReceivedMessageTrigger : public Trigger { - public: - explicit Sim800LReceivedMessageTrigger(Sim800LComponent *parent) { - parent->add_on_sms_received_callback( - [this](const std::string &message, const std::string &sender) { this->trigger(message, sender); }); - } -}; - -class Sim800LIncomingCallTrigger : public Trigger { - public: - explicit Sim800LIncomingCallTrigger(Sim800LComponent *parent) { - parent->add_on_incoming_call_callback([this](const std::string &caller_id) { this->trigger(caller_id); }); - } -}; - -class Sim800LCallConnectedTrigger : public Trigger<> { - public: - explicit Sim800LCallConnectedTrigger(Sim800LComponent *parent) { - parent->add_on_call_connected_callback([this]() { this->trigger(); }); - } -}; - -class Sim800LCallDisconnectedTrigger : public Trigger<> { - public: - explicit Sim800LCallDisconnectedTrigger(Sim800LComponent *parent) { - parent->add_on_call_disconnected_callback([this]() { this->trigger(); }); - } -}; -class Sim800LReceivedUssdTrigger : public Trigger { - public: - explicit Sim800LReceivedUssdTrigger(Sim800LComponent *parent) { - parent->add_on_ussd_received_callback([this](const std::string &ussd) { this->trigger(ussd); }); - } -}; - template class Sim800LSendSmsAction : public Action { public: Sim800LSendSmsAction(Sim800LComponent *parent) : parent_(parent) {} From 6ffb5af60ced80ff47720fc572c4476475954f97 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:22:47 -1000 Subject: [PATCH 086/115] [fingerprint_grow] Migrate triggers to callback automation (#15223) --- .../components/fingerprint_grow/__init__.py | 142 +++++------------- .../fingerprint_grow/fingerprint_grow.h | 58 ------- 2 files changed, 36 insertions(+), 164 deletions(-) diff --git a/esphome/components/fingerprint_grow/__init__.py b/esphome/components/fingerprint_grow/__init__.py index 2637097be8..0b01ba7cab 100644 --- a/esphome/components/fingerprint_grow/__init__.py +++ b/esphome/components/fingerprint_grow/__init__.py @@ -21,7 +21,6 @@ from esphome.const import ( CONF_SENSING_PIN, CONF_SPEED, CONF_STATE, - CONF_TRIGGER_ID, ) CODEOWNERS = ["@OnFreund", "@loongyh", "@alexborro"] @@ -38,38 +37,6 @@ FingerprintGrowComponent = fingerprint_grow_ns.class_( "FingerprintGrowComponent", cg.PollingComponent, uart.UARTDevice ) -FingerScanStartTrigger = fingerprint_grow_ns.class_( - "FingerScanStartTrigger", automation.Trigger.template() -) - -FingerScanMatchedTrigger = fingerprint_grow_ns.class_( - "FingerScanMatchedTrigger", automation.Trigger.template(cg.uint16, cg.uint16) -) - -FingerScanUnmatchedTrigger = fingerprint_grow_ns.class_( - "FingerScanUnmatchedTrigger", automation.Trigger.template() -) - -FingerScanMisplacedTrigger = fingerprint_grow_ns.class_( - "FingerScanMisplacedTrigger", automation.Trigger.template() -) - -FingerScanInvalidTrigger = fingerprint_grow_ns.class_( - "FingerScanInvalidTrigger", automation.Trigger.template() -) - -EnrollmentScanTrigger = fingerprint_grow_ns.class_( - "EnrollmentScanTrigger", automation.Trigger.template(cg.uint8, cg.uint16) -) - -EnrollmentDoneTrigger = fingerprint_grow_ns.class_( - "EnrollmentDoneTrigger", automation.Trigger.template(cg.uint16) -) - -EnrollmentFailedTrigger = fingerprint_grow_ns.class_( - "EnrollmentFailedTrigger", automation.Trigger.template(cg.uint16) -) - EnrollmentAction = fingerprint_grow_ns.class_("EnrollmentAction", automation.Action) CancelEnrollmentAction = fingerprint_grow_ns.class_( "CancelEnrollmentAction", automation.Action @@ -125,62 +92,22 @@ CONFIG_SCHEMA = cv.All( ): cv.positive_time_period_milliseconds, cv.Optional(CONF_PASSWORD): cv.uint32_t, cv.Optional(CONF_NEW_PASSWORD): cv.uint32_t, - cv.Optional(CONF_ON_FINGER_SCAN_START): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FingerScanStartTrigger - ), - } - ), + cv.Optional(CONF_ON_FINGER_SCAN_START): automation.validate_automation({}), cv.Optional(CONF_ON_FINGER_SCAN_MATCHED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FingerScanMatchedTrigger - ), - } + {} ), cv.Optional(CONF_ON_FINGER_SCAN_UNMATCHED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FingerScanUnmatchedTrigger - ), - } + {} ), cv.Optional(CONF_ON_FINGER_SCAN_MISPLACED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FingerScanMisplacedTrigger - ), - } + {} ), cv.Optional(CONF_ON_FINGER_SCAN_INVALID): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FingerScanInvalidTrigger - ), - } - ), - cv.Optional(CONF_ON_ENROLLMENT_SCAN): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - EnrollmentScanTrigger - ), - } - ), - cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - EnrollmentDoneTrigger - ), - } - ), - cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - EnrollmentFailedTrigger - ), - } + {} ), + cv.Optional(CONF_ON_ENROLLMENT_SCAN): automation.validate_automation({}), + cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation({}), + cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation({}), } ) .extend(cv.polling_component_schema("500ms")) @@ -214,40 +141,43 @@ async def to_code(config): cg.add(var.set_idle_period_to_sleep_ms(idle_period_to_sleep_ms)) for conf in config.get(CONF_ON_FINGER_SCAN_START, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_on_finger_scan_start_callback", [], conf + ) for conf in config.get(CONF_ON_FINGER_SCAN_MATCHED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.uint16, "finger_id"), (cg.uint16, "confidence")], conf + await automation.build_callback_automation( + var, + "add_on_finger_scan_matched_callback", + [(cg.uint16, "finger_id"), (cg.uint16, "confidence")], + conf, ) - for conf in config.get(CONF_ON_FINGER_SCAN_UNMATCHED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_on_finger_scan_unmatched_callback", [], conf + ) for conf in config.get(CONF_ON_FINGER_SCAN_MISPLACED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_on_finger_scan_misplaced_callback", [], conf + ) for conf in config.get(CONF_ON_FINGER_SCAN_INVALID, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_on_finger_scan_invalid_callback", [], conf + ) for conf in config.get(CONF_ON_ENROLLMENT_SCAN, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.uint8, "scan_num"), (cg.uint16, "finger_id")], conf + await automation.build_callback_automation( + var, + "add_on_enrollment_scan_callback", + [(cg.uint8, "scan_num"), (cg.uint16, "finger_id")], + conf, ) - for conf in config.get(CONF_ON_ENROLLMENT_DONE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.uint16, "finger_id")], conf) - + await automation.build_callback_automation( + var, "add_on_enrollment_done_callback", [(cg.uint16, "finger_id")], conf + ) for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.uint16, "finger_id")], conf) + await automation.build_callback_automation( + var, "add_on_enrollment_failed_callback", [(cg.uint16, "finger_id")], conf + ) @automation.register_action( diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index 63839534f6..947c701c98 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -210,64 +210,6 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic CallbackManager enrollment_failed_callback_; }; -class FingerScanStartTrigger : public Trigger<> { - public: - explicit FingerScanStartTrigger(FingerprintGrowComponent *parent) { - parent->add_on_finger_scan_start_callback([this]() { this->trigger(); }); - } -}; - -class FingerScanMatchedTrigger : public Trigger { - public: - explicit FingerScanMatchedTrigger(FingerprintGrowComponent *parent) { - parent->add_on_finger_scan_matched_callback( - [this](uint16_t finger_id, uint16_t confidence) { this->trigger(finger_id, confidence); }); - } -}; - -class FingerScanUnmatchedTrigger : public Trigger<> { - public: - explicit FingerScanUnmatchedTrigger(FingerprintGrowComponent *parent) { - parent->add_on_finger_scan_unmatched_callback([this]() { this->trigger(); }); - } -}; - -class FingerScanMisplacedTrigger : public Trigger<> { - public: - explicit FingerScanMisplacedTrigger(FingerprintGrowComponent *parent) { - parent->add_on_finger_scan_misplaced_callback([this]() { this->trigger(); }); - } -}; - -class FingerScanInvalidTrigger : public Trigger<> { - public: - explicit FingerScanInvalidTrigger(FingerprintGrowComponent *parent) { - parent->add_on_finger_scan_invalid_callback([this]() { this->trigger(); }); - } -}; - -class EnrollmentScanTrigger : public Trigger { - public: - explicit EnrollmentScanTrigger(FingerprintGrowComponent *parent) { - parent->add_on_enrollment_scan_callback( - [this](uint8_t scan_num, uint16_t finger_id) { this->trigger(scan_num, finger_id); }); - } -}; - -class EnrollmentDoneTrigger : public Trigger { - public: - explicit EnrollmentDoneTrigger(FingerprintGrowComponent *parent) { - parent->add_on_enrollment_done_callback([this](uint16_t finger_id) { this->trigger(finger_id); }); - } -}; - -class EnrollmentFailedTrigger : public Trigger { - public: - explicit EnrollmentFailedTrigger(FingerprintGrowComponent *parent) { - parent->add_on_enrollment_failed_callback([this](uint16_t finger_id) { this->trigger(finger_id); }); - } -}; - template class EnrollmentAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint16_t, finger_id) From a95f9f41fb418a66d9d3d550b69aa29d1fc303ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:22:58 -1000 Subject: [PATCH 087/115] [ltr_als_ps] Migrate triggers to callback automation (#15224) --- esphome/components/ltr_als_ps/ltr_als_ps.h | 36 +++++----------------- esphome/components/ltr_als_ps/sensor.py | 33 ++++++-------------- 2 files changed, 18 insertions(+), 51 deletions(-) diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.h b/esphome/components/ltr_als_ps/ltr_als_ps.h index 2e24a14283..8aa5c9f24b 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.h +++ b/esphome/components/ltr_als_ps/ltr_als_ps.h @@ -58,6 +58,14 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { void set_actual_integration_time_sensor(sensor::Sensor *sensor) { this->actual_integration_time_sensor_ = sensor; } void set_proximity_counts_sensor(sensor::Sensor *sensor) { this->proximity_counts_sensor_ = sensor; } + template void add_on_ps_high_trigger_callback(F &&callback) { + this->on_ps_high_trigger_callback_.add(std::forward(callback)); + } + + template void add_on_ps_low_trigger_callback(F &&callback) { + this->on_ps_low_trigger_callback_.add(std::forward(callback)); + } + protected: // // Internal state machine, used to split all the actions into @@ -151,36 +159,8 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { } bool is_any_ps_sensor_enabled_() const { return this->proximity_counts_sensor_ != nullptr; } - // - // Trigger section for the automations - // - friend class LTRPsHighTrigger; - friend class LTRPsLowTrigger; - CallbackManager on_ps_high_trigger_callback_; CallbackManager on_ps_low_trigger_callback_; - - template void add_on_ps_high_trigger_callback_(F &&callback) { - this->on_ps_high_trigger_callback_.add(std::forward(callback)); - } - - template void add_on_ps_low_trigger_callback_(F &&callback) { - this->on_ps_low_trigger_callback_.add(std::forward(callback)); - } -}; - -class LTRPsHighTrigger : public Trigger<> { - public: - explicit LTRPsHighTrigger(LTRAlsPsComponent *parent) { - parent->add_on_ps_high_trigger_callback_([this]() { this->trigger(); }); - } -}; - -class LTRPsLowTrigger : public Trigger<> { - public: - explicit LTRPsLowTrigger(LTRAlsPsComponent *parent) { - parent->add_on_ps_low_trigger_callback_([this]() { this->trigger(); }); - } }; } // namespace ltr_als_ps } // namespace esphome diff --git a/esphome/components/ltr_als_ps/sensor.py b/esphome/components/ltr_als_ps/sensor.py index 0dbcff1bfb..57503772a1 100644 --- a/esphome/components/ltr_als_ps/sensor.py +++ b/esphome/components/ltr_als_ps/sensor.py @@ -14,7 +14,6 @@ from esphome.const import ( CONF_INTEGRATION_TIME, CONF_NAME, CONF_REPEAT, - CONF_TRIGGER_ID, CONF_TYPE, DEVICE_CLASS_ILLUMINANCE, ICON_BRIGHTNESS_5, @@ -93,11 +92,6 @@ PS_GAINS = { "64X": PsGain.PS_GAIN_64, } -LTRPsHighTrigger = ltr_als_ps_ns.class_( - "LTRPsHighTrigger", automation.Trigger.template() -) -LTRPsLowTrigger = ltr_als_ps_ns.class_("LTRPsLowTrigger", automation.Trigger.template()) - def validate_integration_time(value): value = cv.positive_time_period_milliseconds(value).total_milliseconds @@ -143,16 +137,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PS_LOW_THRESHOLD, default=0): cv.int_range( min=0, max=65535 ), - cv.Optional(CONF_ON_PS_HIGH_THRESHOLD): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsHighTrigger), - } - ), - cv.Optional(CONF_ON_PS_LOW_THRESHOLD): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsLowTrigger), - } - ), + cv.Optional(CONF_ON_PS_HIGH_THRESHOLD): automation.validate_automation({}), + cv.Optional(CONF_ON_PS_LOW_THRESHOLD): automation.validate_automation({}), cv.Optional(CONF_AMBIENT_LIGHT): cv.maybe_simple_value( sensor.sensor_schema( unit_of_measurement=UNIT_LUX, @@ -244,13 +230,14 @@ async def to_code(config): sens = await sensor.new_sensor(prox_cnt_config) cg.add(var.set_proximity_counts_sensor(sens)) - for prox_high_tr in config.get(CONF_ON_PS_HIGH_THRESHOLD, []): - trigger = cg.new_Pvariable(prox_high_tr[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], prox_high_tr) - - for prox_low_tr in config.get(CONF_ON_PS_LOW_THRESHOLD, []): - trigger = cg.new_Pvariable(prox_low_tr[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], prox_low_tr) + for conf in config.get(CONF_ON_PS_HIGH_THRESHOLD, []): + await automation.build_callback_automation( + var, "add_on_ps_high_trigger_callback", [], conf + ) + for conf in config.get(CONF_ON_PS_LOW_THRESHOLD, []): + await automation.build_callback_automation( + var, "add_on_ps_low_trigger_callback", [], conf + ) cg.add(var.set_ltr_type(config[CONF_TYPE])) From a73c67e4763c971573e89c0d5b4f7e6971ef2341 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:23:17 -1000 Subject: [PATCH 088/115] [ltr501] Migrate triggers to callback automation (#15225) --- esphome/components/ltr501/ltr501.h | 36 +++++++---------------------- esphome/components/ltr501/sensor.py | 31 ++++++++----------------- 2 files changed, 18 insertions(+), 49 deletions(-) diff --git a/esphome/components/ltr501/ltr501.h b/esphome/components/ltr501/ltr501.h index 2bd838a0fe..2b91463108 100644 --- a/esphome/components/ltr501/ltr501.h +++ b/esphome/components/ltr501/ltr501.h @@ -58,6 +58,14 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { void set_actual_integration_time_sensor(sensor::Sensor *sensor) { this->actual_integration_time_sensor_ = sensor; } void set_proximity_counts_sensor(sensor::Sensor *sensor) { this->proximity_counts_sensor_ = sensor; } + template void add_on_ps_high_trigger_callback(F &&callback) { + this->on_ps_high_trigger_callback_.add(std::forward(callback)); + } + + template void add_on_ps_low_trigger_callback(F &&callback) { + this->on_ps_low_trigger_callback_.add(std::forward(callback)); + } + protected: // // Internal state machine, used to split all the actions into @@ -151,36 +159,8 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { } bool is_any_ps_sensor_enabled_() const { return this->proximity_counts_sensor_ != nullptr; } - // - // Trigger section for the automations - // - friend class LTRPsHighTrigger; - friend class LTRPsLowTrigger; - CallbackManager on_ps_high_trigger_callback_; CallbackManager on_ps_low_trigger_callback_; - - template void add_on_ps_high_trigger_callback_(F &&callback) { - this->on_ps_high_trigger_callback_.add(std::forward(callback)); - } - - template void add_on_ps_low_trigger_callback_(F &&callback) { - this->on_ps_low_trigger_callback_.add(std::forward(callback)); - } -}; - -class LTRPsHighTrigger : public Trigger<> { - public: - explicit LTRPsHighTrigger(LTRAlsPs501Component *parent) { - parent->add_on_ps_high_trigger_callback_([this]() { this->trigger(); }); - } -}; - -class LTRPsLowTrigger : public Trigger<> { - public: - explicit LTRPsLowTrigger(LTRAlsPs501Component *parent) { - parent->add_on_ps_low_trigger_callback_([this]() { this->trigger(); }); - } }; } // namespace ltr501 } // namespace esphome diff --git a/esphome/components/ltr501/sensor.py b/esphome/components/ltr501/sensor.py index adaf669a72..712810222c 100644 --- a/esphome/components/ltr501/sensor.py +++ b/esphome/components/ltr501/sensor.py @@ -14,7 +14,6 @@ from esphome.const import ( CONF_INTEGRATION_TIME, CONF_NAME, CONF_REPEAT, - CONF_TRIGGER_ID, CONF_TYPE, DEVICE_CLASS_DISTANCE, DEVICE_CLASS_ILLUMINANCE, @@ -87,9 +86,6 @@ PS_GAINS = { "16X": PsGain.PS_GAIN_16, } -LTRPsHighTrigger = ltr501_ns.class_("LTRPsHighTrigger", automation.Trigger.template()) -LTRPsLowTrigger = ltr501_ns.class_("LTRPsLowTrigger", automation.Trigger.template()) - def validate_integration_time(value): value = cv.positive_time_period_milliseconds(value).total_milliseconds @@ -146,16 +142,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PS_LOW_THRESHOLD, default=0): cv.int_range( min=0, max=65535 ), - cv.Optional(CONF_ON_PS_HIGH_THRESHOLD): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsHighTrigger), - } - ), - cv.Optional(CONF_ON_PS_LOW_THRESHOLD): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsLowTrigger), - } - ), + cv.Optional(CONF_ON_PS_HIGH_THRESHOLD): automation.validate_automation({}), + cv.Optional(CONF_ON_PS_LOW_THRESHOLD): automation.validate_automation({}), cv.Optional(CONF_AMBIENT_LIGHT): cv.maybe_simple_value( sensor.sensor_schema( unit_of_measurement=UNIT_LUX, @@ -252,13 +240,14 @@ async def to_code(config): sens = await sensor.new_sensor(prox_cnt_config) cg.add(var.set_proximity_counts_sensor(sens)) - for prox_high_tr in config.get(CONF_ON_PS_HIGH_THRESHOLD, []): - trigger = cg.new_Pvariable(prox_high_tr[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], prox_high_tr) - - for prox_low_tr in config.get(CONF_ON_PS_LOW_THRESHOLD, []): - trigger = cg.new_Pvariable(prox_low_tr[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], prox_low_tr) + for conf in config.get(CONF_ON_PS_HIGH_THRESHOLD, []): + await automation.build_callback_automation( + var, "add_on_ps_high_trigger_callback", [], conf + ) + for conf in config.get(CONF_ON_PS_LOW_THRESHOLD, []): + await automation.build_callback_automation( + var, "add_on_ps_low_trigger_callback", [], conf + ) cg.add(var.set_ltr_type(config[CONF_TYPE])) From f5cd1e5e76831637ecf987439e205243a32c1fb6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:23:26 -1000 Subject: [PATCH 089/115] [ld2450] Fix flaky integration test race condition (#15226) --- tests/integration/test_uart_mock_ld2450.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_uart_mock_ld2450.py b/tests/integration/test_uart_mock_ld2450.py index b1aa2f6952..2469273e0a 100644 --- a/tests/integration/test_uart_mock_ld2450.py +++ b/tests/integration/test_uart_mock_ld2450.py @@ -83,11 +83,18 @@ async def test_uart_mock_ld2450( ], ) - # Signal when we see recovery frame values (target 1 distance ≈ 500mm) + # Signal when we see all recovery frame values + # Must wait for ALL values to avoid race where some arrive after the waiter fires recovery_received = collector.add_waiter( lambda: ( pytest.approx(500.0, abs=1.0) in collector.sensor_states["target_1_distance"] + and pytest.approx(300.0) in collector.sensor_states["target_1_x"] + and pytest.approx(400.0) in collector.sensor_states["target_1_y"] + and pytest.approx(30.0) in collector.sensor_states["target_1_speed"] + and pytest.approx(1.0) in collector.sensor_states["target_count"] + and pytest.approx(1.0) in collector.sensor_states["moving_target_count"] + and pytest.approx(0.0) in collector.sensor_states["still_target_count"] ) ) From d77bf23c76b7257d20cd0c9541eabcf2613adf69 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:23:37 -1000 Subject: [PATCH 090/115] [nextion] Migrate triggers to callback automation (#15227) --- esphome/components/nextion/automation.h | 44 ------------ esphome/components/nextion/display.py | 90 +++++++------------------ 2 files changed, 25 insertions(+), 109 deletions(-) diff --git a/esphome/components/nextion/automation.h b/esphome/components/nextion/automation.h index 8e85e15823..9f52507d67 100644 --- a/esphome/components/nextion/automation.h +++ b/esphome/components/nextion/automation.h @@ -5,50 +5,6 @@ namespace esphome { namespace nextion { -class BufferOverflowTrigger : public Trigger<> { - public: - explicit BufferOverflowTrigger(Nextion *nextion) { - nextion->add_buffer_overflow_event_callback([this]() { this->trigger(); }); - } -}; - -class SetupTrigger : public Trigger<> { - public: - explicit SetupTrigger(Nextion *nextion) { - nextion->add_setup_state_callback([this]() { this->trigger(); }); - } -}; - -class SleepTrigger : public Trigger<> { - public: - explicit SleepTrigger(Nextion *nextion) { - nextion->add_sleep_state_callback([this]() { this->trigger(); }); - } -}; - -class WakeTrigger : public Trigger<> { - public: - explicit WakeTrigger(Nextion *nextion) { - nextion->add_wake_state_callback([this]() { this->trigger(); }); - } -}; - -class PageTrigger : public Trigger { - public: - explicit PageTrigger(Nextion *nextion) { - nextion->add_new_page_callback([this](const uint8_t page_id) { this->trigger(page_id); }); - } -}; - -class TouchTrigger : public Trigger { - public: - explicit TouchTrigger(Nextion *nextion) { - nextion->add_touch_event_callback([this](uint8_t page_id, uint8_t component_id, bool touch_event) { - this->trigger(page_id, component_id, touch_event); - }); - } -}; - template class NextionSetBrightnessAction : public Action { public: explicit NextionSetBrightnessAction(Nextion *component) : component_(component) {} diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 5b2dfc488d..506eb1202b 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -2,13 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import display, esp32, uart import esphome.config_validation as cv -from esphome.const import ( - CONF_BRIGHTNESS, - CONF_ID, - CONF_LAMBDA, - CONF_ON_TOUCH, - CONF_TRIGGER_ID, -) +from esphome.const import CONF_BRIGHTNESS, CONF_ID, CONF_LAMBDA, CONF_ON_TOUCH from esphome.core import CORE, TimePeriod from . import ( # noqa: F401 pylint: disable=unused-import @@ -55,14 +49,6 @@ def AUTO_LOAD() -> list[str]: NextionSetBrightnessAction = nextion_ns.class_( "NextionSetBrightnessAction", automation.Action ) -SetupTrigger = nextion_ns.class_("SetupTrigger", automation.Trigger.template()) -SleepTrigger = nextion_ns.class_("SleepTrigger", automation.Trigger.template()) -WakeTrigger = nextion_ns.class_("WakeTrigger", automation.Trigger.template()) -PageTrigger = nextion_ns.class_("PageTrigger", automation.Trigger.template()) -TouchTrigger = nextion_ns.class_("TouchTrigger", automation.Trigger.template()) -BufferOverflowTrigger = nextion_ns.class_( - "BufferOverflowTrigger", automation.Trigger.template() -) def _validate_tft_upload(config): @@ -101,38 +87,12 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_MAX_COMMANDS_PER_LOOP): cv.uint16_t, cv.Optional(CONF_MAX_QUEUE_SIZE): cv.positive_int, - cv.Optional(CONF_ON_BUFFER_OVERFLOW): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - BufferOverflowTrigger - ), - } - ), - cv.Optional(CONF_ON_PAGE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PageTrigger), - } - ), - cv.Optional(CONF_ON_SETUP): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SetupTrigger), - } - ), - cv.Optional(CONF_ON_SLEEP): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SleepTrigger), - } - ), - cv.Optional(CONF_ON_TOUCH): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TouchTrigger), - } - ), - cv.Optional(CONF_ON_WAKE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WakeTrigger), - } - ), + cv.Optional(CONF_ON_BUFFER_OVERFLOW): automation.validate_automation({}), + cv.Optional(CONF_ON_PAGE): automation.validate_automation({}), + cv.Optional(CONF_ON_SETUP): automation.validate_automation({}), + cv.Optional(CONF_ON_SLEEP): automation.validate_automation({}), + cv.Optional(CONF_ON_TOUCH): automation.validate_automation({}), + cv.Optional(CONF_ON_WAKE): automation.validate_automation({}), cv.Optional(CONF_SKIP_CONNECTION_HANDSHAKE, default=False): cv.boolean, cv.Optional(CONF_STARTUP_OVERRIDE_MS, default="8000ms"): cv.All( cv.positive_time_period_milliseconds, @@ -273,25 +233,25 @@ async def to_code(config): await display.register_display(var, config) for conf in config.get(CONF_ON_SETUP, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_setup_state_callback", [], conf + ) for conf in config.get(CONF_ON_SLEEP, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_sleep_state_callback", [], conf + ) for conf in config.get(CONF_ON_WAKE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_wake_state_callback", [], conf + ) for conf in config.get(CONF_ON_PAGE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.uint8, "x")], conf) - + await automation.build_callback_automation( + var, "add_new_page_callback", [(cg.uint8, "x")], conf + ) for conf in config.get(CONF_ON_TOUCH, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, + await automation.build_callback_automation( + var, + "add_touch_event_callback", [ (cg.uint8, "page_id"), (cg.uint8, "component_id"), @@ -299,7 +259,7 @@ async def to_code(config): ], conf, ) - for conf in config.get(CONF_ON_BUFFER_OVERFLOW, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_buffer_overflow_event_callback", [], conf + ) From 2f3c21c7c16b37d2ad1f57e4d90883129a50c86c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:23:50 -1000 Subject: [PATCH 091/115] [ezo] Migrate triggers to callback automation (#15228) --- esphome/components/ezo/automation.h | 53 ---------------- esphome/components/ezo/sensor.py | 94 ++++++++--------------------- 2 files changed, 25 insertions(+), 122 deletions(-) delete mode 100644 esphome/components/ezo/automation.h diff --git a/esphome/components/ezo/automation.h b/esphome/components/ezo/automation.h deleted file mode 100644 index a4a6fa3014..0000000000 --- a/esphome/components/ezo/automation.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include - -#include "esphome/core/automation.h" -#include "ezo.h" - -namespace esphome { -namespace ezo { - -class LedTrigger : public Trigger { - public: - explicit LedTrigger(EZOSensor *ezo) { - ezo->add_led_state_callback([this](bool value) { this->trigger(value); }); - } -}; - -class CustomTrigger : public Trigger { - public: - explicit CustomTrigger(EZOSensor *ezo) { - ezo->add_custom_callback([this](const std::string &value) { this->trigger(value); }); - } -}; - -class TTrigger : public Trigger { - public: - explicit TTrigger(EZOSensor *ezo) { - ezo->add_t_callback([this](const std::string &value) { this->trigger(value); }); - } -}; - -class CalibrationTrigger : public Trigger { - public: - explicit CalibrationTrigger(EZOSensor *ezo) { - ezo->add_calibration_callback([this](const std::string &value) { this->trigger(value); }); - } -}; - -class SlopeTrigger : public Trigger { - public: - explicit SlopeTrigger(EZOSensor *ezo) { - ezo->add_slope_callback([this](const std::string &value) { this->trigger(value); }); - } -}; - -class DeviceInformationTrigger : public Trigger { - public: - explicit DeviceInformationTrigger(EZOSensor *ezo) { - ezo->add_device_infomation_callback([this](const std::string &value) { this->trigger(value); }); - } -}; - -} // namespace ezo -} // namespace esphome diff --git a/esphome/components/ezo/sensor.py b/esphome/components/ezo/sensor.py index cf240faec3..7c81f9c848 100644 --- a/esphome/components/ezo/sensor.py +++ b/esphome/components/ezo/sensor.py @@ -2,7 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import i2c, sensor import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_TRIGGER_ID +from esphome.const import CONF_ID CODEOWNERS = ["@ssieb"] @@ -21,61 +21,16 @@ EZOSensor = ezo_ns.class_( "EZOSensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice ) -CustomTrigger = ezo_ns.class_( - "CustomTrigger", automation.Trigger.template(cg.std_string) -) - - -TTrigger = ezo_ns.class_("TTrigger", automation.Trigger.template(cg.std_string)) - -SlopeTrigger = ezo_ns.class_("SlopeTrigger", automation.Trigger.template(cg.std_string)) - -CalibrationTrigger = ezo_ns.class_( - "CalibrationTrigger", automation.Trigger.template(cg.std_string) -) - -DeviceInformationTrigger = ezo_ns.class_( - "DeviceInformationTrigger", automation.Trigger.template(cg.std_string) -) - -LedTrigger = ezo_ns.class_("LedTrigger", automation.Trigger.template(cg.bool_)) - CONFIG_SCHEMA = ( sensor.sensor_schema(EZOSensor) .extend( { - cv.Optional(CONF_ON_CUSTOM): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CustomTrigger), - } - ), - cv.Optional(CONF_ON_CALIBRATION): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CalibrationTrigger), - } - ), - cv.Optional(CONF_ON_SLOPE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SlopeTrigger), - } - ), - cv.Optional(CONF_ON_T): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TTrigger), - } - ), - cv.Optional(CONF_ON_DEVICE_INFORMATION): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - DeviceInformationTrigger - ), - } - ), - cv.Optional(CONF_ON_LED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LedTrigger), - } - ), + cv.Optional(CONF_ON_CUSTOM): automation.validate_automation({}), + cv.Optional(CONF_ON_CALIBRATION): automation.validate_automation({}), + cv.Optional(CONF_ON_SLOPE): automation.validate_automation({}), + cv.Optional(CONF_ON_T): automation.validate_automation({}), + cv.Optional(CONF_ON_DEVICE_INFORMATION): automation.validate_automation({}), + cv.Optional(CONF_ON_LED): automation.validate_automation({}), } ) .extend(cv.polling_component_schema("60s")) @@ -90,25 +45,26 @@ async def to_code(config): await i2c.register_i2c_device(var, config) for conf in config.get(CONF_ON_CUSTOM, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) - + await automation.build_callback_automation( + var, "add_custom_callback", [(cg.std_string, "x")], conf + ) for conf in config.get(CONF_ON_LED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(bool, "x")], conf) - + await automation.build_callback_automation( + var, "add_led_state_callback", [(bool, "x")], conf + ) for conf in config.get(CONF_ON_DEVICE_INFORMATION, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) - + await automation.build_callback_automation( + var, "add_device_infomation_callback", [(cg.std_string, "x")], conf + ) for conf in config.get(CONF_ON_SLOPE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) - + await automation.build_callback_automation( + var, "add_slope_callback", [(cg.std_string, "x")], conf + ) for conf in config.get(CONF_ON_CALIBRATION, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) - + await automation.build_callback_automation( + var, "add_calibration_callback", [(cg.std_string, "x")], conf + ) for conf in config.get(CONF_ON_T, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + await automation.build_callback_automation( + var, "add_t_callback", [(cg.std_string, "x")], conf + ) From 39509265bc76a0eadce17c9e28a4b032fc3597fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:24:03 -1000 Subject: [PATCH 092/115] [haier] Migrate triggers to callback automation (#15229) --- esphome/components/haier/climate.py | 62 ++++++++------------------ esphome/components/haier/haier_base.h | 7 --- esphome/components/haier/hon_climate.h | 16 ------- 3 files changed, 18 insertions(+), 67 deletions(-) diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index caaaa18dd6..9c2c999f25 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -22,7 +22,6 @@ from esphome.const import ( CONF_SUPPORTED_SWING_MODES, CONF_TARGET_TEMPERATURE, CONF_TEMPERATURE_STEP, - CONF_TRIGGER_ID, CONF_VISUAL, CONF_WIFI, ) @@ -122,21 +121,6 @@ SUPPORTED_HON_CONTROL_METHODS = { "SET_SINGLE_PARAMETER": HonControlMethod.SET_SINGLE_PARAMETER, } -HaierAlarmStartTrigger = haier_ns.class_( - "HaierAlarmStartTrigger", - automation.Trigger.template(cg.uint8, cg.const_char_ptr), -) - -HaierAlarmEndTrigger = haier_ns.class_( - "HaierAlarmEndTrigger", - automation.Trigger.template(cg.uint8, cg.const_char_ptr), -) - -StatusMessageTrigger = haier_ns.class_( - "StatusMessageTrigger", - automation.Trigger.template(cg.const_char_ptr, cg.size_t), -) - def validate_visual(config): if CONF_VISUAL in config: @@ -203,13 +187,7 @@ def _base_config_schema(class_: MockObjClass) -> cv.Schema: cv.Optional( CONF_ANSWER_TIMEOUT, ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_ON_STATUS_MESSAGE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - StatusMessageTrigger - ), - } - ), + cv.Optional(CONF_ON_STATUS_MESSAGE): automation.validate_automation({}), } ) .extend(uart.UART_DEVICE_SCHEMA) @@ -264,19 +242,9 @@ CONFIG_SCHEMA = cv.All( f"The {CONF_OUTDOOR_TEMPERATURE} option is deprecated, use a sensor for a haier platform instead" ), cv.Optional(CONF_ON_ALARM_START): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - HaierAlarmStartTrigger - ), - } - ), - cv.Optional(CONF_ON_ALARM_END): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - HaierAlarmEndTrigger - ), - } + {} ), + cv.Optional(CONF_ON_ALARM_END): automation.validate_automation({}), } ), }, @@ -530,19 +498,25 @@ async def to_code(config): var.set_status_message_header_size(config[CONF_STATUS_MESSAGE_HEADER_SIZE]) ) for conf in config.get(CONF_ON_ALARM_START, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf + await automation.build_callback_automation( + var, + "add_alarm_start_callback", + [(cg.uint8, "code"), (cg.const_char_ptr, "message")], + conf, ) for conf in config.get(CONF_ON_ALARM_END, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf + await automation.build_callback_automation( + var, + "add_alarm_end_callback", + [(cg.uint8, "code"), (cg.const_char_ptr, "message")], + conf, ) for conf in config.get(CONF_ON_STATUS_MESSAGE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.const_char_ptr, "data"), (cg.size_t, "data_size")], conf + await automation.build_callback_automation( + var, + "add_status_message_callback", + [(cg.const_char_ptr, "data"), (cg.size_t, "data_size")], + conf, ) # https://github.com/paveldn/HaierProtocol cg.add_library("pavlodn/HaierProtocol", "0.9.31") diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index 87aa1d65ef..0c416623c0 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -177,12 +177,5 @@ class HaierClimateBase : public esphome::Component, ESPPreferenceObject base_rtc_; }; -class StatusMessageTrigger : public Trigger { - public: - explicit StatusMessageTrigger(HaierClimateBase *parent) { - parent->add_status_message_callback([this](const char *data, size_t data_size) { this->trigger(data, data_size); }); - } -}; - } // namespace haier } // namespace esphome diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index 7c48a3748b..7a87f27b66 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -200,21 +200,5 @@ class HonClimate : public HaierClimateBase { SwitchState quiet_mode_state_{SwitchState::OFF}; }; -class HaierAlarmStartTrigger : public Trigger { - public: - explicit HaierAlarmStartTrigger(HonClimate *parent) { - parent->add_alarm_start_callback( - [this](uint8_t alarm_code, const char *alarm_message) { this->trigger(alarm_code, alarm_message); }); - } -}; - -class HaierAlarmEndTrigger : public Trigger { - public: - explicit HaierAlarmEndTrigger(HonClimate *parent) { - parent->add_alarm_end_callback( - [this](uint8_t alarm_code, const char *alarm_message) { this->trigger(alarm_code, alarm_message); }); - } -}; - } // namespace haier } // namespace esphome From f9d41bd36adf4923f0509db82da47c321ffafcac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:24:15 -1000 Subject: [PATCH 093/115] [modbus_controller] Migrate triggers to callback automation (#15230) --- .../components/modbus_controller/__init__.py | 64 ++++++------------- .../components/modbus_controller/automation.h | 35 ---------- 2 files changed, 19 insertions(+), 80 deletions(-) delete mode 100644 esphome/components/modbus_controller/automation.h diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index aea79b2053..dfc43bf23b 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -5,14 +5,7 @@ import esphome.codegen as cg from esphome.components import modbus from esphome.components.const import CONF_ENABLED import esphome.config_validation as cv -from esphome.const import ( - CONF_ADDRESS, - CONF_ID, - CONF_LAMBDA, - CONF_NAME, - CONF_OFFSET, - CONF_TRIGGER_ID, -) +from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_NAME, CONF_OFFSET from esphome.cpp_helpers import logging from .const import ( @@ -135,17 +128,6 @@ CPP_TYPE_REGISTER_MAP = { "FP32_R": cg.float_, } -ModbusCommandSentTrigger = modbus_controller_ns.class_( - "ModbusCommandSentTrigger", automation.Trigger.template(cg.int_, cg.int_) -) - -ModbusOnlineTrigger = modbus_controller_ns.class_( - "ModbusOnlineTrigger", automation.Trigger.template(cg.int_, cg.int_) -) - -ModbusOfflineTrigger = modbus_controller_ns.class_( - "ModbusOfflineTrigger", automation.Trigger.template(cg.int_, cg.int_) -) _LOGGER = logging.getLogger(__name__) @@ -182,23 +164,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_SERVER_REGISTERS, ): cv.ensure_list(ModbusServerRegisterSchema), - cv.Optional(CONF_ON_COMMAND_SENT): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - ModbusCommandSentTrigger - ), - } - ), - cv.Optional(CONF_ON_ONLINE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ModbusOnlineTrigger), - } - ), - cv.Optional(CONF_ON_OFFLINE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ModbusOfflineTrigger), - } - ), + cv.Optional(CONF_ON_COMMAND_SENT): automation.validate_automation({}), + cv.Optional(CONF_ON_ONLINE): automation.validate_automation({}), + cv.Optional(CONF_ON_OFFLINE): automation.validate_automation({}), } ) .extend(cv.polling_component_schema("60s")) @@ -363,19 +331,25 @@ async def to_code(config): cg.add(var.add_server_register(server_register_var)) await register_modbus_device(var, config) for conf in config.get(CONF_ON_COMMAND_SENT, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf + await automation.build_callback_automation( + var, + "add_on_command_sent_callback", + [(cg.int_, "function_code"), (cg.int_, "address")], + conf, ) for conf in config.get(CONF_ON_ONLINE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf + await automation.build_callback_automation( + var, + "add_on_online_callback", + [(cg.int_, "function_code"), (cg.int_, "address")], + conf, ) for conf in config.get(CONF_ON_OFFLINE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf + await automation.build_callback_automation( + var, + "add_on_offline_callback", + [(cg.int_, "function_code"), (cg.int_, "address")], + conf, ) diff --git a/esphome/components/modbus_controller/automation.h b/esphome/components/modbus_controller/automation.h deleted file mode 100644 index b3338192cc..0000000000 --- a/esphome/components/modbus_controller/automation.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/core/automation.h" -#include "esphome/components/modbus_controller/modbus_controller.h" - -namespace esphome { -namespace modbus_controller { - -class ModbusCommandSentTrigger : public Trigger { - public: - ModbusCommandSentTrigger(ModbusController *a_modbuscontroller) { - a_modbuscontroller->add_on_command_sent_callback( - [this](int function_code, int address) { this->trigger(function_code, address); }); - } -}; - -class ModbusOnlineTrigger : public Trigger { - public: - ModbusOnlineTrigger(ModbusController *a_modbuscontroller) { - a_modbuscontroller->add_on_online_callback( - [this](int function_code, int address) { this->trigger(function_code, address); }); - } -}; - -class ModbusOfflineTrigger : public Trigger { - public: - ModbusOfflineTrigger(ModbusController *a_modbuscontroller) { - a_modbuscontroller->add_on_offline_callback( - [this](int function_code, int address) { this->trigger(function_code, address); }); - } -}; - -} // namespace modbus_controller -} // namespace esphome From 0d67f91facbe654bbb634d95aa16c791ff52a3f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:24:25 -1000 Subject: [PATCH 094/115] [rf_bridge] Migrate triggers to callback automation (#15231) --- esphome/components/rf_bridge/__init__.py | 37 +++++++----------------- esphome/components/rf_bridge/rf_bridge.h | 14 --------- 2 files changed, 10 insertions(+), 41 deletions(-) diff --git a/esphome/components/rf_bridge/__init__.py b/esphome/components/rf_bridge/__init__.py index 934f24b789..c6eb1749c3 100644 --- a/esphome/components/rf_bridge/__init__.py +++ b/esphome/components/rf_bridge/__init__.py @@ -12,7 +12,6 @@ from esphome.const import ( CONF_PROTOCOL, CONF_RAW, CONF_SYNC, - CONF_TRIGGER_ID, ) DEPENDENCIES = ["uart"] @@ -26,14 +25,6 @@ RFBridgeComponent = rf_bridge_ns.class_( RFBridgeData = rf_bridge_ns.struct("RFBridgeData") RFBridgeAdvancedData = rf_bridge_ns.struct("RFBridgeAdvancedData") -RFBridgeReceivedCodeTrigger = rf_bridge_ns.class_( - "RFBridgeReceivedCodeTrigger", automation.Trigger.template(RFBridgeData) -) -RFBridgeReceivedAdvancedCodeTrigger = rf_bridge_ns.class_( - "RFBridgeReceivedAdvancedCodeTrigger", - automation.Trigger.template(RFBridgeAdvancedData), -) - RFBridgeSendCodeAction = rf_bridge_ns.class_( "RFBridgeSendCodeAction", automation.Action ) @@ -65,19 +56,9 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(RFBridgeComponent), - cv.Optional(CONF_ON_CODE_RECEIVED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - RFBridgeReceivedCodeTrigger - ), - } - ), + cv.Optional(CONF_ON_CODE_RECEIVED): automation.validate_automation({}), cv.Optional(CONF_ON_ADVANCED_CODE_RECEIVED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - RFBridgeReceivedAdvancedCodeTrigger - ), - } + {} ), } ) @@ -92,13 +73,15 @@ async def to_code(config): await uart.register_uart_device(var, config) for conf in config.get(CONF_ON_CODE_RECEIVED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(RFBridgeData, "data")], conf) - + await automation.build_callback_automation( + var, "add_on_code_received_callback", [(RFBridgeData, "data")], conf + ) for conf in config.get(CONF_ON_ADVANCED_CODE_RECEIVED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(RFBridgeAdvancedData, "data")], conf + await automation.build_callback_automation( + var, + "add_on_advanced_code_received_callback", + [(RFBridgeAdvancedData, "data")], + conf, ) diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index e5780c9ebe..571ac6c385 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -77,20 +77,6 @@ class RFBridgeComponent : public uart::UARTDevice, public Component { CallbackManager advanced_data_callback_; }; -class RFBridgeReceivedCodeTrigger : public Trigger { - public: - explicit RFBridgeReceivedCodeTrigger(RFBridgeComponent *parent) { - parent->add_on_code_received_callback([this](RFBridgeData data) { this->trigger(data); }); - } -}; - -class RFBridgeReceivedAdvancedCodeTrigger : public Trigger { - public: - explicit RFBridgeReceivedAdvancedCodeTrigger(RFBridgeComponent *parent) { - parent->add_on_advanced_code_received_callback([this](const RFBridgeAdvancedData &data) { this->trigger(data); }); - } -}; - template class RFBridgeSendCodeAction : public Action { public: RFBridgeSendCodeAction(RFBridgeComponent *parent) : parent_(parent) {} From 5a8d6931a8d8a0f1fe05727e2e5d00098aa2dbb4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 08:24:35 -1000 Subject: [PATCH 095/115] [factory_reset] Migrate FastBootTrigger to callback automation (#15232) --- esphome/components/factory_reset/__init__.py | 21 ++++++------------- .../components/factory_reset/factory_reset.h | 6 ------ 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/esphome/components/factory_reset/__init__.py b/esphome/components/factory_reset/__init__.py index 5784d09ce6..20b191a2b7 100644 --- a/esphome/components/factory_reset/__init__.py +++ b/esphome/components/factory_reset/__init__.py @@ -1,10 +1,9 @@ -from esphome.automation import Trigger, build_automation, validate_automation +from esphome import automation import esphome.codegen as cg from esphome.components.esp8266 import CONF_RESTORE_FROM_FLASH, KEY_ESP8266 import esphome.config_validation as cv from esphome.const import ( CONF_ID, - CONF_TRIGGER_ID, PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, @@ -18,7 +17,6 @@ CODEOWNERS = ["@anatoly-savchenkov"] factory_reset_ns = cg.esphome_ns.namespace("factory_reset") FactoryResetComponent = factory_reset_ns.class_("FactoryResetComponent", cg.Component) -FastBootTrigger = factory_reset_ns.class_("FastBootTrigger", Trigger, cg.Component) CONF_MAX_DELAY = "max_delay" CONF_RESETS_REQUIRED = "resets_required" @@ -55,11 +53,7 @@ CONFIG_SCHEMA = cv.All( ), ), cv.Optional(CONF_RESETS_REQUIRED): cv.positive_not_null_int, - cv.Optional(CONF_ON_INCREMENT): validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FastBootTrigger), - } - ), + cv.Optional(CONF_ON_INCREMENT): automation.validate_automation({}), } ).extend(cv.COMPONENT_SCHEMA), _validate, @@ -88,12 +82,9 @@ async def to_code(config): ) await cg.register_component(var, config) for conf in config.get(CONF_ON_INCREMENT, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await build_automation( - trigger, - [ - (cg.uint8, "x"), - (cg.uint8, "target"), - ], + await automation.build_callback_automation( + var, + "add_increment_callback", + [(cg.uint8, "x"), (cg.uint8, "target")], conf, ) diff --git a/esphome/components/factory_reset/factory_reset.h b/esphome/components/factory_reset/factory_reset.h index 34f89d73b6..41ee627c4b 100644 --- a/esphome/components/factory_reset/factory_reset.h +++ b/esphome/components/factory_reset/factory_reset.h @@ -30,12 +30,6 @@ class FactoryResetComponent : public Component { uint8_t required_count_; // The number of boot attempts before fast boot is enabled }; -class FastBootTrigger : public Trigger { - public: - explicit FastBootTrigger(FactoryResetComponent *parent) { - parent->add_increment_callback([this](uint8_t current, uint8_t target) { this->trigger(current, target); }); - } -}; } // namespace esphome::factory_reset #endif // !defined(USE_RP2040) && !defined(USE_HOST) From 810c046cc68dba48b3748f4a60de2049146beee7 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:25:38 -0400 Subject: [PATCH 096/115] [multiple] Fix misc hardware register bugs (#15208) --- esphome/components/mcp23008/mcp23008.cpp | 4 +++- esphome/components/mcp23017/mcp23017.cpp | 6 ++++-- esphome/components/mcp23s08/mcp23s08.cpp | 15 ++++++++++----- esphome/components/mcp23s17/mcp23s17.cpp | 22 +++++++++++++--------- esphome/components/mmc5603/mmc5603.cpp | 6 +++--- esphome/components/sx1509/sx1509.cpp | 4 ++-- 6 files changed, 35 insertions(+), 22 deletions(-) diff --git a/esphome/components/mcp23008/mcp23008.cpp b/esphome/components/mcp23008/mcp23008.cpp index 0c34e4971a..64b120daa4 100644 --- a/esphome/components/mcp23008/mcp23008.cpp +++ b/esphome/components/mcp23008/mcp23008.cpp @@ -6,6 +6,8 @@ namespace mcp23008 { static const char *const TAG = "mcp23008"; +static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin + void MCP23008::setup() { uint8_t iocon; if (!this->read_reg(mcp23x08_base::MCP23X08_IOCON, &iocon)) { @@ -18,7 +20,7 @@ void MCP23008::setup() { if (this->open_drain_ints_) { // enable open-drain interrupt pins, 3.3V-safe - this->write_reg(mcp23x08_base::MCP23X08_IOCON, 0x04); + this->write_reg(mcp23x08_base::MCP23X08_IOCON, iocon | IOCON_ODR); } } diff --git a/esphome/components/mcp23017/mcp23017.cpp b/esphome/components/mcp23017/mcp23017.cpp index 1ad2036939..e14e317d44 100644 --- a/esphome/components/mcp23017/mcp23017.cpp +++ b/esphome/components/mcp23017/mcp23017.cpp @@ -6,6 +6,8 @@ namespace mcp23017 { static const char *const TAG = "mcp23017"; +static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin + void MCP23017::setup() { uint8_t iocon; if (!this->read_reg(mcp23x17_base::MCP23X17_IOCONA, &iocon)) { @@ -19,8 +21,8 @@ void MCP23017::setup() { if (this->open_drain_ints_) { // enable open-drain interrupt pins, 3.3V-safe - this->write_reg(mcp23x17_base::MCP23X17_IOCONA, 0x04); - this->write_reg(mcp23x17_base::MCP23X17_IOCONB, 0x04); + this->write_reg(mcp23x17_base::MCP23X17_IOCONA, iocon | IOCON_ODR); + this->write_reg(mcp23x17_base::MCP23X17_IOCONB, iocon | IOCON_ODR); } } diff --git a/esphome/components/mcp23s08/mcp23s08.cpp b/esphome/components/mcp23s08/mcp23s08.cpp index 3d944b45d5..1c17b66637 100644 --- a/esphome/components/mcp23s08/mcp23s08.cpp +++ b/esphome/components/mcp23s08/mcp23s08.cpp @@ -6,6 +6,11 @@ namespace mcp23s08 { static const char *const TAG = "mcp23s08"; +// IOCON register bits +static constexpr uint8_t IOCON_SEQOP = 0x20; // Sequential operation mode +static constexpr uint8_t IOCON_HAEN = 0x08; // Hardware address enable +static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin + void MCP23S08::set_device_address(uint8_t device_addr) { if (device_addr != 0) { this->device_opcode_ |= ((device_addr & 0x03) << 1); @@ -15,19 +20,19 @@ void MCP23S08::set_device_address(uint8_t device_addr) { void MCP23S08::setup() { this->spi_setup(); + // Enable HAEN (broadcast to all chips since HAEN isn't active yet) this->enable(); - uint8_t cmd = 0b01000000; - this->transfer_byte(cmd); + this->transfer_byte(0b01000000); this->transfer_byte(mcp23x08_base::MCP23X08_IOCON); - this->transfer_byte(0b00011000); // Enable HAEN pins for addressing + this->transfer_byte(IOCON_SEQOP | IOCON_HAEN); this->disable(); // Read current output register state this->read_reg(mcp23x08_base::MCP23X08_OLAT, &this->olat_); if (this->open_drain_ints_) { - // enable open-drain interrupt pins, 3.3V-safe - this->write_reg(mcp23x08_base::MCP23X08_IOCON, 0x04); + // enable open-drain interrupt pins, 3.3V-safe (addressed, only this chip) + this->write_reg(mcp23x08_base::MCP23X08_IOCON, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR); } } diff --git a/esphome/components/mcp23s17/mcp23s17.cpp b/esphome/components/mcp23s17/mcp23s17.cpp index 1624eda9e4..c6abd7ad59 100644 --- a/esphome/components/mcp23s17/mcp23s17.cpp +++ b/esphome/components/mcp23s17/mcp23s17.cpp @@ -6,6 +6,11 @@ namespace mcp23s17 { static const char *const TAG = "mcp23s17"; +// IOCON register bits +static constexpr uint8_t IOCON_SEQOP = 0x20; // Sequential operation mode +static constexpr uint8_t IOCON_HAEN = 0x08; // Hardware address enable +static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin + void MCP23S17::set_device_address(uint8_t device_addr) { if (device_addr != 0) { this->device_opcode_ |= ((device_addr & 0b111) << 1); @@ -15,18 +20,17 @@ void MCP23S17::set_device_address(uint8_t device_addr) { void MCP23S17::setup() { this->spi_setup(); + // Enable HAEN (broadcast to addresses 0 and 4 since HAEN isn't active yet) this->enable(); - uint8_t cmd = 0b01000000; - this->transfer_byte(cmd); + this->transfer_byte(0b01000000); this->transfer_byte(mcp23x17_base::MCP23X17_IOCONA); - this->transfer_byte(0b00011000); // Enable HAEN pins for addressing + this->transfer_byte(IOCON_SEQOP | IOCON_HAEN); this->disable(); this->enable(); - cmd = 0b01001000; - this->transfer_byte(cmd); + this->transfer_byte(0b01001000); this->transfer_byte(mcp23x17_base::MCP23X17_IOCONA); - this->transfer_byte(0b00011000); // Enable HAEN pins for addressing + this->transfer_byte(IOCON_SEQOP | IOCON_HAEN); this->disable(); // Read current output register state @@ -34,9 +38,9 @@ void MCP23S17::setup() { this->read_reg(mcp23x17_base::MCP23X17_OLATB, &this->olat_b_); if (this->open_drain_ints_) { - // enable open-drain interrupt pins, 3.3V-safe - this->write_reg(mcp23x17_base::MCP23X17_IOCONA, 0x04); - this->write_reg(mcp23x17_base::MCP23X17_IOCONB, 0x04); + // enable open-drain interrupt pins, 3.3V-safe (addressed, only this chip) + this->write_reg(mcp23x17_base::MCP23X17_IOCONA, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR); + this->write_reg(mcp23x17_base::MCP23X17_IOCONB, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR); } } diff --git a/esphome/components/mmc5603/mmc5603.cpp b/esphome/components/mmc5603/mmc5603.cpp index 1cbc84191f..51b94eb767 100644 --- a/esphome/components/mmc5603/mmc5603.cpp +++ b/esphome/components/mmc5603/mmc5603.cpp @@ -126,21 +126,21 @@ void MMC5603Component::update() { int32_t raw_x = 0; raw_x |= buffer[0] << 12; raw_x |= buffer[1] << 4; - raw_x |= buffer[2] << 0; + raw_x |= buffer[2] & 0x0F; const float x = 0.00625 * (raw_x - 524288); int32_t raw_y = 0; raw_y |= buffer[3] << 12; raw_y |= buffer[4] << 4; - raw_y |= buffer[5] << 0; + raw_y |= buffer[5] & 0x0F; const float y = 0.00625 * (raw_y - 524288); int32_t raw_z = 0; raw_z |= buffer[6] << 12; raw_z |= buffer[7] << 4; - raw_z |= buffer[8] << 0; + raw_z |= buffer[8] & 0x0F; const float z = 0.00625 * (raw_z - 524288); diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp index dfe1277297..1cdae76eaf 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -309,8 +309,8 @@ void SX1509Component::set_debounce_keypad_(uint8_t time, uint8_t num_rows, uint8 set_debounce_time_(time); for (uint16_t i = 0; i < num_rows; i++) set_debounce_pin_(i); - for (uint16_t i = 0; i < (8 + num_cols); i++) - set_debounce_pin_(i); + for (uint16_t i = 0; i < num_cols; i++) + set_debounce_pin_(i + 8); } } // namespace sx1509 From 0a607b9c93c0a1c8504a73de09b564c723a258b2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:36:16 -0400 Subject: [PATCH 097/115] [esp32_ble_server] Fix wrong union member in STOP_EVT handler (#15239) --- esphome/components/esp32_ble_server/ble_service.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble_server/ble_service.cpp b/esphome/components/esp32_ble_server/ble_service.cpp index 96fedf2346..8956c87b3e 100644 --- a/esphome/components/esp32_ble_server/ble_service.cpp +++ b/esphome/components/esp32_ble_server/ble_service.cpp @@ -159,7 +159,7 @@ void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t g break; } case ESP_GATTS_STOP_EVT: { - if (param->start.service_handle == this->handle_) { + if (param->stop.service_handle == this->handle_) { this->state_ = STOPPED; } break; From 4b9467cd0cd03bf33aa24ec8be48bc9395976a6a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:37:33 -0400 Subject: [PATCH 098/115] [esp32_ble_client] Fix wrong union member in OPEN_EVT handler (#15236) --- esphome/components/esp32_ble_client/ble_client_base.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 9d6e079d92..7f0f2c624d 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -350,7 +350,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ // For V3_WITHOUT_CACHE, we already set fast params before connecting // No need to update them again here this->log_event_("Searching for services"); - esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr); + esp_ble_gattc_search_service(esp_gattc_if, param->open.conn_id, nullptr); break; } case ESP_GATTC_CONNECT_EVT: { From 53bd57f3c2557d75fade7864b7371a2de27cabc5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:37:54 -0400 Subject: [PATCH 099/115] [pid] Fix inverted debug log conditions and broken smoothing formula (#15240) --- esphome/components/pid/pid_autotuner.cpp | 4 ++-- esphome/components/pid/pid_simulator.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/pid/pid_autotuner.cpp b/esphome/components/pid/pid_autotuner.cpp index e1ddd1d7c6..3b971e6559 100644 --- a/esphome/components/pid/pid_autotuner.cpp +++ b/esphome/components/pid/pid_autotuner.cpp @@ -101,10 +101,10 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce if (!zc_symmetrical || !amplitude_convergent) { // The frequency/amplitude is not fully accurate yet, try to wait // until the fault clears, or terminate after a while anyway - if (zc_symmetrical) { + if (!zc_symmetrical) { ESP_LOGVV(TAG, "%s: ZC is not symmetrical", this->id_.c_str()); } - if (amplitude_convergent) { + if (!amplitude_convergent) { ESP_LOGVV(TAG, "%s: Amplitude is not convergent", this->id_.c_str()); } uint32_t phase = this->relay_function_.phase_count; diff --git a/esphome/components/pid/pid_simulator.h b/esphome/components/pid/pid_simulator.h index 30222f2f7a..629784cea5 100644 --- a/esphome/components/pid/pid_simulator.h +++ b/esphome/components/pid/pid_simulator.h @@ -59,7 +59,7 @@ class PIDSimulator : public PollingComponent, public output::FloatOutput { delayed_temps.erase(delayed_temps.begin()); float prev_temp = this->delayed_temps[0]; float alpha = 0.1f; - float ret = (1 - alpha) * prev_temp + alpha * prev_temp; + float ret = (1 - alpha) * prev_temp + alpha * temperature; return ret; } From 951ad91cb259262675dbfbf7f71d1d6fb27d596a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:39:30 -0400 Subject: [PATCH 100/115] [atm90e32] Fix phase angle precision loss and remove unused member (#15238) --- esphome/components/atm90e32/atm90e32.cpp | 4 ++-- esphome/components/atm90e32/atm90e32.h | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index ee7fe5ce75..db29702c54 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -550,8 +550,8 @@ float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) { } float ATM90E32Component::get_phase_angle_(uint8_t phase) { - uint16_t val = this->read16_(ATM90E32_REGISTER_PANGLE + phase) / 10.0; - return (val > 180) ? (float) (val - 360.0f) : (float) val; + float val = this->read16_(ATM90E32_REGISTER_PANGLE + phase) / 10.0f; + return (val > 180.0f) ? val - 360.0f : val; } float ATM90E32Component::get_phase_peak_current_(uint8_t phase) { diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index 2524616470..c44a11e3ed 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -134,7 +134,6 @@ class ATM90E32Component : public PollingComponent, void set_freq_status_text_sensor(text_sensor::TextSensor *sensor) { this->freq_status_text_sensor_ = sensor; } #endif uint16_t calculate_voltage_threshold(int line_freq, uint16_t ugain, float multiplier); - int32_t last_periodic_millis = millis(); protected: #ifdef USE_NUMBER From 05c15f4241d20c78d3f8eb40a19d3046723aeaf7 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:44:40 -0400 Subject: [PATCH 101/115] [remote_base] Fix gobox uint64_t format specifier (#15237) --- esphome/components/remote_base/gobox_protocol.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/remote_base/gobox_protocol.cpp b/esphome/components/remote_base/gobox_protocol.cpp index 4f6de5e59e..0e1617659d 100644 --- a/esphome/components/remote_base/gobox_protocol.cpp +++ b/esphome/components/remote_base/gobox_protocol.cpp @@ -1,5 +1,6 @@ #include "gobox_protocol.h" #include "esphome/core/log.h" +#include namespace esphome { namespace remote_base { @@ -25,7 +26,7 @@ void GoboxProtocol::encode(RemoteTransmitData *dst, const GoboxData &data) { dst->set_carrier_frequency(38000); dst->reserve((HEADER_SIZE + CODE_SIZE + 1) * 2); uint64_t code = (HEADER << CODE_SIZE) | (data.code & ((1UL << CODE_SIZE) - 1)); - ESP_LOGI(TAG, "Send Gobox: code=0x%Lx", code); + ESP_LOGI(TAG, "Send Gobox: code=0x%016" PRIx64, code); for (int16_t i = (HEADER_SIZE + CODE_SIZE - 1); i >= 0; i--) { if (code & ((uint64_t) 1 << i)) { dst->item(BIT_MARK_US, BIT_ONE_SPACE_US); From f0db0c105424e31022e9521b44eb577cfa18d2d5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:48:08 -0400 Subject: [PATCH 102/115] [esp32] Add ESP-IDF 5.5.4 and 6.0.0 version mappings (#15241) --- esphome/components/esp32/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 91eb913e3d..0ce1117262 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -690,9 +690,15 @@ ARDUINO_IDF_VERSION_LOOKUP = { ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { "recommended": cv.Version(5, 5, 3, "1"), "latest": cv.Version(5, 5, 3, "1"), - "dev": cv.Version(5, 5, 3, "1"), + "dev": cv.Version(5, 5, 4), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { + cv.Version( + 6, 0, 0 + ): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6", + cv.Version( + 5, 5, 4 + ): "https://github.com/pioarduino/platform-espressif32.git#develop", cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37), cv.Version(5, 5, 3): cv.Version(55, 3, 37), cv.Version(5, 5, 2): cv.Version(55, 3, 37), From 7532e1f957499ca880c71c0e8c1f693c585b4cf3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:58:41 -0400 Subject: [PATCH 103/115] [multiple] Fix uninitialized members and error constant types (#15235) --- esphome/components/max44009/max44009.cpp | 18 +++++++++--------- esphome/components/max44009/max44009.h | 4 ++-- .../modbus_controller/output/modbus_output.h | 4 ++-- esphome/components/tuya/climate/tuya_climate.h | 10 +++++----- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/esphome/components/max44009/max44009.cpp b/esphome/components/max44009/max44009.cpp index 8b8e38c1ea..cbce053519 100644 --- a/esphome/components/max44009/max44009.cpp +++ b/esphome/components/max44009/max44009.cpp @@ -8,17 +8,17 @@ namespace max44009 { static const char *const TAG = "max44009.sensor"; // REGISTERS -static const uint8_t MAX44009_REGISTER_CONFIGURATION = 0x02; -static const uint8_t MAX44009_LUX_READING_HIGH = 0x03; -static const uint8_t MAX44009_LUX_READING_LOW = 0x04; +static constexpr uint8_t MAX44009_REGISTER_CONFIGURATION = 0x02; +static constexpr uint8_t MAX44009_LUX_READING_HIGH = 0x03; +static constexpr uint8_t MAX44009_LUX_READING_LOW = 0x04; // CONFIGURATION MASKS -static const uint8_t MAX44009_CFG_CONTINUOUS = 0x80; +static constexpr uint8_t MAX44009_CFG_CONTINUOUS = 0x80; // ERROR CODES -static const uint8_t MAX44009_OK = 0; -static const uint8_t MAX44009_ERROR_WIRE_REQUEST = -10; -static const uint8_t MAX44009_ERROR_OVERFLOW = -20; -static const uint8_t MAX44009_ERROR_HIGH_BYTE = -30; -static const uint8_t MAX44009_ERROR_LOW_BYTE = -31; +static constexpr int8_t MAX44009_OK = 0; +static constexpr int8_t MAX44009_ERROR_WIRE_REQUEST = -10; +static constexpr int8_t MAX44009_ERROR_OVERFLOW = -20; +static constexpr int8_t MAX44009_ERROR_HIGH_BYTE = -30; +static constexpr int8_t MAX44009_ERROR_LOW_BYTE = -31; void MAX44009Sensor::setup() { bool state_ok = false; diff --git a/esphome/components/max44009/max44009.h b/esphome/components/max44009/max44009.h index 59eea66ed9..d0ffd7bc70 100644 --- a/esphome/components/max44009/max44009.h +++ b/esphome/components/max44009/max44009.h @@ -28,8 +28,8 @@ class MAX44009Sensor : public sensor::Sensor, public PollingComponent, public i2 uint8_t read_(uint8_t reg); void write_(uint8_t reg, uint8_t value); - int error_; - MAX44009Mode mode_; + int8_t error_{0}; + MAX44009Mode mode_{MAX44009_MODE_AUTO}; }; } // namespace max44009 diff --git a/esphome/components/modbus_controller/output/modbus_output.h b/esphome/components/modbus_controller/output/modbus_output.h index 0fb4bb89ea..3f3cadfe2f 100644 --- a/esphome/components/modbus_controller/output/modbus_output.h +++ b/esphome/components/modbus_controller/output/modbus_output.h @@ -15,7 +15,7 @@ class ModbusFloatOutput : public output::FloatOutput, public Component, public S this->register_type = ModbusRegisterType::HOLDING; this->start_address = start_address; this->offset = offset; - this->bitmask = bitmask; + this->bitmask = 0xFFFFFFFF; this->register_count = register_count; this->sensor_value_type = value_type; this->skip_updates = 0; @@ -47,7 +47,7 @@ class ModbusBinaryOutput : public output::BinaryOutput, public Component, public ModbusBinaryOutput(uint16_t start_address, uint8_t offset) { this->register_type = ModbusRegisterType::COIL; this->start_address = start_address; - this->bitmask = bitmask; + this->bitmask = 0xFFFFFFFF; this->sensor_value_type = SensorValueType::BIT; this->skip_updates = 0; this->register_count = 1; diff --git a/esphome/components/tuya/climate/tuya_climate.h b/esphome/components/tuya/climate/tuya_climate.h index 31bef57639..09f3fd30c3 100644 --- a/esphome/components/tuya/climate/tuya_climate.h +++ b/esphome/components/tuya/climate/tuya_climate.h @@ -105,8 +105,8 @@ class TuyaClimate : public climate::Climate, public Component { optional sleep_id_{}; optional eco_temperature_{}; TuyaDatapointType eco_type_{}; - uint8_t active_state_; - uint8_t fan_state_; + uint8_t active_state_{0}; + uint8_t fan_state_{0}; optional swing_vertical_id_{}; optional swing_horizontal_id_{}; optional fan_speed_id_{}; @@ -119,9 +119,9 @@ class TuyaClimate : public climate::Climate, public Component { bool swing_horizontal_{false}; bool heating_state_{false}; bool cooling_state_{false}; - float manual_temperature_; - bool eco_; - bool sleep_; + float manual_temperature_{NAN}; + bool eco_{false}; + bool sleep_{false}; bool reports_fahrenheit_{false}; }; From 3016cd363617d080e7eb6ed6532e5668cb07bdbc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 09:29:08 -1000 Subject: [PATCH 104/115] Bump github/codeql-action from 4.34.1 to 4.35.1 (#15245) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6baab70b42..67f4690ac9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: category: "/language:${{matrix.language}}" From a2dee21e8e8f43c9ba68ab5a7b99908d7cd22eaf Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Fri, 27 Mar 2026 21:24:19 +0100 Subject: [PATCH 105/115] [nextion] Replace `std::deque` queues with `std::list` (#15211) --- esphome/components/nextion/nextion.cpp | 14 ++++++-------- esphome/components/nextion/nextion.h | 12 ++++++------ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index fa1582c209..964dbfb660 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -841,10 +841,10 @@ void Nextion::process_nextion_commands_() { if (this->max_q_age_ms_ > 0 && !this->nextion_queue_.empty() && ms - this->nextion_queue_.front()->queue_time > this->max_q_age_ms_) { - for (size_t i = 0; i < this->nextion_queue_.size(); i++) { - NextionComponentBase *component = this->nextion_queue_[i]->component; - if (ms - this->nextion_queue_[i]->queue_time > this->max_q_age_ms_) { - if (this->nextion_queue_[i]->queue_time == 0) { + for (auto it = this->nextion_queue_.begin(); it != this->nextion_queue_.end();) { + NextionComponentBase *component = (*it)->component; + if (ms - (*it)->queue_time > this->max_q_age_ms_) { + if ((*it)->queue_time == 0) { ESP_LOGD(TAG, "Remove old queue '%s':'%s' (t=0)", component->get_queue_type_string().c_str(), component->get_variable_name().c_str()); } @@ -863,10 +863,8 @@ void Nextion::process_nextion_commands_() { delete component; // NOLINT(cppcoreguidelines-owning-memory) } - delete this->nextion_queue_[i]; // NOLINT(cppcoreguidelines-owning-memory) - - this->nextion_queue_.erase(this->nextion_queue_.begin() + i); - i--; + delete *it; // NOLINT(cppcoreguidelines-owning-memory) + it = this->nextion_queue_.erase(it); } else { break; diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 217d2e605d..b5aaecd667 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1,16 +1,16 @@ #pragma once -#include +#include #include +#include "esphome/components/display/display.h" +#include "esphome/components/display/display_color_utils.h" +#include "esphome/components/uart/uart.h" #include "esphome/core/defines.h" #include "esphome/core/time.h" -#include "esphome/components/uart/uart.h" #include "nextion_base.h" #include "nextion_component.h" -#include "esphome/components/display/display.h" -#include "esphome/components/display/display_color_utils.h" #ifdef USE_NEXTION_TFT_UPLOAD #ifdef USE_ESP32 @@ -1391,8 +1391,8 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void process_pending_in_queue_(); #endif // USE_NEXTION_COMMAND_SPACING - std::deque nextion_queue_; - std::deque waveform_queue_; + std::list nextion_queue_; + std::list waveform_queue_; uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag); void all_components_send_state_(bool force_update = false); uint32_t comok_sent_ = 0; From d245b9f123e37618e51f027412f9cc860309478a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:24:03 -0400 Subject: [PATCH 106/115] [sm2135] Fix copy-paste error in setup pin mode (#15248) --- esphome/components/sm2135/sm2135.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/sm2135/sm2135.cpp b/esphome/components/sm2135/sm2135.cpp index 1293c3f321..c3d10e70c2 100644 --- a/esphome/components/sm2135/sm2135.cpp +++ b/esphome/components/sm2135/sm2135.cpp @@ -25,7 +25,7 @@ void SM2135::setup() { this->data_pin_->pin_mode(gpio::FLAG_OUTPUT); this->clock_pin_->setup(); this->clock_pin_->digital_write(false); - this->data_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->clock_pin_->pin_mode(gpio::FLAG_OUTPUT); this->data_pin_->pin_mode(gpio::FLAG_PULLUP); this->clock_pin_->pin_mode(gpio::FLAG_PULLUP); From 24b8a95340d79ad00157c261e0e3c9f92998439e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:24:15 -0400 Subject: [PATCH 107/115] [pid] Remove unused PIDSimulator class (#15247) --- esphome/components/pid/pid_autotuner.h | 1 - esphome/components/pid/pid_simulator.h | 77 -------------------------- 2 files changed, 78 deletions(-) delete mode 100644 esphome/components/pid/pid_simulator.h diff --git a/esphome/components/pid/pid_autotuner.h b/esphome/components/pid/pid_autotuner.h index 98dc02bcc4..1db9ca7138 100644 --- a/esphome/components/pid/pid_autotuner.h +++ b/esphome/components/pid/pid_autotuner.h @@ -3,7 +3,6 @@ #include "esphome/core/component.h" #include "esphome/core/optional.h" #include "pid_controller.h" -#include "pid_simulator.h" #include diff --git a/esphome/components/pid/pid_simulator.h b/esphome/components/pid/pid_simulator.h deleted file mode 100644 index 629784cea5..0000000000 --- a/esphome/components/pid/pid_simulator.h +++ /dev/null @@ -1,77 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/core/helpers.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/output/float_output.h" - -#include - -namespace esphome { -namespace pid { - -class PIDSimulator : public PollingComponent, public output::FloatOutput { - public: - PIDSimulator() : PollingComponent(1000) {} - - float surface = 1; /// surface area in m² - float mass = 3; /// mass of simulated object in kg - float temperature = 21; /// current temperature of object in °C - float efficiency = 0.98; /// heating efficiency, 1 is 100% efficient - float thermal_conductivity = 15; /// thermal conductivity of surface are in W/(m*K), here: steel - float specific_heat_capacity = 4.182; /// specific heat capacity of mass in kJ/(kg*K), here: water - float heat_power = 500; /// Heating power in W - float ambient_temperature = 20; /// Ambient temperature in °C - float update_interval = 1; /// The simulated updated interval in seconds - std::vector delayed_temps; /// storage of past temperatures for delaying temperature reading - size_t delay_cycles = 15; /// how many update cycles to delay the output - float output_value = 0.0; /// Current output value of heating element - sensor::Sensor *sensor = new sensor::Sensor(); - - float delta_t(float power) { - // P = Q / t - // Q = c * m * 𝚫t - // 𝚫t = (P*t) / (c*m) - float c = this->specific_heat_capacity; - float t = this->update_interval; - float p = power / 1000; // in kW - float m = this->mass; - return (p * t) / (c * m); - } - - float update_temp() { - float value = clamp(output_value, 0.0f, 1.0f); - - // Heat - float power = value * heat_power * efficiency; - temperature += this->delta_t(power); - - // Cool - // Q = k_w * A * (T_mass - T_ambient) - // P = Q / t - float dt = temperature - ambient_temperature; - float cool_power = (thermal_conductivity * surface * dt) / update_interval; - temperature -= this->delta_t(cool_power); - - // Delay temperature readings - delayed_temps.push_back(temperature); - if (delayed_temps.size() > delay_cycles) - delayed_temps.erase(delayed_temps.begin()); - float prev_temp = this->delayed_temps[0]; - float alpha = 0.1f; - float ret = (1 - alpha) * prev_temp + alpha * temperature; - return ret; - } - - void setup() override { sensor->publish_state(this->temperature); } - void update() override { - float new_temp = this->update_temp(); - sensor->publish_state(new_temp); - } - - protected: - void write_state(float state) override { this->output_value = state; } -}; - -} // namespace pid -} // namespace esphome From 68d9f657adf9d2a65621486627596b5f6275a317 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:32:37 -0400 Subject: [PATCH 108/115] [bl0940] Fix energy reference default using wrong constant in legacy mode (#15249) --- esphome/components/bl0940/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/bl0940/sensor.py b/esphome/components/bl0940/sensor.py index d2e0ea435d..f36250ecdf 100644 --- a/esphome/components/bl0940/sensor.py +++ b/esphome/components/bl0940/sensor.py @@ -124,7 +124,7 @@ def set_reference_values(config): config.setdefault(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_LEGACY_UREF) config.setdefault(CONF_CURRENT_REFERENCE, DEFAULT_BL0940_LEGACY_IREF) config.setdefault(CONF_POWER_REFERENCE, DEFAULT_BL0940_LEGACY_PREF) - config.setdefault(CONF_ENERGY_REFERENCE, DEFAULT_BL0940_LEGACY_PREF) + config.setdefault(CONF_ENERGY_REFERENCE, DEFAULT_BL0940_LEGACY_EREF) else: vref = config.get(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_VREF) r_one = config.get(CONF_RESISTOR_ONE, DEFAULT_BL0940_R1) From 76d75850a3bd100fb056d5a82c8792abb3f23154 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:35:12 -0400 Subject: [PATCH 109/115] [sgp4x] Remove dead voc_baseline config option (#15250) --- esphome/components/sgp4x/sensor.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/esphome/components/sgp4x/sensor.py b/esphome/components/sgp4x/sensor.py index 8d52ffb4f2..1e58a0f26a 100644 --- a/esphome/components/sgp4x/sensor.py +++ b/esphome/components/sgp4x/sensor.py @@ -15,7 +15,6 @@ from esphome.const import ( CONF_STORE_BASELINE, CONF_TEMPERATURE_SOURCE, CONF_VOC, - CONF_VOC_BASELINE, DEVICE_CLASS_AQI, ICON_RADIATOR, STATE_CLASS_MEASUREMENT, @@ -83,7 +82,6 @@ CONFIG_SCHEMA = cv.All( state_class=STATE_CLASS_MEASUREMENT, ).extend(NOX_SENSOR), cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, - cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, cv.Optional(CONF_COMPENSATION): cv.Schema( { cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor), @@ -112,9 +110,6 @@ async def to_code(config): cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE])) - if CONF_VOC_BASELINE in config: - cg.add(var.set_voc_baseline(CONF_VOC_BASELINE)) - if CONF_VOC in config: sens = await sensor.new_sensor(config[CONF_VOC]) cg.add(var.set_voc_sensor(sens)) From f6c63c62e43b88b8933a3f02f866c7e0936dd290 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Fri, 27 Mar 2026 17:59:26 -0500 Subject: [PATCH 110/115] [tmp117] Code clean-up (#15260) --- esphome/components/tmp117/tmp117.cpp | 17 +++++++---------- esphome/components/tmp117/tmp117.h | 6 ++---- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/esphome/components/tmp117/tmp117.cpp b/esphome/components/tmp117/tmp117.cpp index f8f52266e0..b3e900f5b6 100644 --- a/esphome/components/tmp117/tmp117.cpp +++ b/esphome/components/tmp117/tmp117.cpp @@ -4,8 +4,7 @@ #include "tmp117.h" #include "esphome/core/log.h" -namespace esphome { -namespace tmp117 { +namespace esphome::tmp117 { static const char *const TAG = "tmp117"; @@ -18,11 +17,10 @@ void TMP117Component::update() { if ((uint16_t) data != 0x8000) { float temperature = data * 0.0078125f; - ESP_LOGD(TAG, "Got temperature=%.2f°C", temperature); this->publish_state(temperature); this->status_clear_warning(); } else { - ESP_LOGD(TAG, "TMP117 not ready"); + ESP_LOGD(TAG, "Not ready"); } } void TMP117Component::setup() { @@ -38,7 +36,7 @@ void TMP117Component::setup() { } } void TMP117Component::dump_config() { - ESP_LOGD(TAG, "TMP117:"); + ESP_LOGCONFIG(TAG, "TMP117:"); LOG_I2C_DEVICE(this); if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); @@ -48,7 +46,7 @@ void TMP117Component::dump_config() { bool TMP117Component::read_data_(int16_t *data) { if (!this->read_byte_16(0, (uint16_t *) data)) { - ESP_LOGW(TAG, "Updating TMP117 failed!"); + ESP_LOGW(TAG, "Updating failed"); return false; } return true; @@ -56,7 +54,7 @@ bool TMP117Component::read_data_(int16_t *data) { bool TMP117Component::read_config_(uint16_t *config) { if (!this->read_byte_16(1, (uint16_t *) config)) { - ESP_LOGW(TAG, "Reading TMP117 config failed!"); + ESP_LOGW(TAG, "Reading config failed"); return false; } return true; @@ -64,11 +62,10 @@ bool TMP117Component::read_config_(uint16_t *config) { bool TMP117Component::write_config_(uint16_t config) { if (!this->write_byte_16(1, config)) { - ESP_LOGE(TAG, "Writing TMP117 config failed!"); + ESP_LOGE(TAG, "Writing config failed"); return false; } return true; } -} // namespace tmp117 -} // namespace esphome +} // namespace esphome::tmp117 diff --git a/esphome/components/tmp117/tmp117.h b/esphome/components/tmp117/tmp117.h index f501ee270c..a8fe7ac7ce 100644 --- a/esphome/components/tmp117/tmp117.h +++ b/esphome/components/tmp117/tmp117.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace tmp117 { +namespace esphome::tmp117 { class TMP117Component : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { public: @@ -22,5 +21,4 @@ class TMP117Component : public PollingComponent, public i2c::I2CDevice, public s uint16_t config_; }; -} // namespace tmp117 -} // namespace esphome +} // namespace esphome::tmp117 From a99f051e19759c70e2007deb4b873327c9127699 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Sat, 28 Mar 2026 00:49:00 +0100 Subject: [PATCH 111/115] [nextion] Replace queue name string literals with short Nextion-native identifiers (#15215) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/nextion/nextion.cpp | 13 +- .../components/nextion/nextion_commands.cpp | 115 ++++++++---------- 2 files changed, 62 insertions(+), 66 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 964dbfb660..97d9b36e4c 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -241,7 +241,7 @@ bool Nextion::send_command(const char *command) { return false; if (this->send_command_(command)) { - this->add_no_result_to_queue_("send_command"); + this->add_no_result_to_queue_("command"); return true; } return false; @@ -262,7 +262,7 @@ bool Nextion::send_command_printf(const char *format, ...) { } if (this->send_command_(buffer)) { - this->add_no_result_to_queue_("send_command_printf"); + this->add_no_result_to_queue_("command_printf"); return true; } return false; @@ -853,8 +853,13 @@ void Nextion::process_nextion_commands_() { this->is_sleeping_ = false; } - ESP_LOGD(TAG, "Remove old queue '%s':'%s'", component->get_queue_type_string().c_str(), - component->get_variable_name().c_str()); + if ((*it)->pending_command.empty()) { + ESP_LOGD(TAG, "Remove old queue '%s':'%s'", component->get_queue_type_string().c_str(), + component->get_variable_name().c_str()); + } else { + ESP_LOGD(TAG, "Remove old queue '%s':'%s' cmd:'%s'", component->get_queue_type_string().c_str(), + component->get_variable_name().c_str(), (*it)->pending_command.c_str()); + } if (component->get_queue_type() == NextionQueueType::NO_RESULT) { if (component->get_variable_name() == "sleep_wake") { diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 4ddbfbee6a..6718646efa 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -12,7 +12,7 @@ void Nextion::soft_reset() { this->send_command_("rest"); } void Nextion::set_wake_up_page(uint8_t wake_up_page) { this->wake_up_page_ = wake_up_page; - this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", wake_up_page, true); + this->add_no_result_to_queue_with_set_internal_("wup", "wup", wake_up_page, true); } void Nextion::set_touch_sleep_timeout(const uint16_t touch_sleep_timeout) { @@ -23,7 +23,7 @@ void Nextion::set_touch_sleep_timeout(const uint16_t touch_sleep_timeout) { this->touch_sleep_timeout_ = touch_sleep_timeout; } - this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", this->touch_sleep_timeout_, true); + this->add_no_result_to_queue_with_set_internal_("thsp", "thsp", this->touch_sleep_timeout_, true); } void Nextion::sleep(bool sleep) { @@ -58,115 +58,107 @@ bool Nextion::set_protocol_reparse_mode(bool active_mode) { // Set Colors - Background void Nextion::set_component_background_color(const char *component, uint16_t color) { - this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%" PRIu16, component, color); + this->add_no_result_to_queue_with_printf_(".bco", "%s.bco=%" PRIu16, component, color); } void Nextion::set_component_background_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%s", component, color); + this->add_no_result_to_queue_with_printf_(".bco", "%s.bco=%s", component, color); } void Nextion::set_component_background_color(const char *component, Color color) { - this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%d", component, - display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_(".bco", "%s.bco=%d", component, display::ColorUtil::color_to_565(color)); } // Set Colors - Background (pressed) void Nextion::set_component_pressed_background_color(const char *component, uint16_t color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%" PRIu16, component, - color); + this->add_no_result_to_queue_with_printf_(".bco2", "%s.bco2=%" PRIu16, component, color); } void Nextion::set_component_pressed_background_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%s", component, color); + this->add_no_result_to_queue_with_printf_(".bco2", "%s.bco2=%s", component, color); } void Nextion::set_component_pressed_background_color(const char *component, Color color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%d", component, - display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_(".bco2", "%s.bco2=%d", component, display::ColorUtil::color_to_565(color)); } // Set Colors - Foreground void Nextion::set_component_foreground_color(const char *component, uint16_t color) { - this->add_no_result_to_queue_with_printf_("set_component_foreground_color", "%s.pco=%" PRIu16, component, color); + this->add_no_result_to_queue_with_printf_(".pco", "%s.pco=%" PRIu16, component, color); } void Nextion::set_component_foreground_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_foreground_color", "%s.pco=%s", component, color); + this->add_no_result_to_queue_with_printf_(".pco", "%s.pco=%s", component, color); } void Nextion::set_component_foreground_color(const char *component, Color color) { - this->add_no_result_to_queue_with_printf_("set_component_foreground_color", "%s.pco=%d", component, - display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_(".pco", "%s.pco=%d", component, display::ColorUtil::color_to_565(color)); } // Set Colors - Foreground (pressed) void Nextion::set_component_pressed_foreground_color(const char *component, uint16_t color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", "%s.pco2=%" PRIu16, component, - color); + this->add_no_result_to_queue_with_printf_(".pco2", "%s.pco2=%" PRIu16, component, color); } void Nextion::set_component_pressed_foreground_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", "%s.pco2=%s", component, color); + this->add_no_result_to_queue_with_printf_(".pco2", "%s.pco2=%s", component, color); } void Nextion::set_component_pressed_foreground_color(const char *component, Color color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", "%s.pco2=%d", component, - display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_(".pco2", "%s.pco2=%d", component, display::ColorUtil::color_to_565(color)); } // Set Colors - Font void Nextion::set_component_font_color(const char *component, uint16_t color) { - this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%" PRIu16, component, color); + this->add_no_result_to_queue_with_printf_(".pco", "%s.pco=%" PRIu16, component, color); } void Nextion::set_component_font_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%s", component, color); + this->add_no_result_to_queue_with_printf_(".pco", "%s.pco=%s", component, color); } void Nextion::set_component_font_color(const char *component, Color color) { - this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%d", component, - display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_(".pco", "%s.pco=%d", component, display::ColorUtil::color_to_565(color)); } // Set Colors - Font (pressed) void Nextion::set_component_pressed_font_color(const char *component, uint16_t color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%" PRIu16, component, color); + this->add_no_result_to_queue_with_printf_(".pco2", "%s.pco2=%" PRIu16, component, color); } void Nextion::set_component_pressed_font_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%s", component, color); + this->add_no_result_to_queue_with_printf_(".pco2", "%s.pco2=%s", component, color); } void Nextion::set_component_pressed_font_color(const char *component, Color color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%d", component, - display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_(".pco2", "%s.pco2=%d", component, display::ColorUtil::color_to_565(color)); } // Set picture void Nextion::set_component_pic(const char *component, uint16_t pic_id) { - this->add_no_result_to_queue_with_printf_("set_component_pic", "%s.pic=%" PRIu16, component, pic_id); + this->add_no_result_to_queue_with_printf_(".pic", "%s.pic=%" PRIu16, component, pic_id); } void Nextion::set_component_picc(const char *component, uint16_t pic_id) { - this->add_no_result_to_queue_with_printf_("set_component_picc", "%s.picc=%" PRIu16, component, pic_id); + this->add_no_result_to_queue_with_printf_(".picc", "%s.picc=%" PRIu16, component, pic_id); } // Set video void Nextion::set_component_vid(const char *component, uint8_t vid_id) { - this->add_no_result_to_queue_with_printf_("set_component_vid", "%s.vid=%" PRIu8, component, vid_id); + this->add_no_result_to_queue_with_printf_(".vid", "%s.vid=%" PRIu8, component, vid_id); } void Nextion::set_component_drag(const char *component, bool drag) { - this->add_no_result_to_queue_with_printf_("set_component_drag", "%s.drag=%i", component, drag ? 1 : 0); + this->add_no_result_to_queue_with_printf_(".drag", "%s.drag=%i", component, drag ? 1 : 0); } void Nextion::set_component_aph(const char *component, uint8_t aph) { - this->add_no_result_to_queue_with_printf_("set_component_aph", "%s.aph=%" PRIu8, component, aph); + this->add_no_result_to_queue_with_printf_(".aph", "%s.aph=%" PRIu8, component, aph); } void Nextion::set_component_position(const char *component, uint32_t x, uint32_t y) { - this->add_no_result_to_queue_with_printf_("set_component_position_x", "%s.x=%" PRIu32, component, x); - this->add_no_result_to_queue_with_printf_("set_component_position_y", "%s.y=%" PRIu32, component, y); + this->add_no_result_to_queue_with_printf_(".x", "%s.x=%" PRIu32, component, x); + this->add_no_result_to_queue_with_printf_(".y", "%s.y=%" PRIu32, component, y); } void Nextion::set_component_text_printf(const char *component, const char *format, ...) { @@ -180,29 +172,29 @@ void Nextion::set_component_text_printf(const char *component, const char *forma } // General Nextion -void Nextion::goto_page(const char *page) { this->add_no_result_to_queue_with_printf_("goto_page", "page %s", page); } -void Nextion::goto_page(uint8_t page) { this->add_no_result_to_queue_with_printf_("goto_page", "page %i", page); } +void Nextion::goto_page(const char *page) { this->add_no_result_to_queue_with_printf_("page", "page %s", page); } +void Nextion::goto_page(uint8_t page) { this->add_no_result_to_queue_with_printf_("page", "page %i", page); } void Nextion::set_backlight_brightness(float brightness) { if (brightness < 0 || brightness > 1.0) { ESP_LOGD(TAG, "Brightness out of bounds (0-1.0)"); return; } - this->add_no_result_to_queue_with_printf_("backlight_brightness", "dim=%d", static_cast(brightness * 100)); + this->add_no_result_to_queue_with_printf_("dim", "dim=%d", static_cast(brightness * 100)); } void Nextion::set_auto_wake_on_touch(bool auto_wake_on_touch) { this->connection_state_.auto_wake_on_touch_ = auto_wake_on_touch; - this->add_no_result_to_queue_with_set("auto_wake_on_touch", "thup", auto_wake_on_touch ? 1 : 0); + this->add_no_result_to_queue_with_set("thup", "thup", auto_wake_on_touch ? 1 : 0); } // General Component void Nextion::set_component_font(const char *component, uint8_t font_id) { - this->add_no_result_to_queue_with_printf_("set_component_font", "%s.font=%" PRIu8, component, font_id); + this->add_no_result_to_queue_with_printf_(".font", "%s.font=%" PRIu8, component, font_id); } void Nextion::set_component_visibility(const char *component, bool show) { - this->add_no_result_to_queue_with_printf_("set_component_visibility", "vis %s,%d", component, show ? 1 : 0); + this->add_no_result_to_queue_with_printf_("vis", "vis %s,%d", component, show ? 1 : 0); } void Nextion::hide_component(const char *component) { this->set_component_visibility(component, false); } @@ -210,56 +202,55 @@ void Nextion::hide_component(const char *component) { this->set_component_visibi void Nextion::show_component(const char *component) { this->set_component_visibility(component, true); } void Nextion::enable_component_touch(const char *component) { - this->add_no_result_to_queue_with_printf_("enable_component_touch", "tsw %s,1", component); + this->add_no_result_to_queue_with_printf_("tsw", "tsw %s,1", component); } void Nextion::disable_component_touch(const char *component) { - this->add_no_result_to_queue_with_printf_("disable_component_touch", "tsw %s,0", component); + this->add_no_result_to_queue_with_printf_("tsw", "tsw %s,0", component); } void Nextion::set_component_text(const char *component, const char *text) { - this->add_no_result_to_queue_with_printf_("set_component_text", "%s.txt=\"%s\"", component, text); + this->add_no_result_to_queue_with_printf_(".txt", "%s.txt=\"%s\"", component, text); } void Nextion::set_component_value(const char *component, int32_t value) { - this->add_no_result_to_queue_with_printf_("set_component_value", "%s.val=%" PRId32, component, value); + this->add_no_result_to_queue_with_printf_(".val", "%s.val=%" PRId32, component, value); } void Nextion::add_waveform_data(uint8_t component_id, uint8_t channel_number, uint8_t value) { - this->add_no_result_to_queue_with_printf_("add_waveform_data", "add %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, - channel_number, value); + this->add_no_result_to_queue_with_printf_("add", "add %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, channel_number, + value); } void Nextion::open_waveform_channel(uint8_t component_id, uint8_t channel_number, uint8_t value) { - this->add_no_result_to_queue_with_printf_("open_waveform_channel", "addt %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, - channel_number, value); + this->add_no_result_to_queue_with_printf_("addt", "addt %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, channel_number, + value); } void Nextion::set_component_coordinates(const char *component, uint16_t x, uint16_t y) { - this->add_no_result_to_queue_with_printf_("set_component_coordinates command 1", "%s.xcen=%" PRIu16, component, x); - this->add_no_result_to_queue_with_printf_("set_component_coordinates command 2", "%s.ycen=%" PRIu16, component, y); + this->add_no_result_to_queue_with_printf_(".xcen", "%s.xcen=%" PRIu16, component, x); + this->add_no_result_to_queue_with_printf_(".ycen", "%s.ycen=%" PRIu16, component, y); } // Drawing void Nextion::display_picture(uint16_t picture_id, uint16_t x_start, uint16_t y_start) { - this->add_no_result_to_queue_with_printf_("display_picture", "pic %" PRIu16 ", %" PRIu16 ", %" PRIu16, x_start, - y_start, picture_id); + this->add_no_result_to_queue_with_printf_("pic", "pic %" PRIu16 ", %" PRIu16 ", %" PRIu16, x_start, y_start, + picture_id); } void Nextion::fill_area(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, uint16_t color) { - this->add_no_result_to_queue_with_printf_( - "fill_area", "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, x1, y1, width, height, color); -} - -void Nextion::fill_area(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, const char *color) { - this->add_no_result_to_queue_with_printf_("fill_area", "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%s", x1, + this->add_no_result_to_queue_with_printf_("fill", "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, x1, y1, width, height, color); } +void Nextion::fill_area(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, const char *color) { + this->add_no_result_to_queue_with_printf_("fill", "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%s", x1, y1, + width, height, color); +} + void Nextion::fill_area(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, Color color) { - this->add_no_result_to_queue_with_printf_("fill_area", - "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, x1, y1, - width, height, display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_("fill", "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, x1, + y1, width, height, display::ColorUtil::color_to_565(color)); } void Nextion::line(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color) { From 34410e92b7e1f9ddd15629ecb5903932554e9077 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:55:40 -0400 Subject: [PATCH 112/115] [as5600] Remove dead angle/position sensor code (#15254) --- esphome/components/as5600/sensor/__init__.py | 15 ----------- .../as5600/sensor/as5600_sensor.cpp | 25 +++---------------- .../components/as5600/sensor/as5600_sensor.h | 6 ----- 3 files changed, 4 insertions(+), 42 deletions(-) diff --git a/esphome/components/as5600/sensor/__init__.py b/esphome/components/as5600/sensor/__init__.py index e84733a484..cf67a3f203 100644 --- a/esphome/components/as5600/sensor/__init__.py +++ b/esphome/components/as5600/sensor/__init__.py @@ -2,11 +2,9 @@ import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv from esphome.const import ( - CONF_ANGLE, CONF_GAIN, CONF_ID, CONF_MAGNITUDE, - CONF_POSITION, CONF_STATUS, ENTITY_CATEGORY_DIAGNOSTIC, ICON_MAGNET, @@ -21,7 +19,6 @@ DEPENDENCIES = ["as5600"] AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingComponent) -CONF_RAW_ANGLE = "raw_angle" CONF_RAW_POSITION = "raw_position" CONF_SLOW_FILTER = "slow_filter" CONF_FAST_FILTER = "fast_filter" @@ -89,18 +86,6 @@ async def to_code(config): if out_of_range_mode_config := config.get(CONF_OUT_OF_RANGE_MODE): cg.add(var.set_out_of_range_mode(out_of_range_mode_config)) - if angle_config := config.get(CONF_ANGLE): - sens = await sensor.new_sensor(angle_config) - cg.add(var.set_angle_sensor(sens)) - - if raw_angle_config := config.get(CONF_RAW_ANGLE): - sens = await sensor.new_sensor(raw_angle_config) - cg.add(var.set_raw_angle_sensor(sens)) - - if position_config := config.get(CONF_POSITION): - sens = await sensor.new_sensor(position_config) - cg.add(var.set_position_sensor(sens)) - if raw_position_config := config.get(CONF_RAW_POSITION): sens = await sensor.new_sensor(raw_position_config) cg.add(var.set_raw_position_sensor(sens)) diff --git a/esphome/components/as5600/sensor/as5600_sensor.cpp b/esphome/components/as5600/sensor/as5600_sensor.cpp index 1c0f4bad2c..4e549d24d5 100644 --- a/esphome/components/as5600/sensor/as5600_sensor.cpp +++ b/esphome/components/as5600/sensor/as5600_sensor.cpp @@ -25,27 +25,10 @@ static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R void AS5600Sensor::dump_config() { LOG_SENSOR("", "AS5600 Sensor", this); ESP_LOGCONFIG(TAG, " Out of Range Mode: %u", this->out_of_range_mode_); - if (this->angle_sensor_ != nullptr) { - LOG_SENSOR(" ", "Angle Sensor", this->angle_sensor_); - } - if (this->raw_angle_sensor_ != nullptr) { - LOG_SENSOR(" ", "Raw Angle Sensor", this->raw_angle_sensor_); - } - if (this->position_sensor_ != nullptr) { - LOG_SENSOR(" ", "Position Sensor", this->position_sensor_); - } - if (this->raw_position_sensor_ != nullptr) { - LOG_SENSOR(" ", "Raw Position Sensor", this->raw_position_sensor_); - } - if (this->gain_sensor_ != nullptr) { - LOG_SENSOR(" ", "Gain Sensor", this->gain_sensor_); - } - if (this->magnitude_sensor_ != nullptr) { - LOG_SENSOR(" ", "Magnitude Sensor", this->magnitude_sensor_); - } - if (this->status_sensor_ != nullptr) { - LOG_SENSOR(" ", "Status Sensor", this->status_sensor_); - } + LOG_SENSOR(" ", "Raw Position Sensor", this->raw_position_sensor_); + LOG_SENSOR(" ", "Gain Sensor", this->gain_sensor_); + LOG_SENSOR(" ", "Magnitude Sensor", this->magnitude_sensor_); + LOG_SENSOR(" ", "Status Sensor", this->status_sensor_); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/as5600/sensor/as5600_sensor.h b/esphome/components/as5600/sensor/as5600_sensor.h index d471be49b5..77593f4b12 100644 --- a/esphome/components/as5600/sensor/as5600_sensor.h +++ b/esphome/components/as5600/sensor/as5600_sensor.h @@ -15,9 +15,6 @@ class AS5600Sensor : public PollingComponent, public Parented, void update() override; void dump_config() override; - void set_angle_sensor(sensor::Sensor *angle_sensor) { this->angle_sensor_ = angle_sensor; } - void set_raw_angle_sensor(sensor::Sensor *raw_angle_sensor) { this->raw_angle_sensor_ = raw_angle_sensor; } - void set_position_sensor(sensor::Sensor *position_sensor) { this->position_sensor_ = position_sensor; } void set_raw_position_sensor(sensor::Sensor *raw_position_sensor) { this->raw_position_sensor_ = raw_position_sensor; } @@ -28,9 +25,6 @@ class AS5600Sensor : public PollingComponent, public Parented, OutRangeMode get_out_of_range_mode() { return this->out_of_range_mode_; } protected: - sensor::Sensor *angle_sensor_{nullptr}; - sensor::Sensor *raw_angle_sensor_{nullptr}; - sensor::Sensor *position_sensor_{nullptr}; sensor::Sensor *raw_position_sensor_{nullptr}; sensor::Sensor *gain_sensor_{nullptr}; sensor::Sensor *magnitude_sensor_{nullptr}; From 47774fb644a162c5e38941a1b392ab7374aecb2e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:55:57 -0400 Subject: [PATCH 113/115] [modbus_controller] Fix wrong enum in function_code_to_register (#15253) --- esphome/components/modbus_controller/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index dfc43bf23b..cb0969913a 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -362,7 +362,7 @@ async def register_modbus_device(var, config): def function_code_to_register(function_code): FUNCTION_CODE_TYPE_MAP = { "read_coils": ModbusRegisterType.COIL, - "read_discrete_inputs": ModbusRegisterType.DISCRETE, + "read_discrete_inputs": ModbusRegisterType.DISCRETE_INPUT, "read_holding_registers": ModbusRegisterType.HOLDING, "read_input_registers": ModbusRegisterType.READ, "write_single_coil": ModbusRegisterType.COIL, From b6abfec82e4e51bac2f0a927ca3180b174d70c02 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:22:24 -0400 Subject: [PATCH 114/115] [core] Fix area/device hash collision validation not running (#15259) --- esphome/config.py | 18 ++++++++++++++++++ esphome/core/config.py | 15 ++++++--------- script/ci-custom.py | 12 ++++++++++++ tests/unit_tests/core/test_config.py | 18 ++++++++++++++++++ .../config/area_singular_hash_collision.yaml | 10 ++++++++++ 5 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 tests/unit_tests/fixtures/core/config/area_singular_hash_collision.yaml diff --git a/esphome/config.py b/esphome/config.py index 7a6feea3d3..641b6ec1b4 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -958,6 +958,23 @@ class FinalValidateValidationStep(ConfigValidationStep): fv.full_config.reset(token) +class CoreFinalValidateStep(ConfigValidationStep): + """Run final validation on core esphome config (area/device hash collisions).""" + + # Same priority as component final validate steps + priority = -20.0 + + def run(self, result: Config) -> None: + if result.errors: + return + + token = fv.full_config.set(result) + with result.catch_error([CONF_ESPHOME]): + if CONF_ESPHOME in result: + core_config.validate_ids_and_references(result[CONF_ESPHOME]) + fv.full_config.reset(token) + + class PinUseValidationCheck(ConfigValidationStep): """Check for pin reuse""" @@ -1085,6 +1102,7 @@ def validate_config( for domain, conf in config.items(): result.add_validation_step(LoadValidationStep(domain, conf)) result.add_validation_step(IDPassValidationStep()) + result.add_validation_step(CoreFinalValidateStep()) result.add_validation_step(PinUseValidationCheck()) result.add_validation_step(RemoveReferenceValidationStep()) diff --git a/esphome/core/config.py b/esphome/core/config.py index e02c6ec75f..c47693c783 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -156,22 +156,22 @@ def validate_ids_and_references(config: ConfigType) -> ConfigType: hash_dict[hash_val] = id_obj.id # Collect all areas - all_areas: list[dict[str, str | core.ID]] = [] + all_areas: list[tuple[dict[str, str | core.ID], str]] = [] if CONF_AREA in config: - all_areas.append(config[CONF_AREA]) - all_areas.extend(config[CONF_AREAS]) + all_areas.append((config[CONF_AREA], CONF_AREA)) + all_areas.extend((area, CONF_AREAS) for area in config.get(CONF_AREAS, [])) # Validate area hash collisions and collect IDs area_hashes: dict[int, str] = {} area_ids: set[str] = set() - for area in all_areas: + for area, key in all_areas: area_id: core.ID = area[CONF_ID] - check_hash_collision(area_id, area_hashes, "Area", [CONF_AREAS, area_id.id]) + check_hash_collision(area_id, area_hashes, "Area", [key, area_id.id]) area_ids.add(area_id.id) # Validate device hash collisions and area references device_hashes: dict[int, str] = {} - for device in config[CONF_DEVICES]: + for device in config.get(CONF_DEVICES, []): device_id: core.ID = device[CONF_ID] check_hash_collision( device_id, device_hashes, "Device", [CONF_DEVICES, device_id.id] @@ -329,9 +329,6 @@ CONFIG_SCHEMA = cv.All( ) -FINAL_VALIDATE_SCHEMA = cv.All(validate_ids_and_references) - - PRELOAD_CONFIG_SCHEMA = cv.Schema( { cv.Required(CONF_NAME): cv.valid_name, diff --git a/script/ci-custom.py b/script/ci-custom.py index 7d0680a491..ad39f92005 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -1006,6 +1006,18 @@ def lint_log_in_header(fname, line, col, content): ) +@lint_content_find_check( + "FINAL_VALIDATE_SCHEMA", + include=["esphome/core/*.py"], + exclude=["esphome/core/entity_helpers.py"], +) +def lint_final_validate_in_core(fname, line, col, content): + return ( + "FINAL_VALIDATE_SCHEMA in esphome/core/ is not picked up by the component loader. " + "Use CoreFinalValidateStep in esphome/config.py instead." + ) + + def main(): colorama.init() diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index 474d31a90a..6fa8f7ed43 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -248,6 +248,24 @@ def test_area_id_hash_collision( ) +def test_area_singular_hash_collision( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test that area hash collisions between singular area: and areas: list are detected.""" + result = load_config_from_fixture( + yaml_file, "area_singular_hash_collision.yaml", FIXTURES_DIR + ) + assert result is None + + captured = capsys.readouterr() + assert ( + "Area ID 'd6ka' with hash 3082558663 collides with existing area ID 'test_2258'" + in captured.out + ) + # Error path should point to 'areas' (where the colliding entry is), not 'area' + assert "areas" in captured.out + + def test_device_duplicate_id( yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] ) -> None: diff --git a/tests/unit_tests/fixtures/core/config/area_singular_hash_collision.yaml b/tests/unit_tests/fixtures/core/config/area_singular_hash_collision.yaml new file mode 100644 index 0000000000..6e137f5f6e --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/area_singular_hash_collision.yaml @@ -0,0 +1,10 @@ +esphome: + name: test + area: + id: test_2258 + name: "Area 1" + areas: + - id: d6ka + name: "Area 2" + +host: From 7a7c33fdb16f2b32d95d194ed5130471e6bb99e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Mar 2026 15:38:06 -1000 Subject: [PATCH 115/115] [esp32_ble_server] Fix set_value action with static data lists (#15285) --- .../components/esp32_ble_server/ble_server_automations.h | 2 ++ tests/components/esp32_ble_server/common.yaml | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/esphome/components/esp32_ble_server/ble_server_automations.h b/esphome/components/esp32_ble_server/ble_server_automations.h index fe18600280..0bbfdffd5b 100644 --- a/esphome/components/esp32_ble_server/ble_server_automations.h +++ b/esphome/components/esp32_ble_server/ble_server_automations.h @@ -70,6 +70,7 @@ template class BLECharacteristicSetValueAction : public Action, buffer) + void set_buffer(std::initializer_list buffer) { this->buffer_ = std::vector(buffer); } void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } void play(const Ts &...x) override { // If the listener is already set, do nothing @@ -115,6 +116,7 @@ template class BLEDescriptorSetValueAction : public Action, buffer) + void set_buffer(std::initializer_list buffer) { this->buffer_ = std::vector(buffer); } void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } void play(const Ts &...x) override { this->parent_->set_value(this->buffer_.value(x...)); } diff --git a/tests/components/esp32_ble_server/common.yaml b/tests/components/esp32_ble_server/common.yaml index 7fe0b2eb5f..4e34049038 100644 --- a/tests/components/esp32_ble_server/common.yaml +++ b/tests/components/esp32_ble_server/common.yaml @@ -69,3 +69,11 @@ esp32_ble_server: - ble_server.descriptor.set_value: id: test_change_descriptor value: !lambda return bytebuffer::ByteBuffer::wrap({0x03, 0x04, 0x05}).get_data(); + - ble_server.characteristic.set_value: + id: test_change_characteristic + value: + data: [0xfc, 0xef, 0xfe, 0x86] + - ble_server.descriptor.set_value: + id: test_change_descriptor + value: + data: [0x01, 0x02, 0x03]