Merge remote-tracking branch 'origin/dev' into jesserockz-2026-607

This commit is contained in:
Jesse Hills
2026-08-25 08:57:53 +12:00
1273 changed files with 15536 additions and 6209 deletions
+2 -2
View File
@@ -67,7 +67,7 @@ jobs:
with: with:
python-version: "3.12" python-version: "3.12"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Determine tag and whether to push - name: Determine tag and whether to push
id: tag id: tag
@@ -153,7 +153,7 @@ jobs:
with: with:
python-version: "3.12" python-version: "3.12"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Log in to the GitHub container registry - name: Log in to the GitHub container registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
+21 -4
View File
@@ -112,6 +112,12 @@ jobs:
component-test-batches: ${{ steps.determine.outputs.component-test-batches }} component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
validate-only-components: ${{ steps.determine.outputs.validate-only-components }} validate-only-components: ${{ steps.determine.outputs.validate-only-components }}
benchmarks: ${{ steps.determine.outputs.benchmarks }} benchmarks: ${{ steps.determine.outputs.benchmarks }}
# "true" when this run is a pull request into one of the release
# branches. Those pull requests are batches of changes already tested on
# their original dev pull requests, so several jobs below trade coverage
# for turnaround time on them. Matched exactly, not by prefix, so an
# ordinary branch named e.g. "release-notes" is not caught by it.
release-pr: ${{ github.base_ref == 'beta' || github.base_ref == 'release' }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -230,7 +236,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- determine-jobs - determine-jobs
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') && needs.determine-jobs.outputs.core-ci == 'true' if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.release-pr == 'false' && needs.determine-jobs.outputs.core-ci == 'true'
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -449,10 +455,21 @@ jobs:
if: >- if: >-
github.repository == 'esphome/esphome' && ( github.repository == 'esphome/esphome' && (
(github.event_name == 'push' && github.ref_name == 'dev') || (github.event_name == 'push' && github.ref_name == 'dev') ||
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true') (
github.event_name == 'pull_request' &&
needs.determine-jobs.outputs.release-pr == 'false' &&
needs.determine-jobs.outputs.benchmarks == 'true'
)
) )
# CodSpeed benchmarks require a CodSpeed account linked to the repository to run # CodSpeed benchmarks require a CodSpeed account linked to the repository to run
# (https://codspeed.io) -- disabled on forks that aren't esphome/esphome itself. # (https://codspeed.io) -- disabled on forks that aren't esphome/esphome itself.
#
# Pull requests into beta and release are skipped as well. CodSpeed compares a
# pull request against the newest commit of its base branch that has a benchmark
# run of its own, and only dev is benchmarked. A release pull request therefore
# falls back to dev's latest run, so every speed-up merged into dev since the
# release branched is reported as a regression in the release. The changes there
# have already been benchmarked on their original dev pull requests.
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -929,7 +946,7 @@ jobs:
ESPHOME_SDK_NRF_PREFIX: ~/.esphome-sdk-nrf ESPHOME_SDK_NRF_PREFIX: ~/.esphome-sdk-nrf
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 32 || 16 }} max-parallel: ${{ needs.determine-jobs.outputs.release-pr == 'true' && 32 || 16 }}
matrix: matrix:
batch: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }} batch: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }}
steps: steps:
@@ -1016,7 +1033,7 @@ jobs:
# - This catches pin conflicts and other issues in directly changed code # - This catches pin conflicts and other issues in directly changed code
# - Grouped tests use --testing-mode to allow config merging (disables some checks) # - Grouped tests use --testing-mode to allow config merging (disables some checks)
# - Dependencies are safe to group since they weren't modified in this PR # - Dependencies are safe to group since they weren't modified in this PR
if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then if [[ "${{ needs.determine-jobs.outputs.release-pr }}" == "true" ]]; then
directly_changed_csv="" directly_changed_csv=""
echo "Testing components: $components_csv" echo "Testing components: $components_csv"
echo "Target branch: ${{ github.base_ref }} - grouping all components" echo "Target branch: ${{ github.base_ref }} - grouping all components"
+2 -2
View File
@@ -56,7 +56,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@@ -84,6 +84,6 @@ jobs:
exit 1 exit 1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"
+2 -2
View File
@@ -123,7 +123,7 @@ jobs:
python-version: "3.12" python-version: "3.12"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Log in to docker hub - name: Log in to docker hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
@@ -202,7 +202,7 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Log in to docker hub - name: Log in to docker hub
if: matrix.registry == 'dockerhub' if: matrix.registry == 'dockerhub'
+1
View File
@@ -381,6 +381,7 @@ esphome/components/nextion/switch/* @senexcrenshaw
esphome/components/nextion/text_sensor/* @senexcrenshaw esphome/components/nextion/text_sensor/* @senexcrenshaw
esphome/components/nfc/* @jesserockz @kbx81 esphome/components/nfc/* @jesserockz @kbx81
esphome/components/noblex/* @AGalfra esphome/components/noblex/* @AGalfra
esphome/components/noise/* @esphome/core
esphome/components/npi19/* @bakerkj esphome/components/npi19/* @bakerkj
esphome/components/nrf52/* @tomaszduda23 esphome/components/nrf52/* @tomaszduda23
esphome/components/number/* @esphome/core esphome/components/number/* @esphome/core
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt -r /requirements.txt
# Install the ESPHome Device Builder dashboard. # Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.12.0 RUN uv pip install --no-cache-dir esphome-device-builder==1.12.4
RUN \ RUN \
platformio settings set enable_telemetry No \ platformio settings set enable_telemetry No \
+18 -3
View File
@@ -762,9 +762,11 @@ def _wrap_to_code(name, comp, yaml_util):
async def wrapped(conf): async def wrapped(conf):
cg.add(cg.LineComment(f"{name}:")) cg.add(cg.LineComment(f"{name}:"))
if comp.config_schema is not None: if comp.config_schema is not None:
conf_str = yaml_util.dump(conf) # sort_keys: voluptuous fills defaults in set order, so an
# unsorted dump would churn main.cpp and relink every run
conf_str = yaml_util.dump(conf, sort_keys=True)
conf_str = conf_str.replace("//", "") conf_str = conf_str.replace("//", "")
# remove tailing \ to avoid multi-line comment warning # remove trailing \ to avoid multi-line comment warning
conf_str = conf_str.replace("\\\n", "\n") conf_str = conf_str.replace("\\\n", "\n")
cg.add(cg.LineComment(indent(conf_str))) cg.add(cg.LineComment(indent(conf_str)))
await coro(conf) await coro(conf)
@@ -855,7 +857,20 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
toolchain.create_factory_bin() toolchain.create_factory_bin()
toolchain.create_ota_bin() toolchain.create_ota_bin()
toolchain.create_elf_copy() toolchain.create_elf_copy()
toolchain.get_idedata() from esphome.build_helpers.idedata import IDEDATA_BEST_EFFORT_ERRORS
try:
if toolchain.get_idedata() is None:
_LOGGER.warning("No idedata was generated for this build")
except IDEDATA_BEST_EFFORT_ERRORS as err:
# The firmware already built; an idedata failure must not fail
# a successful build.
_LOGGER.warning(
"Could not generate idedata: %s (IDE, clang-tidy, and "
"memory-analysis data will be unavailable for this build)",
err,
)
_LOGGER.debug("Idedata failure detail", exc_info=True)
else: else:
from esphome.platformio import toolchain from esphome.platformio import toolchain
+45 -4
View File
@@ -3,7 +3,12 @@
import json import json
from pathlib import Path from pathlib import Path
from esphome.components.esp32 import get_esp32_variant, idf_version from esphome.components.esp32 import (
get_esp32_variant,
get_excluded_builtin_components,
get_managed_component_require_names,
idf_version,
)
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.core import CORE from esphome.core import CORE
from esphome.framework_helpers import ( from esphome.framework_helpers import (
@@ -67,6 +72,13 @@ def has_discovered_components() -> bool:
return get_available_components() is not None return get_available_components() is not None
def _cmake_quote(value: str) -> str:
"""Quote a cmake arg value for a set() line. add_cmake_arg rejects
whitespace, quotes, and '$', so only backslashes need escaping."""
escaped = value.replace("\\", "\\\\")
return f'"{escaped}"'
def get_project_cmakelists(minimal: bool = False) -> str: def get_project_cmakelists(minimal: bool = False) -> str:
"""Generate the top-level CMakeLists.txt for ESP-IDF project. """Generate the top-level CMakeLists.txt for ESP-IDF project.
@@ -109,6 +121,15 @@ def get_project_cmakelists(minimal: bool = False) -> str:
else "" else ""
) )
# CMake variables registered via cg.add_cmake_arg(). Emitted before
# include(project.cmake) so values like EXCLUDE_COMPONENTS are already
# set when project.cmake seeds the component list, and on minimal
# (discovery) writes too so excluded components never register.
cmake_args = "\n".join(
f"set({name} {_cmake_quote(value)})"
for name, value in sorted(CORE.cmake_args.items())
)
# Per-project list exposed as a CMake variable so converted PIO libs # Per-project list exposed as a CMake variable so converted PIO libs
# can reference ${ESPHOME_PROJECT_MANAGED_COMPONENTS} without baking # can reference ${ESPHOME_PROJECT_MANAGED_COMPONENTS} without baking
# project-specific names into their cached CMakeLists. # project-specific names into their cached CMakeLists.
@@ -119,8 +140,6 @@ def get_project_cmakelists(minimal: bool = False) -> str:
# runs as a separate CMake script invocation that doesn't load the # runs as a separate CMake script invocation that doesn't load the
# project's top-level CMakeLists; without this, ${ESPHOME_PROJECT_ # project's top-level CMakeLists; without this, ${ESPHOME_PROJECT_
# MANAGED_COMPONENTS} in a converted-lib REQUIRES expands to empty). # MANAGED_COMPONENTS} in a converted-lib REQUIRES expands to empty).
from esphome.components.esp32 import get_managed_component_require_names
managed_components_property = "\n".join( managed_components_property = "\n".join(
f"idf_build_set_property(ESPHOME_PROJECT_MANAGED_COMPONENTS {name} APPEND)" f"idf_build_set_property(ESPHOME_PROJECT_MANAGED_COMPONENTS {name} APPEND)"
for name in get_managed_component_require_names() for name in get_managed_component_require_names()
@@ -131,12 +150,22 @@ def get_project_cmakelists(minimal: bool = False) -> str:
# component's REQUIRES including real IDF components). Referenced by # component's REQUIRES including real IDF components). Referenced by
# src/CMakeLists and by each converted PIO lib's CMakeLists. Skipped # src/CMakeLists and by each converted PIO lib's CMakeLists. Skipped
# on minimal writes because project_description.json may be stale. # on minimal writes because project_description.json may be stale.
# Excluded components are dropped here as well: a stale
# project_description.json from a build without exclusions may still
# list them, and requiring an excluded component pulls it back into
# the build (IDF requirement expansion overrides EXCLUDE_COMPONENTS).
# Derived from the EXCLUDE_COMPONENTS cmake arg emitted above so the
# two can never disagree within one generated file.
builtin_components_property = ( builtin_components_property = (
"" ""
if minimal if minimal
else "\n".join( else "\n".join(
f"idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS {name} APPEND)" f"idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS {name} APPEND)"
for name in sorted(get_available_components() or []) for name in sorted(
set(get_available_components() or []).difference(
CORE.cmake_args.get("EXCLUDE_COMPONENTS", "").split(";")
)
)
) )
) )
@@ -163,6 +192,8 @@ set(CMAKE_NINJA_FORCE_RESPONSE_FILE 1)
set(IDF_TARGET {idf_target}) set(IDF_TARGET {idf_target})
set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src) set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
{cmake_args}
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake) include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
{cpp_standard_options} {cpp_standard_options}
@@ -264,3 +295,13 @@ def write_project(minimal: bool = False) -> None:
CORE.relative_src_path("CMakeLists.txt"), CORE.relative_src_path("CMakeLists.txt"),
get_component_cmakelists(), get_component_cmakelists(),
) )
# Snapshot the exclusion set so has_outdated_files() can trigger a
# discovery reconfigure when it changes. Excluded components never
# register in project_description.json, so re-including one (e.g. a
# config gains mqtt) requires a fresh discovery pass before the
# ESPHOME_PROJECT_BUILTIN_COMPONENTS property can list it.
write_file_if_changed(
CORE.relative_build_path("exclude_components.esphomeinternal"),
";".join(get_excluded_builtin_components()),
)
+11
View File
@@ -63,6 +63,17 @@ def get_ini_content():
# Add extra script for C++ flags # Add extra script for C++ flags
CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"]) CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"])
# Add CMake args. A user-supplied value (str or list) is deliberately
# replaced; this option was always overwritten at FINAL priority.
if CORE.cmake_args:
CORE.add_platformio_option(
"board_build.cmake_extra_args",
" ".join(
f"-D{name}={value}" for name, value in sorted(CORE.cmake_args.items())
),
replace=True,
)
content = "[platformio]\n" content = "[platformio]\n"
content += f"description = ESPHome {__version__}\n" content += f"description = ESPHome {__version__}\n"
+1
View File
@@ -0,0 +1 @@
"""Build helpers shared by the native (non-PlatformIO) toolchains."""
@@ -1,10 +1,10 @@
"""Derive idedata from an ESP-IDF native-toolchain ``compile_commands.json``. """Derive idedata from a native (non-PlatformIO) build's ``compile_commands.json``.
PlatformIO exposes a curated ``pio run -t idedata`` JSON; the native ESP-IDF PlatformIO exposes a curated ``pio run -t idedata`` JSON; the native
toolchain has no such command, but its CMake build emits toolchains have no such command, but each build produces a
``build/compile_commands.json`` (CMAKE_EXPORT_COMPILE_COMMANDS). This module ``compile_commands.json`` (CMAKE_EXPORT_COMPILE_COMMANDS for ESP-IDF, ninja's
turns that file into the same fields consumers (IDE integration, clang-tidy) compdb tool otherwise). This module turns that file into the same fields
expect: consumers (IDE integration, clang-tidy) expect:
{cc_path, cxx_path, cxx_flags, defines, includes: {build, toolchain}} {cc_path, cxx_path, cxx_flags, defines, includes: {build, toolchain}}
""" """
@@ -18,6 +18,19 @@ from pathlib import Path
import shlex import shlex
import subprocess import subprocess
from esphome.core import EsphomeError
from esphome.helpers import write_file
# Everything idedata generation may raise after a successful link; idedata
# is a bonus artifact, so consumers warn instead of failing the build
IDEDATA_BEST_EFFORT_ERRORS = (
EsphomeError,
LookupError,
OSError,
RuntimeError,
ValueError,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# C++ translation-unit suffixes used to identify ESPHome source files. # C++ translation-unit suffixes used to identify ESPHome source files.
@@ -30,12 +43,8 @@ _ESPHOME_SRC_MARKER = "/src/esphome/"
def _is_esphome_src(file: str) -> bool: def _is_esphome_src(file: str) -> bool:
"""Whether ``file`` is an ESPHome C++ translation unit. """Whether ``file`` is an ESPHome C++ translation unit; normalized to
``/`` first since Windows compile DBs use backslashes."""
``compile_commands.json`` ``file`` paths use the OS-native separator, so on
Windows they contain backslashes; normalize to ``/`` before testing the
marker, otherwise no source matches and the build-include union is empty.
"""
return _ESPHOME_SRC_MARKER in file.replace("\\", "/") and file.endswith( return _ESPHOME_SRC_MARKER in file.replace("\\", "/") and file.endswith(
_CXX_SUFFIXES _CXX_SUFFIXES
) )
@@ -106,11 +115,8 @@ def _expand_response_files(tokens: list[str], directory: Path) -> list[str]:
def _pick_entry(entries: list[dict]) -> dict: def _pick_entry(entries: list[dict]) -> dict:
"""Pick a representative ESPHome C++ translation unit. """Pick a representative ESPHome C++ TU; all share the same component
flags/defines."""
All ESPHome sources share the same component flags/defines, so any one of
them yields the cxx_path / cxx_flags / defines we need.
"""
for entry in entries: for entry in entries:
if _is_esphome_src(entry["file"]): if _is_esphome_src(entry["file"]):
return entry return entry
@@ -120,25 +126,46 @@ def _pick_entry(entries: list[dict]) -> dict:
raise ValueError("no C++ translation unit found in compile_commands.json") raise ValueError("no C++ translation unit found in compile_commands.json")
def _parse_entry(entry: dict) -> tuple[str, list[str], list[str], list[str]]: # Compiler launchers that may prefix a compile command; a closed launcher
# denylist beats enumerating compiler names, an open set.
_LAUNCHER_STEMS = frozenset({"ccache", "sccache", "distcc", "icecc", "buildcache"})
def _is_launcher(token: str) -> bool:
return Path(token).stem.lower() in _LAUNCHER_STEMS
def parse_entry(
entry: dict, launcher: str | None = None
) -> tuple[str, list[str], list[str], list[str]]:
"""Parse one compile_commands entry -> (cxx_path, defines, includes, cxx_flags).""" """Parse one compile_commands entry -> (cxx_path, defines, includes, cxx_flags)."""
directory = Path(entry["directory"]) directory = Path(entry["directory"])
tokens = _expand_response_files(_split_command(entry["command"]), directory) tokens = _expand_response_files(_split_command(entry["command"]), directory)
def _include(raw: str) -> str: def _include(raw: str) -> str:
# Include paths in compile_commands are interpreted relative to the # Resolve against the entry's ``directory`` so cached idedata works
# entry's ``directory`` (e.g. build-local ``-Iconfig``); resolve them # from any cwd; emit forward slashes to match the JSON's own entries
# so the cached idedata is usable regardless of the consumer's cwd.
# Emit forward slashes (``normpath`` yields ``\`` on Windows) so the
# paths match the absolute, already-forward-slash entries in the JSON.
raw = raw.strip() raw = raw.strip()
if raw and not Path(raw).is_absolute(): if raw and not Path(raw).is_absolute():
raw = os.path.normpath(directory / raw) raw = os.path.normpath(directory / raw)
return raw.replace("\\", "/") return raw.replace("\\", "/")
# A launcher-wrapped command ("ccache g++ ...") names the compiler second
if launcher is not None and tokens[:1] == [launcher]:
tokens = tokens[1:]
if not tokens:
# An empty command, or one that was only the launcher; fail by name
raise ValueError(f"empty compile command for {entry.get('file')}")
if _is_launcher(tokens[0]) and len(tokens) > 1 and not tokens[1].startswith("-"):
# Stale DB built with a launcher this run no longer configures; the
# real compiler is the next token
_LOGGER.warning("Stripping unconfigured launcher %s", tokens[0])
tokens = tokens[1:]
# token0 is the compiler path; the rest of the command already uses forward # token0 is the compiler path; the rest of the command already uses forward
# slashes on Windows, so normalize it too for a consistent idedata file. # slashes on Windows, so normalize it too for a consistent idedata file.
cxx_path = tokens[0].replace("\\", "/") cxx_path = tokens[0].replace("\\", "/")
# Enforced here so no caller can record ccache as the compiler
reject_launcher_compiler(cxx_path)
defines: list[str] = [] defines: list[str] = []
includes: list[str] = [] includes: list[str] = []
cxx_flags: list[str] = [] cxx_flags: list[str] = []
@@ -168,7 +195,7 @@ def _parse_entry(entry: dict) -> tuple[str, list[str], list[str], list[str]]:
return cxx_path, defines, includes, cxx_flags return cxx_path, defines, includes, cxx_flags
def _get_toolchain_includes(cxx_path: str) -> list[str]: def get_toolchain_includes(cxx_path: str) -> list[str]:
"""Query the compiler for its builtin ``#include <...>`` search dirs.""" """Query the compiler for its builtin ``#include <...>`` search dirs."""
result = subprocess.run( result = subprocess.run(
[cxx_path, "-E", "-x", "c++", "-", "-v"], [cxx_path, "-E", "-x", "c++", "-", "-v"],
@@ -219,26 +246,128 @@ def _cc_path_from_cxx(cxx_path: str) -> str:
return f"{stem}{suffix}" return f"{stem}{suffix}"
def idedata_from_build(compile_commands: Path) -> dict: def _cache_usable(cached: object) -> bool:
"""Check a cached idedata dict against the guarantees of the write path.
Caches written by older versions predate the launcher rejection and the
include-union shape; serving one would bypass both. The dict check also
keeps "in" from substring-matching a bare JSON string.
"""
if not isinstance(cached, dict) or "cc_path" not in cached:
return False
cxx_path = cached.get("cxx_path")
if not isinstance(cxx_path, str) or _is_launcher(cxx_path):
return False
includes = cached.get("includes")
return isinstance(includes, dict) and isinstance(includes.get("build"), list)
def load_or_build_idedata(
compile_commands: Path,
elf_path: Path,
cache: Path,
launcher: str | None = None,
) -> dict | None:
"""Return idedata for a compile_commands.json build, cached on mtime.
Shared by the native ESP-IDF and ESP8266 Arduino toolchains. Returns None
when the compile DB doesn't exist yet (nothing was built). ``launcher``
is the compiler-launcher path (ccache) the build was generated with, if
any; commands in the compile DB are prefixed with it.
"""
if not compile_commands.is_file():
_LOGGER.debug("No %s yet; skipping idedata generation", compile_commands)
return None
if cache.is_file() and cache.stat().st_mtime >= compile_commands.stat().st_mtime:
try:
cached = json.loads(cache.read_text(encoding="utf-8"))
except (ValueError, OSError) as err:
# A recurring cause (interrupted write, disk full) would otherwise
# look like unexplained slow builds
_LOGGER.warning("Discarding unreadable idedata cache %s: %s", cache, err)
else:
if _cache_usable(cached):
# Re-stamp so a relocated build dir cannot serve a stale ELF path
cached["prog_path"] = str(elf_path)
return cached
_LOGGER.debug("Regenerating idedata: cache %s fails validation", cache)
data = idedata_from_build(compile_commands, launcher)
data["prog_path"] = str(elf_path)
cache.parent.mkdir(parents=True, exist_ok=True)
# Atomic so a crash mid-write cannot leave a truncated cache
write_file(cache, json.dumps(data, indent=2) + "\n")
return data
def reject_launcher_compiler(cxx_path: str) -> None:
"""Reject a compile DB naming a launcher (ccache) as the compiler; it
must never be probed, cached, or consumed."""
if _is_launcher(cxx_path):
raise EsphomeError(
f"compile_commands.json names the launcher {cxx_path} as the "
"compiler; the compile database is unusable"
)
def idedata_from_build(compile_commands: Path, launcher: str | None = None) -> dict:
"""Parse compile_commands.json into the idedata fields consumers expect. """Parse compile_commands.json into the idedata fields consumers expect.
A single ESP-IDF compile entry only carries its own component's REQUIRES A single compile entry only carries the include set its own translation
include set, but consumers (clang-tidy) analyze ESPHome headers that unit was built with (per-component under ESP-IDF), but consumers
transitively pull in other components. So take cxx_path / cxx_flags / (clang-tidy) analyze ESPHome headers that transitively pull in other
defines from a representative ESPHome TU, but union the include dirs across components. So take cxx_path / cxx_flags / defines from a representative
all ESPHome TUs to get a project-wide superset (as PlatformIO's idedata ESPHome TU, but union the include dirs across all ESPHome TUs to get a
provides). project-wide superset (as PlatformIO's idedata provides).
""" """
entries = json.loads(Path(compile_commands).read_text(encoding="utf-8")) entries = json.loads(Path(compile_commands).read_text(encoding="utf-8"))
cxx_path, defines, _, cxx_flags = _parse_entry(_pick_entry(entries)) if not isinstance(entries, list) or not all(isinstance(e, dict) for e in entries):
# A TypeError here would escape IDEDATA_BEST_EFFORT_ERRORS
raise EsphomeError(f"{compile_commands} is not a compile-command list")
build_includes: dict[str, None] = {} representative = _pick_entry(entries)
cxx_path, defines, rep_includes, cxx_flags = parse_entry(representative, launcher)
# Seed with the representative's includes so it is not parsed twice
has_esphome_tu = _is_esphome_src(representative["file"])
build_includes: dict[str, None] = dict.fromkeys(
rep_includes if has_esphome_tu else ()
)
def _shape(entry: dict) -> str:
# directory + command minus TU-specific paths: same shape means the
# same include set, so tokenize once per shape. Response-file
# commands never dedupe (the .rsp contents differ per object)
command = entry["command"]
directory = entry.get("directory", "")
if "@" in command:
return f"unique:{directory}|{entry.get('output') or command}"
stripped = command.replace(entry.get("file", ""), "").replace(
entry.get("output", ""), ""
)
return f"{directory}|{stripped}"
seen_shapes = {_shape(representative)}
for entry in entries: for entry in entries:
if not _is_esphome_src(entry["file"]): if entry is representative or not _is_esphome_src(entry["file"]):
continue continue
for inc in _parse_entry(entry)[2]: has_esphome_tu = True
if (shape := _shape(entry)) in seen_shapes:
_LOGGER.debug("Include union: %s shares a command shape", entry["file"])
continue
seen_shapes.add(shape)
for inc in parse_entry(entry, launcher)[2]:
build_includes.setdefault(inc, None) build_includes.setdefault(inc, None)
if not has_esphome_tu:
# An arbitrary fallback TU breaks clang-tidy/IDE consumers, and a
# warning would be cached into permanence; call sites downgrade this
raise EsphomeError(
f"No ESPHome translation unit found in {compile_commands}; "
"refusing to cache unusable idedata"
)
return { return {
"cc_path": _cc_path_from_cxx(cxx_path), "cc_path": _cc_path_from_cxx(cxx_path),
"cxx_path": cxx_path, "cxx_path": cxx_path,
@@ -246,6 +375,6 @@ def idedata_from_build(compile_commands: Path) -> dict:
"defines": defines, "defines": defines,
"includes": { "includes": {
"build": list(build_includes), "build": list(build_includes),
"toolchain": _get_toolchain_includes(cxx_path), "toolchain": get_toolchain_includes(cxx_path),
}, },
} }
+24
View File
@@ -0,0 +1,24 @@
"""The PlatformIO-format size bar shared by the native toolchains."""
from __future__ import annotations
def format_bar(used: int, total: int) -> str:
"""Match PlatformIO's ``_format_availale_bytes`` (sic, pioupload.py) exactly."""
pct_raw = used / total if total else 0
blocks = 10
filled = min(int(round(blocks * pct_raw)), blocks)
progress = "=" * filled
return (
f"[{progress:<{blocks}}] {pct_raw: 6.1%} "
f"(used {used:d} bytes from {total:d} bytes)"
)
def print_size_line(label: str, used: int, total: int) -> None:
"""One PlatformIO-format summary line (``RAM``/``Flash``).
The label padding is part of the format: ``script/ci_memory_impact_extract.py``
matches these lines verbatim.
"""
print(f"{label + ':':<7}{format_bar(used, total)}")
+1
View File
@@ -25,6 +25,7 @@ from esphome.cpp_generator import ( # noqa: F401
add, add,
add_build_flag, add_build_flag,
add_build_unflag, add_build_unflag,
add_cmake_arg,
add_cxx_build_flag, add_cxx_build_flag,
add_define, add_define,
add_global, add_global,
+2 -1
View File
@@ -6,6 +6,7 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_METER, UNIT_METER,
) )
from esphome.types import ConfigType
CODEOWNERS = ["@MrSuicideParrot"] CODEOWNERS = ["@MrSuicideParrot"]
DEPENDENCIES = ["uart"] DEPENDENCIES = ["uart"]
@@ -35,7 +36,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config) var = await sensor.new_sensor(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await uart.register_uart_device(var, config) await uart.register_uart_device(var, config)
+2 -1
View File
@@ -6,6 +6,7 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_MILLIMETER, UNIT_MILLIMETER,
) )
from esphome.types import ConfigType
CODEOWNERS = ["@TH-Braemer"] CODEOWNERS = ["@TH-Braemer"]
DEPENDENCIES = ["uart"] DEPENDENCIES = ["uart"]
@@ -35,7 +36,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config) var = await sensor.new_sensor(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await uart.register_uart_device(var, config) await uart.register_uart_device(var, config)
+2 -1
View File
@@ -3,6 +3,7 @@ import esphome.codegen as cg
from esphome.components import stepper from esphome.components import stepper
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_DIR_PIN, CONF_ID, CONF_SLEEP_PIN, CONF_STEP_PIN from esphome.const import CONF_DIR_PIN, CONF_ID, CONF_SLEEP_PIN, CONF_STEP_PIN
from esphome.types import ConfigType
a4988_ns = cg.esphome_ns.namespace("a4988") a4988_ns = cg.esphome_ns.namespace("a4988")
A4988 = a4988_ns.class_("A4988", stepper.Stepper, cg.Component) A4988 = a4988_ns.class_("A4988", stepper.Stepper, cg.Component)
@@ -17,7 +18,7 @@ CONFIG_SCHEMA = stepper.STEPPER_SCHEMA.extend(
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await stepper.register_stepper(var, config) await stepper.register_stepper(var, config)
@@ -9,6 +9,7 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_GRAMS_PER_CUBIC_METER, UNIT_GRAMS_PER_CUBIC_METER,
) )
from esphome.types import ConfigType
absolute_humidity_ns = cg.esphome_ns.namespace("absolute_humidity") absolute_humidity_ns = cg.esphome_ns.namespace("absolute_humidity")
AbsoluteHumidityComponent = absolute_humidity_ns.class_( AbsoluteHumidityComponent = absolute_humidity_ns.class_(
@@ -43,7 +44,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config) var = await sensor.new_sensor(config)
await cg.register_component(var, config) await cg.register_component(var, config)
+8 -1
View File
@@ -4,6 +4,7 @@ from esphome.components import output
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_METHOD, CONF_MIN_POWER from esphome.const import CONF_ID, CONF_METHOD, CONF_MIN_POWER
from esphome.core import CORE from esphome.core import CORE
from esphome.types import ConfigType
CODEOWNERS = ["@glmnet"] CODEOWNERS = ["@glmnet"]
@@ -48,7 +49,13 @@ CONFIG_SCHEMA = cv.All(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
if CORE.is_esp32:
from esphome.components.esp32 import include_builtin_idf_component
# Re-enable the gptimer driver (excluded by default to save compile time)
include_builtin_idf_component("esp_driver_gptimer")
if CORE.is_esp8266: if CORE.is_esp8266:
# ac_dimmer uses setTimer1Callback which requires the waveform generator # ac_dimmer uses setTimer1Callback which requires the waveform generator
from esphome.components.esp8266.const import require_waveform from esphome.components.esp8266.const import require_waveform
+4 -1
View File
@@ -4,6 +4,9 @@ from esphome.components.light.effects import register_addressable_effect
from esphome.components.light.types import AddressableLightEffect from esphome.components.light.types import AddressableLightEffect
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_NAME, CONF_UART_ID from esphome.const import CONF_NAME, CONF_UART_ID
from esphome.core import ID
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
DEPENDENCIES = ["uart"] DEPENDENCIES = ["uart"]
@@ -21,7 +24,7 @@ CONFIG_SCHEMA = cv.Schema({})
"Adalight", "Adalight",
{cv.GenerateID(CONF_UART_ID): cv.use_id(uart.UARTComponent)}, {cv.GenerateID(CONF_UART_ID): cv.use_id(uart.UARTComponent)},
) )
async def adalight_light_effect_to_code(config, effect_id): async def adalight_light_effect_to_code(config: ConfigType, effect_id: ID) -> MockObj:
effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) effect = cg.new_Pvariable(effect_id, config[CONF_NAME])
await uart.register_uart_device(effect, config) await uart.register_uart_device(effect, config)
return effect return effect
+4 -1
View File
@@ -1,3 +1,5 @@
from typing import Any
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.esp32 import ( from esphome.components.esp32 import (
@@ -16,6 +18,7 @@ from esphome.components.esp32 import (
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266 from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266
from esphome.core import CORE from esphome.core import CORE
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
@@ -225,7 +228,7 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
} }
def validate_adc_pin(value): def validate_adc_pin(value: Any) -> ConfigType | str:
if str(value).upper() == "VCC": if str(value).upper() == "VCC":
if CORE.is_rp2: if CORE.is_rp2:
return pins.internal_gpio_input_pin_schema(29) return pins.internal_gpio_input_pin_schema(29)
+3 -3
View File
@@ -52,7 +52,7 @@ _attenuation = cv.enum(ATTENUATION_MODES, lower=True)
_sampling_mode = cv.enum(SAMPLING_MODES, lower=True) _sampling_mode = cv.enum(SAMPLING_MODES, lower=True)
def validate_config(config): def validate_config(config: ConfigType) -> ConfigType:
if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto": if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto":
raise cv.Invalid("Automatic attenuation cannot be used when raw output is set") raise cv.Invalid("Automatic attenuation cannot be used when raw output is set")
@@ -120,7 +120,7 @@ CONFIG_SCHEMA = cv.All(
CONF_ADC_CHANNEL_ID = "adc_channel_id" CONF_ADC_CHANNEL_ID = "adc_channel_id"
def _overlay_io_channels(): def _overlay_io_channels() -> str:
channel_count = CORE.data[CONF_ADC_CHANNEL_ID] channel_count = CORE.data[CONF_ADC_CHANNEL_ID]
entries = ", ".join(f"<&adc {channel_id}>" for channel_id in range(channel_count)) entries = ", ".join(f"<&adc {channel_id}>" for channel_id in range(channel_count))
return f""" return f"""
@@ -132,7 +132,7 @@ def _overlay_io_channels():
""" """
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await sensor.register_sensor(var, config) await sensor.register_sensor(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import spi from esphome.components import spi
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID from esphome.const import CONF_ID
from esphome.types import ConfigType
DEPENDENCIES = ["spi"] DEPENDENCIES = ["spi"]
MULTI_CONF = True MULTI_CONF = True
@@ -17,7 +18,7 @@ CONFIG_SCHEMA = cv.Schema(
).extend(spi.spi_device_schema(cs_pin_required=True)) ).extend(spi.spi_device_schema(cs_pin_required=True))
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await spi.register_spi_device(var, config) await spi.register_spi_device(var, config)
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import sensor, voltage_sampler from esphome.components import sensor, voltage_sampler
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID from esphome.const import CONF_CHANNEL, CONF_ID
from esphome.types import ConfigType
from .. import ADC128S102, adc128s102_ns from .. import ADC128S102, adc128s102_ns
@@ -28,7 +29,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable( var = cg.new_Pvariable(
config[CONF_ID], config[CONF_ID],
config[CONF_CHANNEL], config[CONF_CHANNEL],
@@ -11,6 +11,7 @@ from esphome.const import (
CONF_UPDATE_INTERVAL, CONF_UPDATE_INTERVAL,
CONF_WIDTH, CONF_WIDTH,
) )
from esphome.types import ConfigType
CODEOWNERS = ["@justfalter"] CODEOWNERS = ["@justfalter"]
@@ -38,7 +39,7 @@ CONFIG_SCHEMA = cv.All(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
wrapped_light = await cg.get_variable(config[CONF_ADDRESSABLE_LIGHT_ID]) wrapped_light = await cg.get_variable(config[CONF_ADDRESSABLE_LIGHT_ID])
cg.add(var.set_width(config[CONF_WIDTH])) cg.add(var.set_width(config[CONF_WIDTH]))
+4 -3
View File
@@ -36,6 +36,7 @@ from esphome.const import (
UNIT_WATT, UNIT_WATT,
UNIT_WATT_HOURS, UNIT_WATT_HOURS,
) )
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType from esphome.types import ConfigType
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
@@ -243,7 +244,7 @@ CONFIG_SCHEMA = cv.All(
) )
async def neutral_channel(config): async def neutral_channel(config: ConfigType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
current = config[CONF_CURRENT] current = config[CONF_CURRENT]
@@ -257,7 +258,7 @@ async def neutral_channel(config):
return var return var
async def power_channel(config): async def power_channel(config: ConfigType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
for sensor_type in POWER_SENSOR_TYPES: for sensor_type in POWER_SENSOR_TYPES:
@@ -280,7 +281,7 @@ async def power_channel(config):
return var return var
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
+3 -1
View File
@@ -23,6 +23,8 @@ from esphome.const import (
UNIT_VOLT_AMPS_REACTIVE, UNIT_VOLT_AMPS_REACTIVE,
UNIT_WATT, UNIT_WATT,
) )
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
CODEOWNERS = ["@angelnu"] CODEOWNERS = ["@angelnu"]
@@ -163,7 +165,7 @@ ADE7953_CONFIG_SCHEMA = cv.Schema(
).extend(cv.polling_component_schema("60s")) ).extend(cv.polling_component_schema("60s"))
async def register_ade7953(var, config): async def register_ade7953(var: MockObj, config: ConfigType) -> None:
await cg.register_component(var, config) await cg.register_component(var, config)
if irq_pin_config := config.get(CONF_IRQ_PIN): if irq_pin_config := config.get(CONF_IRQ_PIN):
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import ade7953_base, i2c from esphome.components import ade7953_base, i2c
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID from esphome.const import CONF_ID
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["ade7953_base"] AUTO_LOAD = ["ade7953_base"]
@@ -20,7 +21,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
await ade7953_base.register_ade7953(var, config) await ade7953_base.register_ade7953(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import ade7953_base, spi from esphome.components import ade7953_base, spi
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID from esphome.const import CONF_ID
from esphome.types import ConfigType
DEPENDENCIES = ["spi"] DEPENDENCIES = ["spi"]
AUTO_LOAD = ["ade7953_base"] AUTO_LOAD = ["ade7953_base"]
@@ -20,7 +21,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await spi.register_spi_device(var, config) await spi.register_spi_device(var, config)
await ade7953_base.register_ade7953(var, config) await ade7953_base.register_ade7953(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import i2c from esphome.components import i2c
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID from esphome.const import CONF_ID
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
MULTI_CONF = True MULTI_CONF = True
@@ -24,7 +25,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
@@ -11,6 +11,7 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_VOLT, UNIT_VOLT,
) )
from esphome.types import ConfigType
from .. import CONF_ADS1115_ID, ADS1115Component, ads1115_ns from .. import CONF_ADS1115_ID, ADS1115Component, ads1115_ns
@@ -86,7 +87,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await sensor.register_sensor(var, config) await sensor.register_sensor(var, config)
await cg.register_component(var, config) await cg.register_component(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import spi from esphome.components import spi
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID from esphome.const import CONF_ID
from esphome.types import ConfigType
CODEOWNERS = ["@solomondg1"] CODEOWNERS = ["@solomondg1"]
DEPENDENCIES = ["spi"] DEPENDENCIES = ["spi"]
@@ -23,7 +24,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await spi.register_spi_device(var, config) await spi.register_spi_device(var, config)
@@ -11,6 +11,7 @@ from esphome.const import (
UNIT_CELSIUS, UNIT_CELSIUS,
UNIT_VOLT, UNIT_VOLT,
) )
from esphome.types import ConfigType
from .. import ADS1118, CONF_ADS1118_ID, ads1118_ns from .. import ADS1118, CONF_ADS1118_ID, ads1118_ns
@@ -86,7 +87,7 @@ CONFIG_SCHEMA = cv.typed_schema(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config) var = await sensor.new_sensor(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_ADS1118_ID]) await cg.register_parented(var, config[CONF_ADS1118_ID])
+16 -3
View File
@@ -17,6 +17,9 @@ from esphome.const import (
UNIT_OHM, UNIT_OHM,
UNIT_PARTS_PER_BILLION, UNIT_PARTS_PER_BILLION,
) )
from esphome.core import ID
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
CONF_RESISTANCE = "resistance" CONF_RESISTANCE = "resistance"
@@ -62,7 +65,7 @@ CONFIG_SCHEMA = (
FINAL_VALIDATE_SCHEMA = i2c.final_validate_device_schema("ags10", max_frequency="15khz") FINAL_VALIDATE_SCHEMA = i2c.final_validate_device_schema("ags10", max_frequency="15khz")
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
@@ -94,7 +97,12 @@ AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value(
AGS10_NEW_I2C_ADDRESS_SCHEMA, AGS10_NEW_I2C_ADDRESS_SCHEMA,
synchronous=True, synchronous=True,
) )
async def ags10newi2caddress_to_code(config, action_id, template_arg, args): async def ags10newi2caddress_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg) var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID]) await cg.register_parented(var, config[CONF_ID])
address = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8) address = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8)
@@ -126,7 +134,12 @@ AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema(
AGS10_SET_ZERO_POINT_SCHEMA, AGS10_SET_ZERO_POINT_SCHEMA,
synchronous=True, synchronous=True,
) )
async def ags10setzeropoint_to_code(config, action_id, template_arg, args): async def ags10setzeropoint_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg) var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID]) await cg.register_parented(var, config[CONF_ID])
mode = await cg.templatable( mode = await cg.templatable(
+2 -1
View File
@@ -12,6 +12,7 @@ from esphome.const import (
UNIT_CELSIUS, UNIT_CELSIUS,
UNIT_PERCENT, UNIT_PERCENT,
) )
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
@@ -50,7 +51,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
+10 -2
View File
@@ -4,6 +4,9 @@ from esphome.components import i2c
from esphome.components.audio_dac import AudioDac from esphome.components.audio_dac import AudioDac
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_MODE from esphome.const import CONF_ID, CONF_MODE
from esphome.core import ID
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
CODEOWNERS = ["@kbx81"] CODEOWNERS = ["@kbx81"]
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
@@ -39,7 +42,12 @@ SET_AUTO_MUTE_ACTION_SCHEMA = cv.maybe_simple_value(
SET_AUTO_MUTE_ACTION_SCHEMA, SET_AUTO_MUTE_ACTION_SCHEMA,
synchronous=True, synchronous=True,
) )
async def aic3204_set_volume_to_code(config, action_id, template_arg, args): async def aic3204_set_volume_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren) var = cg.new_Pvariable(action_id, template_arg, paren)
@@ -49,7 +57,7 @@ async def aic3204_set_volume_to_code(config, action_id, template_arg, args):
return var return var
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import ble_device_base from esphome.components import ble_device_base
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID from esphome.const import CONF_ID
from esphome.types import ConfigType
AUTO_LOAD = ["ble_device_base"] AUTO_LOAD = ["ble_device_base"]
CODEOWNERS = ["@jeromelaban"] CODEOWNERS = ["@jeromelaban"]
@@ -21,6 +22,6 @@ CONFIG_SCHEMA = cv.All(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await ble_device_base.register_ble_device(var, config) await ble_device_base.register_ble_device(var, config)
@@ -20,6 +20,8 @@ from esphome.const import (
UNIT_PERCENT, UNIT_PERCENT,
UNIT_VOLT, UNIT_VOLT,
) )
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
CODEOWNERS = ["@ncareau", "@jeromelaban"] CODEOWNERS = ["@ncareau", "@jeromelaban"]
@@ -78,7 +80,7 @@ BASE_SCHEMA = (
) )
async def wave_base_to_code(var, config): async def wave_base_to_code(var: MockObj, config: ConfigType) -> None:
await cg.register_component(var, config) await cg.register_component(var, config)
await ble_client.register_ble_node(var, config) await ble_client.register_ble_node(var, config)
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import airthings_wave_base from esphome.components import airthings_wave_base
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID from esphome.const import CONF_ID
from esphome.types import ConfigType
DEPENDENCIES = airthings_wave_base.DEPENDENCIES DEPENDENCIES = airthings_wave_base.DEPENDENCIES
@@ -20,6 +21,6 @@ CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await airthings_wave_base.wave_base_to_code(var, config) await airthings_wave_base.wave_base_to_code(var, config)
@@ -83,7 +83,7 @@ CONFIG_SCHEMA = cv.All(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await airthings_wave_base.wave_base_to_code(var, config) await airthings_wave_base.wave_base_to_code(var, config)
+2 -1
View File
@@ -20,6 +20,7 @@ from esphome.const import (
UNIT_VOLT, UNIT_VOLT,
UNIT_WATT, UNIT_WATT,
) )
from esphome.types import ConfigType
alpha3_ns = cg.esphome_ns.namespace("alpha3") alpha3_ns = cg.esphome_ns.namespace("alpha3")
Alpha3 = alpha3_ns.class_("Alpha3", ble_client.BLEClientNode, cg.PollingComponent) Alpha3 = alpha3_ns.class_("Alpha3", ble_client.BLEClientNode, cg.PollingComponent)
@@ -68,7 +69,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await ble_client.register_ble_node(var, config) await ble_client.register_ble_node(var, config)
+2 -1
View File
@@ -11,6 +11,7 @@ from esphome.const import (
UNIT_CELSIUS, UNIT_CELSIUS,
UNIT_PERCENT, UNIT_PERCENT,
) )
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
@@ -40,7 +41,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -11,6 +11,7 @@ from esphome.const import (
UNIT_CELSIUS, UNIT_CELSIUS,
UNIT_PERCENT, UNIT_PERCENT,
) )
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
@@ -42,7 +43,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import ble_client, cover from esphome.components import ble_client, cover
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_PIN from esphome.const import CONF_PIN
from esphome.types import ConfigType
CODEOWNERS = ["@buxtronix"] CODEOWNERS = ["@buxtronix"]
DEPENDENCIES = ["ble_client"] DEPENDENCIES = ["ble_client"]
@@ -27,7 +28,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = await cover.new_cover(config) var = await cover.new_cover(config)
cg.add(var.set_pin(config[CONF_PIN])) cg.add(var.set_pin(config[CONF_PIN]))
cg.add(var.set_invert_position(config[CONF_INVERT_POSITION])) cg.add(var.set_invert_position(config[CONF_INVERT_POSITION]))
+2 -1
View File
@@ -11,6 +11,7 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_PERCENT, UNIT_PERCENT,
) )
from esphome.types import ConfigType
AUTO_LOAD = ["am43"] AUTO_LOAD = ["am43"]
CODEOWNERS = ["@buxtronix"] CODEOWNERS = ["@buxtronix"]
@@ -42,7 +43,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await ble_client.register_ble_node(var, config) await ble_client.register_ble_node(var, config)
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import binary_sensor, sensor from esphome.components import binary_sensor, sensor
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_SENSOR_ID, CONF_THRESHOLD from esphome.const import CONF_SENSOR_ID, CONF_THRESHOLD
from esphome.types import ConfigType
analog_threshold_ns = cg.esphome_ns.namespace("analog_threshold") analog_threshold_ns = cg.esphome_ns.namespace("analog_threshold")
@@ -32,7 +33,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = await binary_sensor.new_binary_sensor(config) var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config) await cg.register_component(var, config)
+8 -1
View File
@@ -6,6 +6,8 @@ from esphome.components.file.image import image_schema, write_image
from esphome.components.image import Image_, validate_settings from esphome.components.image import Image_, validate_settings
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_REPEAT from esphome.const import CONF_ID, CONF_REPEAT
from esphome.core import ID
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType from esphome.types import ConfigType
CODEOWNERS = ["@syndlex"] CODEOWNERS = ["@syndlex"]
@@ -79,7 +81,12 @@ SET_FRAME_SCHEMA = cv.Schema(
@automation.register_action( @automation.register_action(
"animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True "animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True
) )
async def animation_action_to_code(config, action_id, template_arg, args): async def animation_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren) var = cg.new_Pvariable(action_id, template_arg, paren)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import ble_client, climate from esphome.components import ble_client, climate
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_UNIT_OF_MEASUREMENT from esphome.const import CONF_UNIT_OF_MEASUREMENT
from esphome.types import ConfigType
UNITS = { UNITS = {
"f": "f", "f": "f",
@@ -28,7 +29,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = await climate.new_climate(config) var = await climate.new_climate(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await ble_client.register_ble_node(var, config) await ble_client.register_ble_node(var, config)
+6 -2
View File
@@ -1,6 +1,8 @@
# Based on this datasheet: # Based on this datasheet:
# https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf # https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf
from typing import Any
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import i2c, sensor from esphome.components import i2c, sensor
import esphome.config_validation as cv import esphome.config_validation as cv
@@ -11,6 +13,8 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_LUX, UNIT_LUX,
) )
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
@@ -55,7 +59,7 @@ AMBIENT_LIGHT_GAINS = {
} }
def _validate_measurement_rate(value): def _validate_measurement_rate(value: Any) -> MockObj:
value = cv.positive_time_period_milliseconds(value) value = cv.positive_time_period_milliseconds(value)
return cv.enum(MEASUREMENT_RATES, int=True)(value.total_milliseconds) return cv.enum(MEASUREMENT_RATES, int=True)(value.total_milliseconds)
@@ -85,7 +89,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config) var = await sensor.new_sensor(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import i2c from esphome.components import i2c
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID from esphome.const import CONF_ID
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
MULTI_CONF = True MULTI_CONF = True
@@ -57,7 +58,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import binary_sensor from esphome.components import binary_sensor
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_DIRECTION, DEVICE_CLASS_MOVING from esphome.const import CONF_DIRECTION, DEVICE_CLASS_MOVING
from esphome.types import ConfigType
from . import APDS9960, CONF_APDS9960_ID from . import APDS9960, CONF_APDS9960_ID
@@ -19,7 +20,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_APDS9960_ID]) hub = await cg.get_variable(config[CONF_APDS9960_ID])
var = await binary_sensor.new_binary_sensor(config) var = await binary_sensor.new_binary_sensor(config)
func = getattr(hub, f"set_{config[CONF_DIRECTION]}_direction_binary_sensor") func = getattr(hub, f"set_{config[CONF_DIRECTION]}_direction_binary_sensor")
+2 -1
View File
@@ -7,6 +7,7 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_PERCENT, UNIT_PERCENT,
) )
from esphome.types import ConfigType
from . import APDS9960, CONF_APDS9960_ID from . import APDS9960, CONF_APDS9960_ID
@@ -27,7 +28,7 @@ CONFIG_SCHEMA = sensor.sensor_schema(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_APDS9960_ID]) hub = await cg.get_variable(config[CONF_APDS9960_ID])
var = await sensor.new_sensor(config) var = await sensor.new_sensor(config)
func = getattr(hub, f"set_{config[CONF_TYPE]}_sensor") func = getattr(hub, f"set_{config[CONF_TYPE]}_sensor")
+60 -64
View File
@@ -1,11 +1,20 @@
import base64
import logging import logging
from typing import Any
from esphome import automation from esphome import automation
from esphome.automation import Condition from esphome.automation import Condition
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.logger import request_log_listener from esphome.components.logger import request_log_listener
from esphome.config_helpers import get_logger_level
# ENCRYPTION_SCHEMA and validate_encryption_key are re-exported for external
# components and downstream consumers that import them from api
from esphome.components.noise import ( # noqa: F401
ENCRYPTION_SCHEMA,
decode_encryption_key,
encryption_schema,
validate_encryption_key,
)
from esphome.config_helpers import filter_source_files_from_defines, get_logger_level
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ACTION, CONF_ACTION,
@@ -37,6 +46,10 @@ from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_pr
from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigFragmentType, ConfigType from esphome.types import ConfigFragmentType, ConfigType
# Compat alias: downstream consumers (e.g. device-builder) referenced the
# schema by its old private name before it moved to the noise component
_encryption_schema = encryption_schema
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = "api" DOMAIN = "api"
@@ -45,9 +58,15 @@ CODEOWNERS = ["@esphome/core"]
def AUTO_LOAD(config: ConfigType) -> list[str]: def AUTO_LOAD(config: ConfigType) -> list[str]:
"""Conditionally auto-load json only when capture_response is used.""" """Conditionally auto-load noise (encryption) and json (capture_response)."""
base = ["socket"] base = ["socket"]
# A falsy config is a tooling probe for the maximal set (None from
# dependency resolution, {} from the components-graph platform probe);
# a validated config always carries defaults, never empty
if not config or CONF_ENCRYPTION in config:
base = base + ["noise"]
# Check if any homeassistant.action/homeassistant.service has capture_response: true # Check if any homeassistant.action/homeassistant.service has capture_response: true
# This flag is set during config validation in _validate_response_config # This flag is set during config validation in _validate_response_config
if not config or CORE.data.get(DOMAIN, {}).get(CONF_CAPTURE_RESPONSE, False): if not config or CORE.data.get(DOMAIN, {}).get(CONF_CAPTURE_RESPONSE, False):
@@ -129,20 +148,6 @@ def _register_provisioning_source(config: ConfigType) -> ConfigType:
return config return config
def validate_encryption_key(value):
value = cv.string_strict(value)
try:
decoded = base64.b64decode(value, validate=True)
except ValueError as err:
raise cv.Invalid("Invalid key format, please check it's using base64") from err
if len(decoded) != 32:
raise cv.Invalid("Encryption key must be base64 and 32 bytes long")
# Return original data for roundtrip conversion
return value
CONF_SUPPORTS_RESPONSE = "supports_response" CONF_SUPPORTS_RESPONSE = "supports_response"
# Enum values in api::enums namespace # Enum values in api::enums namespace
@@ -217,7 +222,7 @@ def _auto_detect_supports_response(config: ConfigType) -> ConfigType:
return config return config
def _validate_supports_response(value): def _validate_supports_response(value: Any) -> str:
"""Validate supports_response after auto-detection has set the value.""" """Validate supports_response after auto-detection has set the value."""
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value) return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
@@ -249,18 +254,6 @@ ACTIONS_SCHEMA = automation.validate_automation(
), ),
) )
ENCRYPTION_SCHEMA = cv.Schema(
{
cv.Optional(CONF_KEY): cv.sensitive(validate_encryption_key),
}
)
def _encryption_schema(config):
if config is None:
config = {}
return ENCRYPTION_SCHEMA(config)
def _consume_api_sockets(config: ConfigType) -> ConfigType: def _consume_api_sockets(config: ConfigType) -> ConfigType:
"""Register socket needs for API component.""" """Register socket needs for API component."""
@@ -296,7 +289,7 @@ CONFIG_SCHEMA = cv.All(
CONF_SERVICES, group_of_exclusion=CONF_ACTIONS CONF_SERVICES, group_of_exclusion=CONF_ACTIONS
): ACTIONS_SCHEMA, ): ACTIONS_SCHEMA,
cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA, cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA,
cv.Optional(CONF_ENCRYPTION): _encryption_schema, cv.Optional(CONF_ENCRYPTION): encryption_schema,
cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All( cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All(
cv.positive_time_period_milliseconds, cv.positive_time_period_milliseconds,
cv.Range(max=cv.TimePeriod(milliseconds=65535)), cv.Range(max=cv.TimePeriod(milliseconds=65535)),
@@ -393,7 +386,7 @@ async def to_code(config: ConfigType) -> None:
if actions := config.get(CONF_ACTIONS, []): if actions := config.get(CONF_ACTIONS, []):
# Collect all triggers first, then register all at once with initializer_list # Collect all triggers first, then register all at once with initializer_list
triggers: list[cg.Pvariable] = [] triggers: list[cg.MockObj] = []
for conf in actions: for conf in actions:
func_args: list[tuple[MockObj, str]] = [] func_args: list[tuple[MockObj, str]] = []
service_template_args: list[MockObj] = [] # User service argument types service_template_args: list[MockObj] = [] # User service argument types
@@ -483,7 +476,7 @@ async def to_code(config: ConfigType) -> None:
if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None: if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None:
if key := encryption_config.get(CONF_KEY): if key := encryption_config.get(CONF_KEY):
decoded = base64.b64decode(key) decoded = decode_encryption_key(key)
cg.add(var.set_noise_psk(list(decoded))) cg.add(var.set_noise_psk(list(decoded)))
cg.add_define("USE_API_NOISE_PSK_FROM_YAML") cg.add_define("USE_API_NOISE_PSK_FROM_YAML")
else: else:
@@ -497,10 +490,6 @@ async def to_code(config: ConfigType) -> None:
# and plaintext disabled. Only a factory reset can remove it. # and plaintext disabled. Only a factory reset can remove it.
cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_PLAINTEXT")
cg.add_define("USE_API_NOISE") cg.add_define("USE_API_NOISE")
cg.add_library("esphome/noise-c", "0.1.21")
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
cg.add_build_flag("-DHAVE_INLINE_ASM=1")
else: else:
cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_PLAINTEXT")
@@ -581,7 +570,7 @@ async def homeassistant_service_to_code(
action_id: ID, action_id: ID,
template_arg: cg.TemplateArguments, template_arg: cg.TemplateArguments,
args: TemplateArgsType, args: TemplateArgsType,
): ) -> MockObj:
cg.add_define("USE_API_HOMEASSISTANT_SERVICES") cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID]) serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, False) var = cg.new_Pvariable(action_id, template_arg, serv, False)
@@ -647,7 +636,7 @@ async def homeassistant_service_to_code(
return var return var
def validate_homeassistant_event(value): def validate_homeassistant_event(value: Any) -> str:
value = cv.string(value) value = cv.string(value)
if not value.startswith("esphome."): if not value.startswith("esphome."):
raise cv.Invalid( raise cv.Invalid(
@@ -676,7 +665,12 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
HOMEASSISTANT_EVENT_ACTION_SCHEMA, HOMEASSISTANT_EVENT_ACTION_SCHEMA,
synchronous=True, synchronous=True,
) )
async def homeassistant_event_to_code(config, action_id, template_arg, args): async def homeassistant_event_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
cg.add_define("USE_API_HOMEASSISTANT_SERVICES") cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID]) serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True) var = cg.new_Pvariable(action_id, template_arg, serv, True)
@@ -724,7 +718,12 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA, HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
synchronous=True, synchronous=True,
) )
async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args): async def homeassistant_tag_scanned_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
cg.add_define("USE_API_HOMEASSISTANT_SERVICES") cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID]) serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True) var = cg.new_Pvariable(action_id, template_arg, serv, True)
@@ -740,7 +739,7 @@ CONF_SUCCESS = "success"
CONF_ERROR_MESSAGE = "error_message" CONF_ERROR_MESSAGE = "error_message"
def _validate_api_respond_data(config): def _validate_api_respond_data(config: ConfigType) -> ConfigType:
"""Set flag during validation so AUTO_LOAD can include json component.""" """Set flag during validation so AUTO_LOAD can include json component."""
if CONF_DATA in config: if CONF_DATA in config:
CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True
@@ -824,18 +823,32 @@ API_CONNECTED_CONDITION_SCHEMA = cv.Schema(
@automation.register_condition( @automation.register_condition(
"api.connected", APIConnectedCondition, API_CONNECTED_CONDITION_SCHEMA "api.connected", APIConnectedCondition, API_CONNECTED_CONDITION_SCHEMA
) )
async def api_connected_to_code(config, condition_id, template_arg, args): async def api_connected_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(condition_id, template_arg) var = cg.new_Pvariable(condition_id, template_arg)
templ = await cg.templatable(config[CONF_STATE_SUBSCRIPTION_ONLY], args, cg.bool_) templ = await cg.templatable(config[CONF_STATE_SUBSCRIPTION_ONLY], args, cg.bool_)
cg.add(var.set_state_subscription_only(templ)) cg.add(var.set_state_subscription_only(templ))
return var return var
# user_services.cpp is only needed when user defined actions exist; the
# frame helpers are fully #ifdef'd on the protocol defines set in to_code
# (both are set when encryption is configured without a key).
_define_filter = filter_source_files_from_defines(
{
"user_services.cpp": "USE_API_USER_DEFINED_ACTIONS",
"api_frame_helper_noise.cpp": "USE_API_NOISE",
"api_frame_helper_plaintext.cpp": "USE_API_PLAINTEXT",
}
)
def FILTER_SOURCE_FILES() -> list[str]: def FILTER_SOURCE_FILES() -> list[str]:
"""Filter out api_pb2_dump.cpp when proto message dumping is not enabled, files_to_filter = _define_filter()
user_services.cpp when no services are defined, and protocol-specific
implementations based on encryption configuration."""
files_to_filter: list[str] = []
# api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined # api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined
# This is a particularly large file that still needs to be opened and read # This is a particularly large file that still needs to be opened and read
@@ -846,21 +859,4 @@ def FILTER_SOURCE_FILES() -> list[str]:
if get_logger_level() != "VERY_VERBOSE": if get_logger_level() != "VERY_VERBOSE":
files_to_filter.append("api_pb2_dump.cpp") files_to_filter.append("api_pb2_dump.cpp")
# user_services.cpp is only needed when services are defined
config = CORE.config.get(DOMAIN, {})
if config and not config.get(CONF_ACTIONS) and not config[CONF_CUSTOM_SERVICES]:
files_to_filter.append("user_services.cpp")
# Filter protocol-specific implementations based on encryption configuration
encryption_config = config.get(CONF_ENCRYPTION) if config else None
# If encryption is not configured at all, we only need plaintext
if encryption_config is None:
files_to_filter.append("api_frame_helper_noise.cpp")
# If encryption is configured with a key, we only need noise
elif encryption_config.get(CONF_KEY):
files_to_filter.append("api_frame_helper_plaintext.cpp")
# If encryption is configured but no key is provided, we need both
# (this allows a plaintext client to provide a noise key)
return files_to_filter return files_to_filter
+34 -3
View File
@@ -232,6 +232,7 @@ enum SerialProxyPortType {
message SerialProxyInfo { message SerialProxyInfo {
string name = 1; // Human-readable port name string name = 1; // Human-readable port name
SerialProxyPortType port_type = 2; // Port type (RS232, RS485) SerialProxyPortType port_type = 2; // Port type (RS232, RS485)
uint32 configured_line_states = 3; // Bitmask of SerialProxyLineStateFlags this instance can drive
} }
// DeviceInfoResponse max_data_length values: // DeviceInfoResponse max_data_length values:
@@ -1002,8 +1003,12 @@ message GetTimeResponse {
option (no_delay) = true; option (no_delay) = true;
fixed32 epoch_seconds = 1; fixed32 epoch_seconds = 1;
string timezone = 2; // Deprecated in 2026.9.0: clients still send this string for older firmware,
ParsedTimezone parsed_timezone = 3; // but new firmware only reads parsed_timezone. Clients older than Home
// Assistant 2026.3.0 that send only the string leave the device on its
// codegen-configured timezone (or UTC).
string timezone = 2 [deprecated = true];
ParsedTimezone parsed_timezone = 3 [(track_presence) = true];
} }
// ==================== USER-DEFINES SERVICES ==================== // ==================== USER-DEFINES SERVICES ====================
@@ -2622,6 +2627,22 @@ message ZWaveProxyRequest {
bytes data = 2; bytes data = 2;
} }
enum ZWaveProxyStatus {
ZWAVE_PROXY_STATUS_OK = 0; // Request completed successfully
ZWAVE_PROXY_STATUS_IN_USE = 1; // Denied: another client is already subscribed
ZWAVE_PROXY_STATUS_NOT_SUPPORTED = 2; // Request type not supported
}
// Acknowledges a ZWaveProxyRequest (subscribe/unsubscribe). Sent since API 1.16.
message ZWaveProxyRequestResponse {
option (id) = 151;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_ZWAVE_PROXY";
ZWaveProxyRequestType type = 1; // Which request type this responds to
ZWaveProxyStatus status = 2; // Result status
}
// ==================== INFRARED ==================== // ==================== INFRARED ====================
// Note: Feature and capability flag enums are defined in // Note: Feature and capability flag enums are defined in
// esphome/components/infrared/infrared.h // esphome/components/infrared/infrared.h
@@ -2765,12 +2786,18 @@ message SerialProxyGetModemPinsResponse {
uint32 instance = 1; // Instance index (0-based) uint32 instance = 1; // Instance index (0-based)
uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags
SerialProxyStatus status = 3; // INVALID_ARGUMENT if the instance index is out of range (since API 1.16)
} }
enum SerialProxyRequestType { enum SerialProxyRequestType {
SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0; // Subscribe to receive data from this serial proxy instance SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0; // Subscribe to receive data from this serial proxy instance
SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1; // Unsubscribe from this serial proxy instance SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1; // Unsubscribe from this serial proxy instance
SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2; // Flush the serial port (block until all TX data is sent) SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2; // Flush the serial port (block until all TX data is sent)
// Values below are only valid in SerialProxyRequestResponse.type, identifying which
// operation is being acknowledged. Sending them in SerialProxyRequest.type is an
// error the device answers with INVALID_ARGUMENT.
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3; // Acknowledges a SerialProxyConfigureRequest
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4; // Acknowledges a SerialProxySetModemPinsRequest
} }
enum SerialProxyStatus { enum SerialProxyStatus {
@@ -2779,6 +2806,8 @@ enum SerialProxyStatus {
SERIAL_PROXY_STATUS_ERROR = 2; // Driver or hardware error SERIAL_PROXY_STATUS_ERROR = 2; // Driver or hardware error
SERIAL_PROXY_STATUS_TIMEOUT = 3; // Timed out before TX completed SERIAL_PROXY_STATUS_TIMEOUT = 3; // Timed out before TX completed
SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4; // Request type not supported by this instance SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4; // Request type not supported by this instance
SERIAL_PROXY_STATUS_PORT_IN_USE = 5; // Denied: another client holds the port
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6; // Invalid instance index or parameter value
} }
// Generic request message for simple serial proxy operations // Generic request message for simple serial proxy operations
@@ -2791,7 +2820,9 @@ message SerialProxyRequest {
SerialProxyRequestType type = 2; // Request type SerialProxyRequestType type = 2; // Request type
} }
// Response to a SerialProxyRequest (e.g. flush completion or failure) // Acknowledges a serial proxy operation; the type field identifies which
// operation is being acknowledged. Flush has been acknowledged since the
// message was introduced; all other acknowledgements are sent since API 1.16.
message SerialProxyRequestResponse { message SerialProxyRequestResponse {
option (id) = 147; option (id) = 147;
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
+107 -67
View File
@@ -412,15 +412,15 @@ void APIConnection::finalize_iterator_sync_() {
} }
void APIConnection::process_iterator_batch_(ComponentIterator &iterator) { void APIConnection::process_iterator_batch_(ComponentIterator &iterator) {
size_t initial_size = this->deferred_batch_.size(); // Budget by remaining batch capacity so a pass cannot overfill the batch;
size_t max_batch = MAX_INITIAL_PER_BATCH; // stops early on a refused send and resumes next loop pass
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) { size_t batch_size = this->deferred_batch_.size();
iterator.advance(); if (batch_size < MAX_INITIAL_BATCH_SIZE)
} iterator.try_advance(MAX_INITIAL_BATCH_SIZE - batch_size);
// If the batch is full, process it immediately // Flush immediately once enough is queued (not guaranteed every pass);
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_() // partial batches go out via the batch timer or finalize_iterator_sync_()
if (this->deferred_batch_.size() >= max_batch) { if (this->deferred_batch_.size() >= MAX_INITIAL_BATCH_SIZE) {
this->process_batch_(); this->process_batch_();
} }
} }
@@ -1204,31 +1204,28 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
if (homeassistant::global_homeassistant_time != nullptr) { if (homeassistant::global_homeassistant_time != nullptr) {
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds); homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
#if defined(USE_HOMEASSISTANT_TIMEZONE) && defined(USE_TIME_TIMEZONE) #if defined(USE_HOMEASSISTANT_TIMEZONE) && defined(USE_TIME_TIMEZONE)
if (!value.timezone.empty()) { // Apply only if the sender provided pre-parsed timezone data (Home Assistant 2026.3.0
// Check if the sender provided pre-parsed timezone data. // and newer); field presence distinguishes a genuine all-zero UTC timezone from an
// If std_offset is non-zero or DST rules are present, the parsed data was populated. // absent field. Older clients send only the deprecated timezone string, which is no
// For UTC (all zeros), string parsing produces the same result, so the fallback is equivalent. // longer decoded; for them the device keeps its codegen-configured timezone.
if (value.has_parsed_timezone) {
const auto &pt = value.parsed_timezone; const auto &pt = value.parsed_timezone;
if (pt.std_offset_seconds != 0 || pt.dst_start.type != enums::DST_RULE_TYPE_NONE) { time::ParsedTimezone tz{};
time::ParsedTimezone tz{}; tz.std_offset_seconds = pt.std_offset_seconds;
tz.std_offset_seconds = pt.std_offset_seconds; tz.dst_offset_seconds = pt.dst_offset_seconds;
tz.dst_offset_seconds = pt.dst_offset_seconds; tz.dst_start.time_seconds = pt.dst_start.time_seconds;
tz.dst_start.time_seconds = pt.dst_start.time_seconds; tz.dst_start.day = static_cast<uint16_t>(pt.dst_start.day);
tz.dst_start.day = static_cast<uint16_t>(pt.dst_start.day); tz.dst_start.type = static_cast<time::DSTRuleType>(pt.dst_start.type);
tz.dst_start.type = static_cast<time::DSTRuleType>(pt.dst_start.type); tz.dst_start.month = static_cast<uint8_t>(pt.dst_start.month);
tz.dst_start.month = static_cast<uint8_t>(pt.dst_start.month); tz.dst_start.week = static_cast<uint8_t>(pt.dst_start.week);
tz.dst_start.week = static_cast<uint8_t>(pt.dst_start.week); tz.dst_start.day_of_week = static_cast<uint8_t>(pt.dst_start.day_of_week);
tz.dst_start.day_of_week = static_cast<uint8_t>(pt.dst_start.day_of_week); tz.dst_end.time_seconds = pt.dst_end.time_seconds;
tz.dst_end.time_seconds = pt.dst_end.time_seconds; tz.dst_end.day = static_cast<uint16_t>(pt.dst_end.day);
tz.dst_end.day = static_cast<uint16_t>(pt.dst_end.day); tz.dst_end.type = static_cast<time::DSTRuleType>(pt.dst_end.type);
tz.dst_end.type = static_cast<time::DSTRuleType>(pt.dst_end.type); tz.dst_end.month = static_cast<uint8_t>(pt.dst_end.month);
tz.dst_end.month = static_cast<uint8_t>(pt.dst_end.month); tz.dst_end.week = static_cast<uint8_t>(pt.dst_end.week);
tz.dst_end.week = static_cast<uint8_t>(pt.dst_end.week); tz.dst_end.day_of_week = static_cast<uint8_t>(pt.dst_end.day_of_week);
tz.dst_end.day_of_week = static_cast<uint8_t>(pt.dst_end.day_of_week); time::set_global_tz(tz);
time::set_global_tz(tz);
} else {
homeassistant::global_homeassistant_time->set_timezone(value.timezone.c_str(), value.timezone.size());
}
} }
#endif #endif
} }
@@ -1384,7 +1381,12 @@ void APIConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) {
} }
void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) {
zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, msg.type); ZWaveProxyRequestResponse resp{};
resp.type = msg.type;
resp.status = zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, msg.type);
if (!this->send_message(resp)) {
API_LOG_MSG_DROPPED(TAG, "Z-Wave proxy response");
}
} }
#endif #endif
@@ -1553,15 +1555,50 @@ void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent
#endif #endif
#ifdef USE_SERIAL_PROXY #ifdef USE_SERIAL_PROXY
static enums::SerialProxyStatus serial_proxy_result_to_status(serial_proxy::SerialProxyResult result) {
switch (result) {
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_OK:
return enums::SERIAL_PROXY_STATUS_OK;
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_ASSUMED_SUCCESS:
return enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS;
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE:
return enums::SERIAL_PROXY_STATUS_PORT_IN_USE;
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT:
return enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_TIMEOUT:
return enums::SERIAL_PROXY_STATUS_TIMEOUT;
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED:
return enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED;
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_ERROR:
return enums::SERIAL_PROXY_STATUS_ERROR;
}
return enums::SERIAL_PROXY_STATUS_ERROR; // Unreachable; all enum values handled above
}
static void send_serial_proxy_ack(APIConnection *conn, uint32_t instance, enums::SerialProxyRequestType type,
enums::SerialProxyStatus status) {
SerialProxyRequestResponse resp{};
resp.instance = instance;
resp.type = type;
resp.status = status;
if (!conn->send_message(resp)) {
API_LOG_MSG_DROPPED(TAG, "Serial proxy response");
}
}
void APIConnection::on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) { void APIConnection::on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) {
auto &proxies = App.get_serial_proxies(); auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) { if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range (max %" PRIu32 ")", msg.instance, ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range (max %" PRIu32 ")", msg.instance,
static_cast<uint32_t>(proxies.size())); static_cast<uint32_t>(proxies.size()));
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE,
enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
return; return;
} }
proxies[msg.instance]->configure(this, msg.baudrate, msg.flow_control, static_cast<uint8_t>(msg.parity), serial_proxy::SerialProxyResult result = proxies[msg.instance]->configure(
msg.stop_bits, msg.data_size); this, msg.baudrate, msg.flow_control, static_cast<uint8_t>(msg.parity), msg.stop_bits, msg.data_size);
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE,
serial_proxy_result_to_status(result));
} }
void APIConnection::on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) { void APIConnection::on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) {
@@ -1577,20 +1614,30 @@ void APIConnection::on_serial_proxy_set_modem_pins_request(const SerialProxySetM
auto &proxies = App.get_serial_proxies(); auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) { if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance); ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS,
enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
return; return;
} }
proxies[msg.instance]->set_modem_pins(this, msg.line_states); serial_proxy::SerialProxyResult result = proxies[msg.instance]->set_modem_pins(this, msg.line_states);
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS,
serial_proxy_result_to_status(result));
} }
void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) { void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) {
auto &proxies = App.get_serial_proxies(); auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
return;
}
SerialProxyGetModemPinsResponse resp{}; SerialProxyGetModemPinsResponse resp{};
resp.instance = msg.instance; resp.instance = msg.instance;
resp.line_states = proxies[msg.instance]->get_modem_pins(); if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
// Pre-1.16 clients do not read the status field and would take this error
// for a successful "both pins deasserted" answer; let them time out as before
if (!this->client_supports_api_version(1, 16)) {
return;
}
resp.status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
} else {
resp.line_states = proxies[msg.instance]->get_modem_pins();
}
if (!this->send_message(resp)) { if (!this->send_message(resp)) {
API_LOG_MSG_DROPPED(TAG, "Serial proxy response"); API_LOG_MSG_DROPPED(TAG, "Serial proxy response");
} }
@@ -1600,40 +1647,31 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
auto &proxies = App.get_serial_proxies(); auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) { if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance); ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
send_serial_proxy_ack(this, msg.instance, msg.type, enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
return; return;
} }
auto *proxy = proxies[msg.instance];
enums::SerialProxyStatus status;
switch (msg.type) { switch (msg.type) {
case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE: case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE:
case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
proxies[msg.instance]->serial_proxy_request(this, msg.type); status = serial_proxy_result_to_status(proxy->serial_proxy_request(this, msg.type));
break; break;
case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: { case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH:
SerialProxyRequestResponse resp{}; status = serial_proxy_result_to_status(proxy->flush_port(this));
resp.instance = msg.instance; break;
resp.type = enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH; case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE:
switch (proxies[msg.instance]->flush_port()) { case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
case uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS: // Response-only discriminators; never valid in a request
resp.status = enums::SERIAL_PROXY_STATUS_OK; ESP_LOGW(TAG, "Response-only serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type));
break; status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
case uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS:
resp.status = enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS;
break;
case uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT:
resp.status = enums::SERIAL_PROXY_STATUS_TIMEOUT;
break;
case uart::UARTFlushResult::UART_FLUSH_RESULT_FAILED:
resp.status = enums::SERIAL_PROXY_STATUS_ERROR;
break;
}
if (!this->send_message(resp)) {
API_LOG_MSG_DROPPED(TAG, "Serial proxy response");
}
break; break;
}
default: default:
ESP_LOGW(TAG, "Unknown serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type)); ESP_LOGW(TAG, "Unknown serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type));
status = enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED;
break; break;
} }
send_serial_proxy_ack(this, msg.instance, msg.type, status);
} }
void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) { void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) {
@@ -1760,7 +1798,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
HelloResponse resp; HelloResponse resp;
resp.api_version_major = 1; resp.api_version_major = 1;
resp.api_version_minor = 15; resp.api_version_minor = 16;
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise // Send only the version string - the client only logs this for debugging and doesn't use it otherwise
resp.server_info = ESPHOME_VERSION_REF; resp.server_info = ESPHOME_VERSION_REF;
resp.name = StringRef(App.get_name()); resp.name = StringRef(App.get_name());
@@ -1894,6 +1932,7 @@ bool APIConnection::send_device_info_response_() {
auto &info = resp.serial_proxies[serial_proxy_index++]; auto &info = resp.serial_proxies[serial_proxy_index++];
info.name = StringRef(proxy->get_name()); info.name = StringRef(proxy->get_name());
info.port_type = proxy->get_port_type(); info.port_type = proxy->get_port_type();
info.configured_line_states = proxy->get_configured_modem_pins();
} }
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
@@ -1954,6 +1993,7 @@ bool APIConnection::send_device_capabilities_response_() {
auto &info = resp.serial_proxies[serial_proxy_index++]; auto &info = resp.serial_proxies[serial_proxy_index++];
info.name = StringRef(proxy->get_name()); info.name = StringRef(proxy->get_name());
info.port_type = proxy->get_port_type(); info.port_type = proxy->get_port_type();
info.configured_line_states = proxy->get_configured_modem_pins();
} }
#endif #endif
return this->send_message(resp); return this->send_message(resp);
@@ -2130,7 +2170,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
} }
#endif #endif
psk_t psk{}; noise::psk_t psk{};
if (msg.key_len == 0) { if (msg.key_len == 0) {
if (this->parent_->clear_noise_psk(true)) { if (this->parent_->clear_noise_psk(true)) {
resp.success = true; resp.success = true;
@@ -2139,7 +2179,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
} }
} else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) { } else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) {
ESP_LOGW(TAG, "Invalid encryption key length"); ESP_LOGW(TAG, "Invalid encryption key length");
} else if (APINoiseContext::is_all_zeros(psk)) { } else if (noise::NoiseContext::is_all_zeros(psk)) {
// Accepting the reserved provisioning PSK would report success without // Accepting the reserved provisioning PSK would report success without
// enabling encryption (or silently clear an existing key) // enabling encryption (or silently clear an existing key)
ESP_LOGW(TAG, "Rejecting all-zero encryption key"); ESP_LOGW(TAG, "Rejecting all-zero encryption key");
+4 -4
View File
@@ -53,11 +53,11 @@ void log_dropped_message(const char *tag, int line, const LogString *what);
// Keepalive timeout in milliseconds // Keepalive timeout in milliseconds
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
// Maximum number of entities to process in a single batch during initial state/info sending // Deferred batch size cap during initial state/info sync
static constexpr size_t MAX_INITIAL_PER_BATCH = 34; static constexpr size_t MAX_INITIAL_BATCH_SIZE = 34;
// Verify MAX_MESSAGES_PER_BATCH (defined in api_frame_helper.h) can hold the initial batch // Verify MAX_MESSAGES_PER_BATCH (defined in api_frame_helper.h) can hold the initial batch
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH, static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_BATCH_SIZE,
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH"); "MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_BATCH_SIZE");
#ifdef USE_BENCHMARK #ifdef USE_BENCHMARK
class APIConnection; class APIConnection;
+1 -1
View File
@@ -36,7 +36,7 @@ static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and oth
static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1; static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1;
// Maximum number of messages to batch in a single write operation // Maximum number of messages to batch in a single write operation
// Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there) // Must be >= MAX_INITIAL_BATCH_SIZE in api_connection.h (enforced by static_assert there)
static constexpr size_t MAX_MESSAGES_PER_BATCH = 34; static constexpr size_t MAX_MESSAGES_PER_BATCH = 34;
// Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars) // Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars)
+55 -155
View File
@@ -2,9 +2,9 @@
#ifdef USE_API #ifdef USE_API
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
#include "api_connection.h" // For ClientInfo struct #include "api_connection.h" // For ClientInfo struct
#include "esphome/components/noise/noise.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/entity_base.h" #include "esphome/core/entity_base.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "proto.h" #include "proto.h"
@@ -17,6 +17,14 @@
namespace esphome::api { namespace esphome::api {
using noise::noise_err_to_logstr;
// api_frame_helper.h keeps its own MAX_HANDSHAKE_SIZE because that header is
// also compiled in plaintext-only builds without the noise component; keep
// the two definitions from drifting apart.
static_assert(MAX_HANDSHAKE_SIZE == noise::MAX_HANDSHAKE_SIZE,
"api and noise component handshake size limits must match");
static const char *const TAG = "api.noise"; static const char *const TAG = "api.noise";
#ifdef USE_ESP8266 #ifdef USE_ESP8266
static constexpr char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit"; static constexpr char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit";
@@ -51,45 +59,6 @@ static constexpr size_t API_MAX_LOG_BYTES = 168;
#define LOG_PACKET_RECEIVED(buffer) ((void) 0) #define LOG_PACKET_RECEIVED(buffer) ((void) 0)
#endif #endif
/// Convert a noise error code to a readable error
const LogString *noise_err_to_logstr(int err) {
if (err == NOISE_ERROR_NO_MEMORY)
return LOG_STR("NO_MEMORY");
if (err == NOISE_ERROR_UNKNOWN_ID)
return LOG_STR("UNKNOWN_ID");
if (err == NOISE_ERROR_UNKNOWN_NAME)
return LOG_STR("UNKNOWN_NAME");
if (err == NOISE_ERROR_MAC_FAILURE)
return LOG_STR("MAC_FAILURE");
if (err == NOISE_ERROR_NOT_APPLICABLE)
return LOG_STR("NOT_APPLICABLE");
if (err == NOISE_ERROR_SYSTEM)
return LOG_STR("SYSTEM");
if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED)
return LOG_STR("REMOTE_KEY_REQUIRED");
if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED)
return LOG_STR("LOCAL_KEY_REQUIRED");
if (err == NOISE_ERROR_PSK_REQUIRED)
return LOG_STR("PSK_REQUIRED");
if (err == NOISE_ERROR_INVALID_LENGTH)
return LOG_STR("INVALID_LENGTH");
if (err == NOISE_ERROR_INVALID_PARAM)
return LOG_STR("INVALID_PARAM");
if (err == NOISE_ERROR_INVALID_STATE)
return LOG_STR("INVALID_STATE");
if (err == NOISE_ERROR_INVALID_NONCE)
return LOG_STR("INVALID_NONCE");
if (err == NOISE_ERROR_INVALID_PRIVATE_KEY)
return LOG_STR("INVALID_PRIVATE_KEY");
if (err == NOISE_ERROR_INVALID_PUBLIC_KEY)
return LOG_STR("INVALID_PUBLIC_KEY");
if (err == NOISE_ERROR_INVALID_FORMAT)
return LOG_STR("INVALID_FORMAT");
if (err == NOISE_ERROR_INVALID_SIGNATURE)
return LOG_STR("INVALID_SIGNATURE");
return LOG_STR("UNKNOWN");
}
/// Initialize the frame helper, returns OK if successful. /// Initialize the frame helper, returns OK if successful.
APIError APINoiseFrameHelper::init() { APIError APINoiseFrameHelper::init() {
APIError err = init_common_(); APIError err = init_common_();
@@ -194,9 +163,9 @@ APIError APINoiseFrameHelper::loop() {
*/ */
APIError APINoiseFrameHelper::try_read_frame_() { APIError APINoiseFrameHelper::try_read_frame_() {
// read header // read header
if (rx_header_buf_len_ < 3) { if (rx_header_buf_len_ < noise::FRAME_HEADER_SIZE) {
// no header information yet // no header information yet
uint8_t to_read = 3 - rx_header_buf_len_; uint8_t to_read = static_cast<uint8_t>(noise::FRAME_HEADER_SIZE) - rx_header_buf_len_;
ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read); ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
APIError err = handle_socket_read_result_(received); APIError err = handle_socket_read_result_(received);
if (err != APIError::OK) { if (err != APIError::OK) {
@@ -208,7 +177,7 @@ APIError APINoiseFrameHelper::try_read_frame_() {
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
if (rx_header_buf_[0] != 0x01) { if (rx_header_buf_[0] != noise::FRAME_INDICATOR) {
state_ = State::FAILED; state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
return APIError::BAD_INDICATOR; return APIError::BAD_INDICATOR;
@@ -348,15 +317,15 @@ APIError APINoiseFrameHelper::state_action_server_hello_() {
return APIError::OK; return APIError::OK;
} }
APIError APINoiseFrameHelper::state_action_handshake_() { APIError APINoiseFrameHelper::state_action_handshake_() {
int action = noise_handshakestate_get_action(this->handshake_); noise::NoiseResponderHandshake::Action action = this->handshake_.action();
if (action == NOISE_ACTION_READ_MESSAGE) { if (action == noise::NoiseResponderHandshake::Action::ACTION_READ) {
return this->state_action_handshake_read_(); return this->state_action_handshake_read_();
} else if (action == NOISE_ACTION_WRITE_MESSAGE) { } else if (action == noise::NoiseResponderHandshake::Action::ACTION_WRITE) {
return this->state_action_handshake_write_(); return this->state_action_handshake_write_();
} }
// bad state for action // bad state for action
this->state_ = State::FAILED; this->state_ = State::FAILED;
HELPER_LOG("Bad action for handshake: %d", action); HELPER_LOG("Bad action for handshake: %d", (int) action);
return APIError::HANDSHAKESTATE_BAD_STATE; return APIError::HANDSHAKESTATE_BAD_STATE;
} }
APIError APINoiseFrameHelper::state_action_handshake_read_() { APIError APINoiseFrameHelper::state_action_handshake_read_() {
@@ -368,20 +337,16 @@ APIError APINoiseFrameHelper::state_action_handshake_read_() {
if (this->rx_buf_.empty()) { if (this->rx_buf_.empty()) {
this->send_explicit_handshake_reject_(LOG_STR("Empty handshake message")); this->send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
return APIError::BAD_HANDSHAKE_ERROR_BYTE; return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} else if (this->rx_buf_[0] != 0x00) { } else if (this->rx_buf_[0] != noise::HANDSHAKE_STATUS_OK) {
HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]); HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]);
this->send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte")); this->send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
return APIError::BAD_HANDSHAKE_ERROR_BYTE; return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} }
NoiseBuffer mbuf; int err = this->handshake_.read_message(this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
noise_buffer_init(mbuf);
noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
int err = noise_handshakestate_read_message(this->handshake_, &mbuf, nullptr);
if (err != 0) { if (err != 0) {
// Special handling for MAC failure // Special handling for MAC failure
this->send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure") this->send_explicit_handshake_reject_(noise::reject_reason_for(err));
: LOG_STR("Handshake error"));
return this->handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"), return this->handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"),
APIError::HANDSHAKESTATE_READ_FAILED); APIError::HANDSHAKESTATE_READ_FAILED);
} }
@@ -390,18 +355,16 @@ APIError APINoiseFrameHelper::state_action_handshake_read_() {
} }
APIError APINoiseFrameHelper::state_action_handshake_write_() { APIError APINoiseFrameHelper::state_action_handshake_write_() {
uint8_t buffer[65]; uint8_t buffer[65];
NoiseBuffer mbuf; size_t msg_len = 0;
noise_buffer_init(mbuf);
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
int err = noise_handshakestate_write_message(this->handshake_, &mbuf, nullptr); int err = this->handshake_.write_message(buffer + 1, sizeof(buffer) - 1, msg_len);
APIError aerr = this->handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"), APIError aerr = this->handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"),
APIError::HANDSHAKESTATE_WRITE_FAILED); APIError::HANDSHAKESTATE_WRITE_FAILED);
if (aerr != APIError::OK) if (aerr != APIError::OK)
return aerr; return aerr;
buffer[0] = 0x00; // success buffer[0] = noise::HANDSHAKE_STATUS_OK;
aerr = this->write_frame_(buffer, mbuf.size + 1); aerr = this->write_frame_(buffer, msg_len + 1);
if (aerr != APIError::OK) if (aerr != APIError::OK)
return aerr; return aerr;
return this->check_handshake_finished_(); return this->check_handshake_finished_();
@@ -409,33 +372,22 @@ APIError APINoiseFrameHelper::state_action_handshake_write_() {
void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) { void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) {
// Max reject message: "Bad handshake packet len" (24) + 1 (failure byte) = 25 bytes // Max reject message: "Bad handshake packet len" (24) + 1 (failure byte) = 25 bytes
uint8_t data[32]; uint8_t data[32];
data[0] = 0x01; // failure static_assert(sizeof(data) >= noise::MAC_FAILURE_PAYLOAD_SIZE,
"reject buffer must fit the MAC failure wire contract");
#ifdef USE_STORE_LOG_STR_IN_FLASH size_t data_size = noise::format_reject_payload(data, sizeof(data), reason);
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
reason_len = std::min(reason_len, sizeof(data) - 1);
if (reason_len > 0) {
memcpy_P(data + 1, reinterpret_cast<PGM_P>(reason), reason_len);
}
#else
// Normal memory access
const char *reason_str = LOG_STR_ARG(reason);
size_t reason_len = strlen(reason_str);
reason_len = std::min(reason_len, sizeof(data) - 1);
if (reason_len > 0) {
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string
std::memcpy(data + 1, reason_str, reason_len);
}
#endif
size_t data_size = reason_len + 1;
// temporarily remove failed state // temporarily remove failed state
auto orig_state = state_; auto orig_state = state_;
state_ = State::EXPLICIT_REJECT; state_ = State::EXPLICIT_REJECT;
write_frame_(data, data_size); APIError aerr = write_frame_(data, data_size);
state_ = orig_state; if (aerr != APIError::OK) {
// Best effort; the reject reason is a diagnosis aid, not a protocol step
ESP_LOGW(TAG, "Sending handshake reject failed: %d", (int) aerr);
}
if (state_ == State::EXPLICIT_REJECT) {
// write_frame_ may have moved the state to FAILED; keep that decision
state_ = orig_state;
}
} }
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
APIError aerr = this->check_data_state_(); APIError aerr = this->check_data_state_();
@@ -492,12 +444,10 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
// Returns APIError::OK on success. // Returns APIError::OK on success.
APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type, APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type,
uint16_t &encrypted_len_out) { uint16_t &encrypted_len_out) {
// Write noise header // The noise frame header is written after encryption, when the size is known
buf_start[0] = 0x01; // indicator
// buf_start[1], buf_start[2] to be set after encryption
// Write message header (to be encrypted) // Write message header (to be encrypted)
constexpr uint8_t msg_offset = 3; constexpr uint8_t msg_offset = noise::FRAME_HEADER_SIZE;
buf_start[msg_offset] = static_cast<uint8_t>(message_type >> 8); // type high byte buf_start[msg_offset] = static_cast<uint8_t>(message_type >> 8); // type high byte
buf_start[msg_offset + 1] = static_cast<uint8_t>(message_type); // type low byte buf_start[msg_offset + 1] = static_cast<uint8_t>(message_type); // type low byte
buf_start[msg_offset + 2] = static_cast<uint8_t>(payload_size >> 8); // data_len high byte buf_start[msg_offset + 2] = static_cast<uint8_t>(payload_size >> 8); // data_len high byte
@@ -515,11 +465,10 @@ APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_
if (aerr != APIError::OK) if (aerr != APIError::OK)
return aerr; return aerr;
// Fill in the encrypted size // Fill in the frame header now that the encrypted size is known
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8); noise::write_frame_header(buf_start, static_cast<uint16_t>(mbuf.size));
buf_start[2] = static_cast<uint8_t>(mbuf.size);
encrypted_len_out = static_cast<uint16_t>(3 + mbuf.size); // indicator + size + encrypted data encrypted_len_out = static_cast<uint16_t>(noise::FRAME_HEADER_SIZE + mbuf.size);
return APIError::OK; return APIError::OK;
} }
@@ -568,21 +517,19 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
} }
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) { APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
uint8_t header[3]; uint8_t header[noise::FRAME_HEADER_SIZE];
header[0] = 0x01; // indicator noise::write_frame_header(header, len);
header[1] = (uint8_t) (len >> 8);
header[2] = (uint8_t) len;
if (len == 0) { if (len == 0) {
return this->write_raw_buf_(header, 3); return this->write_raw_buf_(header, noise::FRAME_HEADER_SIZE);
} }
struct iovec iov[2]; struct iovec iov[2];
iov[0].iov_base = header; iov[0].iov_base = header;
iov[0].iov_len = 3; iov[0].iov_len = noise::FRAME_HEADER_SIZE;
iov[1].iov_base = const_cast<uint8_t *>(data); iov[1].iov_base = const_cast<uint8_t *>(data);
iov[1].iov_len = len; iov[1].iov_len = len;
return this->write_raw_iov_(iov, 2, 3 + len); return this->write_raw_iov_(iov, 2, noise::FRAME_HEADER_SIZE + len);
} }
/** Initiate the data structures for the handshake. /** Initiate the data structures for the handshake.
@@ -590,45 +537,12 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
* @return 0 on success, -1 on error (check errno) * @return 0 on success, -1 on error (check errno)
*/ */
APIError APINoiseFrameHelper::init_handshake_() { APIError APINoiseFrameHelper::init_handshake_() {
int err; int err = this->handshake_.init(this->ctx_.get_psk(), prologue_.data(), prologue_.size());
// Noise_NNpsk0_25519_ChaChaPoly_SHA256, built on the stack: APIError aerr = handle_noise_error_(err, LOG_STR("noise_handshake_init"), APIError::HANDSHAKESTATE_SETUP_FAILED);
// noise_handshakestate_new_by_id copies it, so a member would waste
// 104 bytes per connection, and a static const would sit in RAM on
// ESP8266 (.rodata is DRAM there).
const NoiseProtocolId nid = {
.prefix_id = NOISE_PREFIX_STANDARD,
.pattern_id = NOISE_PATTERN_NN,
.modifier_ids = {NOISE_MODIFIER_PSK0},
.dh_id = NOISE_DH_CURVE25519,
.cipher_id = NOISE_CIPHER_CHACHAPOLY,
.hash_id = NOISE_HASH_SHA256,
.hybrid_id = NOISE_DH_NONE,
};
err = noise_handshakestate_new_by_id(&handshake_, &nid, NOISE_ROLE_RESPONDER);
APIError aerr =
handle_noise_error_(err, LOG_STR("noise_handshakestate_new_by_id"), APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK) if (aerr != APIError::OK)
return aerr; return aerr;
// init copies the prologue into the handshakestate, so we can get rid of it now
const auto &psk = this->ctx_.get_psk();
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"),
APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size());
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_prologue"), APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
// set_prologue copies it into handshakestate, so we can get rid of it now
prologue_.release(); prologue_.release();
err = noise_handshakestate_start(handshake_);
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
return APIError::OK; return APIError::OK;
} }
@@ -637,15 +551,17 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
assert(state_ == State::HANDSHAKE); assert(state_ == State::HANDSHAKE);
#endif #endif
int action = noise_handshakestate_get_action(handshake_); noise::NoiseResponderHandshake::Action action = this->handshake_.action();
if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE) if (action == noise::NoiseResponderHandshake::Action::ACTION_READ ||
action == noise::NoiseResponderHandshake::Action::ACTION_WRITE)
return APIError::OK; return APIError::OK;
if (action != NOISE_ACTION_SPLIT) { if (action != noise::NoiseResponderHandshake::Action::ACTION_SPLIT) {
state_ = State::FAILED; state_ = State::FAILED;
HELPER_LOG("Bad action for handshake: %d", action); HELPER_LOG("Bad action for handshake: %d", (int) action);
return APIError::HANDSHAKESTATE_BAD_STATE; return APIError::HANDSHAKESTATE_BAD_STATE;
} }
int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_); // split() also frees the handshake state
int err = this->handshake_.split(send_cipher_, recv_cipher_);
APIError aerr = APIError aerr =
handle_noise_error_(err, LOG_STR("noise_handshakestate_split"), APIError::HANDSHAKESTATE_SPLIT_FAILED); handle_noise_error_(err, LOG_STR("noise_handshakestate_split"), APIError::HANDSHAKESTATE_SPLIT_FAILED);
if (aerr != APIError::OK) if (aerr != APIError::OK)
@@ -654,17 +570,11 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
this->frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_); this->frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
HELPER_LOG("Handshake complete!"); HELPER_LOG("Handshake complete!");
noise_handshakestate_free(handshake_);
handshake_ = nullptr;
state_ = State::DATA; state_ = State::DATA;
return APIError::OK; return APIError::OK;
} }
APINoiseFrameHelper::~APINoiseFrameHelper() { APINoiseFrameHelper::~APINoiseFrameHelper() {
if (handshake_ != nullptr) {
noise_handshakestate_free(handshake_);
handshake_ = nullptr;
}
if (send_cipher_ != nullptr) { if (send_cipher_ != nullptr) {
noise_cipherstate_free(send_cipher_); noise_cipherstate_free(send_cipher_);
send_cipher_ = nullptr; send_cipher_ = nullptr;
@@ -675,16 +585,6 @@ APINoiseFrameHelper::~APINoiseFrameHelper() {
} }
} }
extern "C" {
// declare how noise generates random bytes (here with a good HWRNG based on the RF system)
void noise_rand_bytes(void *output, size_t len) {
if (!esphome::random_bytes(reinterpret_cast<uint8_t *>(output), len)) {
ESP_LOGE(TAG, "Acquiring random bytes failed; rebooting");
arch_restart();
}
}
}
} // namespace esphome::api } // namespace esphome::api
#endif // USE_API_NOISE #endif // USE_API_NOISE
#endif // USE_API #endif // USE_API
@@ -3,7 +3,7 @@
#ifdef USE_API #ifdef USE_API
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
#include "noise/protocol.h" #include "noise/protocol.h"
#include "api_noise_context.h" #include "esphome/components/noise/noise_handshake.h"
namespace esphome::api { namespace esphome::api {
@@ -14,9 +14,9 @@ class APINoiseFrameHelper final : public APIFrameHelper {
// Pos 1-2: encrypted payload size (16-bit big-endian) // Pos 1-2: encrypted payload size (16-bit big-endian)
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit) // Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
// Pos 7+: actual payload data // Pos 7+: actual payload data
static constexpr uint8_t HEADER_PADDING = 1 + 2 + 2 + 2; // indicator + size + type + data_len static constexpr uint8_t HEADER_PADDING = noise::FRAME_HEADER_SIZE + 2 + 2; // frame header + type + data_len
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx) APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, noise::NoiseContext &ctx)
: APIFrameHelper(std::move(socket)), ctx_(ctx) { : APIFrameHelper(std::move(socket)), ctx_(ctx) {
frame_header_padding_ = HEADER_PADDING; frame_header_padding_ = HEADER_PADDING;
} }
@@ -52,13 +52,13 @@ class APINoiseFrameHelper final : public APIFrameHelper {
APIError handle_handshake_frame_error_(APIError aerr); APIError handle_handshake_frame_error_(APIError aerr);
APIError handle_noise_error_(int err, const LogString *func_name, APIError api_err); APIError handle_noise_error_(int err, const LogString *func_name, APIError api_err);
// Pointers first (4 bytes each) // Pointers first (4 bytes each; the handshake wrapper holds one pointer)
NoiseHandshakeState *handshake_{nullptr}; noise::NoiseResponderHandshake handshake_;
NoiseCipherState *send_cipher_{nullptr}; NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr}; NoiseCipherState *recv_cipher_{nullptr};
// Reference to noise context (4 bytes on 32-bit) // Reference to noise context (4 bytes on 32-bit)
APINoiseContext &ctx_; noise::NoiseContext &ctx_;
// Buffer for noise handshake prologue (released after handshake) // Buffer for noise handshake prologue (released after handshake)
APIBuffer prologue_; APIBuffer prologue_;
@@ -67,7 +67,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
// Fixed-size header buffer for noise protocol: // Fixed-size header buffer for noise protocol:
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint) // 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
// Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase // Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase
uint8_t rx_header_buf_[3]; uint8_t rx_header_buf_[noise::FRAME_HEADER_SIZE];
uint8_t rx_header_buf_len_ = 0; uint8_t rx_header_buf_len_ = 0;
// 4 bytes total, no padding // 4 bytes total, no padding
}; };
@@ -1,37 +0,0 @@
#pragma once
#include <array>
#include <cstdint>
#include "esphome/core/defines.h"
namespace esphome::api {
#ifdef USE_API_NOISE
using psk_t = std::array<uint8_t, 32>;
class APINoiseContext {
public:
// The all-zeros PSK is reserved: it marks the device as unprovisioned and
// doubles as the well-known provisioning PSK that unprovisioned devices
// accept for Noise handshakes (passive-sniffing protection only, no
// authentication). It is never a valid real key.
static bool is_all_zeros(const psk_t &psk) {
uint8_t acc = 0;
for (uint8_t b : psk) {
acc |= b;
}
return acc == 0;
}
void set_psk(psk_t psk) {
this->psk_ = psk;
this->has_psk_ = !is_all_zeros(psk);
}
const psk_t &get_psk() const { return this->psk_; }
bool has_psk() const { return this->has_psk_; }
protected:
psk_t psk_{};
bool has_psk_{false};
};
#endif // USE_API_NOISE
} // namespace esphome::api
+6
View File
@@ -116,4 +116,10 @@ extend google.protobuf.FieldOptions {
// the per-byte loop when the upper bits are non-zero (the common case // the per-byte loop when the upper bits are non-zero (the common case
// for real MAC addresses, since OUIs occupy the top 24 bits). // for real MAC addresses, since OUIs occupy the top 24 bits).
optional bool mac_address = 50019 [default=false]; optional bool mac_address = 50019 [default=false];
// track_presence: Track whether this message-typed field was present on the wire.
// Generates a `bool has_<field>{false};` member on the decoding side that is set
// to true when the field arrives, so an all-default submessage can be told apart
// from an absent one (e.g. a UTC ParsedTimezone, which is all zeros).
optional bool track_presence = 50020 [default=false];
} }
+17 -4
View File
@@ -102,12 +102,14 @@ uint8_t *SerialProxyInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PAR
uint8_t *__restrict__ pos = buffer.get_pos(); uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->name); ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->name);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->port_type)); ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->port_type));
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->configured_line_states);
return pos; return pos;
} }
uint32_t SerialProxyInfo::calculate_size() const { uint32_t SerialProxyInfo::calculate_size() const {
uint32_t size = 0; uint32_t size = 0;
size += ProtoSize::calc_length(1, this->name.size()); size += ProtoSize::calc_length(1, this->name.size());
size += this->port_type ? 2 : 0; size += this->port_type ? 2 : 0;
size += ProtoSize::calc_uint32(1, this->configured_line_states);
return size; return size;
} }
#endif #endif
@@ -1249,12 +1251,9 @@ bool ParsedTimezone::decode_length(uint32_t field_id, ProtoLengthDelimited value
} }
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 2: {
this->timezone = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
break;
}
case 3: case 3:
value.decode_to_message(this->parsed_timezone); value.decode_to_message(this->parsed_timezone);
this->has_parsed_timezone = true;
break; break;
default: default:
return false; return false;
@@ -3945,6 +3944,18 @@ uint32_t ZWaveProxyRequest::calculate_size() const {
size += ProtoSize::calc_length(1, this->data_len); size += ProtoSize::calc_length(1, this->data_len);
return size; return size;
} }
uint8_t *ZWaveProxyRequestResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->type));
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->status));
return pos;
}
uint32_t ZWaveProxyRequestResponse::calculate_size() const {
uint32_t size = 0;
size += this->type ? 2 : 0;
size += this->status ? 2 : 0;
return size;
}
#endif #endif
#ifdef USE_INFRARED #ifdef USE_INFRARED
uint8_t *ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -4187,12 +4198,14 @@ uint8_t *SerialProxyGetModemPinsResponse::encode(ProtoWriteBuffer &buffer PROTO_
uint8_t *__restrict__ pos = buffer.get_pos(); uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->instance); ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->instance);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->line_states); ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->line_states);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, static_cast<uint32_t>(this->status));
return pos; return pos;
} }
uint32_t SerialProxyGetModemPinsResponse::calculate_size() const { uint32_t SerialProxyGetModemPinsResponse::calculate_size() const {
uint32_t size = 0; uint32_t size = 0;
size += ProtoSize::calc_uint32(1, this->instance); size += ProtoSize::calc_uint32(1, this->instance);
size += ProtoSize::calc_uint32(1, this->line_states); size += ProtoSize::calc_uint32(1, this->line_states);
size += this->status ? 2 : 0;
return size; return size;
} }
bool SerialProxyRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { bool SerialProxyRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
+31 -3
View File
@@ -334,6 +334,11 @@ enum ZWaveProxyRequestType : uint32_t {
ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1, ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1,
ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE = 2, ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE = 2,
}; };
enum ZWaveProxyStatus : uint32_t {
ZWAVE_PROXY_STATUS_OK = 0,
ZWAVE_PROXY_STATUS_IN_USE = 1,
ZWAVE_PROXY_STATUS_NOT_SUPPORTED = 2,
};
#endif #endif
#ifdef USE_SERIAL_PROXY #ifdef USE_SERIAL_PROXY
enum SerialProxyParity : uint32_t { enum SerialProxyParity : uint32_t {
@@ -345,6 +350,8 @@ enum SerialProxyRequestType : uint32_t {
SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0, SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0,
SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1, SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1,
SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2, SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2,
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3,
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4,
}; };
enum SerialProxyStatus : uint32_t { enum SerialProxyStatus : uint32_t {
SERIAL_PROXY_STATUS_OK = 0, SERIAL_PROXY_STATUS_OK = 0,
@@ -352,6 +359,8 @@ enum SerialProxyStatus : uint32_t {
SERIAL_PROXY_STATUS_ERROR = 2, SERIAL_PROXY_STATUS_ERROR = 2,
SERIAL_PROXY_STATUS_TIMEOUT = 3, SERIAL_PROXY_STATUS_TIMEOUT = 3,
SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4, SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4,
SERIAL_PROXY_STATUS_PORT_IN_USE = 5,
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6,
}; };
#endif #endif
@@ -523,6 +532,7 @@ class SerialProxyInfo final : public ProtoMessage {
public: public:
StringRef name{}; StringRef name{};
enums::SerialProxyPortType port_type{}; enums::SerialProxyPortType port_type{};
uint32_t configured_line_states{0};
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const; uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1283,13 +1293,13 @@ class ParsedTimezone final : public ProtoDecodableMessage {
class GetTimeResponse final : public ProtoDecodableMessage { class GetTimeResponse final : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 37; static constexpr uint8_t MESSAGE_TYPE = 37;
static constexpr uint8_t ESTIMATED_SIZE = 31; static constexpr uint8_t ESTIMATED_SIZE = 22;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("get_time_response"); } const LogString *message_name() const override { return LOG_STR("get_time_response"); }
#endif #endif
uint32_t epoch_seconds{0}; uint32_t epoch_seconds{0};
StringRef timezone{};
ParsedTimezone parsed_timezone{}; ParsedTimezone parsed_timezone{};
bool has_parsed_timezone{false};
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override; const char *dump_to(DumpBuffer &out) const override;
#endif #endif
@@ -3130,6 +3140,23 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
}; };
class ZWaveProxyRequestResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 151;
static constexpr uint8_t ESTIMATED_SIZE = 4;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("z_wave_proxy_request_response"); }
#endif
enums::ZWaveProxyRequestType type{};
enums::ZWaveProxyStatus status{};
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#endif #endif
#ifdef USE_INFRARED #ifdef USE_INFRARED
class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage { class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage {
@@ -3314,12 +3341,13 @@ class SerialProxyGetModemPinsRequest final : public ProtoDecodableMessage {
class SerialProxyGetModemPinsResponse final : public ProtoMessage { class SerialProxyGetModemPinsResponse final : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 143; static constexpr uint8_t MESSAGE_TYPE = 143;
static constexpr uint8_t ESTIMATED_SIZE = 8; static constexpr uint8_t ESTIMATED_SIZE = 10;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("serial_proxy_get_modem_pins_response"); } const LogString *message_name() const override { return LOG_STR("serial_proxy_get_modem_pins_response"); }
#endif #endif
uint32_t instance{0}; uint32_t instance{0};
uint32_t line_states{0}; uint32_t line_states{0};
enums::SerialProxyStatus status{};
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const; uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
+29 -1
View File
@@ -816,6 +816,18 @@ template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums:
return ESPHOME_PSTR("UNKNOWN"); return ESPHOME_PSTR("UNKNOWN");
} }
} }
template<> const char *proto_enum_to_string<enums::ZWaveProxyStatus>(enums::ZWaveProxyStatus value) {
switch (value) {
case enums::ZWAVE_PROXY_STATUS_OK:
return ESPHOME_PSTR("ZWAVE_PROXY_STATUS_OK");
case enums::ZWAVE_PROXY_STATUS_IN_USE:
return ESPHOME_PSTR("ZWAVE_PROXY_STATUS_IN_USE");
case enums::ZWAVE_PROXY_STATUS_NOT_SUPPORTED:
return ESPHOME_PSTR("ZWAVE_PROXY_STATUS_NOT_SUPPORTED");
default:
return ESPHOME_PSTR("UNKNOWN");
}
}
#endif #endif
#ifdef USE_SERIAL_PROXY #ifdef USE_SERIAL_PROXY
template<> const char *proto_enum_to_string<enums::SerialProxyParity>(enums::SerialProxyParity value) { template<> const char *proto_enum_to_string<enums::SerialProxyParity>(enums::SerialProxyParity value) {
@@ -838,6 +850,10 @@ template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE"); return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE");
case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH:
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_FLUSH"); return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_FLUSH");
case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE:
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_CONFIGURE");
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS");
default: default:
return ESPHOME_PSTR("UNKNOWN"); return ESPHOME_PSTR("UNKNOWN");
} }
@@ -854,6 +870,10 @@ template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::Ser
return ESPHOME_PSTR("SERIAL_PROXY_STATUS_TIMEOUT"); return ESPHOME_PSTR("SERIAL_PROXY_STATUS_TIMEOUT");
case enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED: case enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED:
return ESPHOME_PSTR("SERIAL_PROXY_STATUS_NOT_SUPPORTED"); return ESPHOME_PSTR("SERIAL_PROXY_STATUS_NOT_SUPPORTED");
case enums::SERIAL_PROXY_STATUS_PORT_IN_USE:
return ESPHOME_PSTR("SERIAL_PROXY_STATUS_PORT_IN_USE");
case enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT:
return ESPHOME_PSTR("SERIAL_PROXY_STATUS_INVALID_ARGUMENT");
default: default:
return ESPHOME_PSTR("UNKNOWN"); return ESPHOME_PSTR("UNKNOWN");
} }
@@ -914,6 +934,7 @@ const char *SerialProxyInfo::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyInfo")); MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyInfo"));
dump_field(out, ESPHOME_PSTR("name"), this->name); dump_field(out, ESPHOME_PSTR("name"), this->name);
dump_field(out, ESPHOME_PSTR("port_type"), static_cast<enums::SerialProxyPortType>(this->port_type)); dump_field(out, ESPHOME_PSTR("port_type"), static_cast<enums::SerialProxyPortType>(this->port_type));
dump_field(out, ESPHOME_PSTR("configured_line_states"), this->configured_line_states);
return out.c_str(); return out.c_str();
} }
#endif #endif
@@ -1468,7 +1489,7 @@ const char *ParsedTimezone::dump_to(DumpBuffer &out) const {
const char *GetTimeResponse::dump_to(DumpBuffer &out) const { const char *GetTimeResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("GetTimeResponse")); MessageDumpHelper helper(out, ESPHOME_PSTR("GetTimeResponse"));
dump_field(out, ESPHOME_PSTR("epoch_seconds"), this->epoch_seconds); dump_field(out, ESPHOME_PSTR("epoch_seconds"), this->epoch_seconds);
dump_field(out, ESPHOME_PSTR("timezone"), this->timezone); dump_field(out, ESPHOME_PSTR("has_parsed_timezone"), this->has_parsed_timezone);
out.append(2, ' ').append_p(ESPHOME_PSTR("parsed_timezone")).append(": "); out.append(2, ' ').append_p(ESPHOME_PSTR("parsed_timezone")).append(": ");
this->parsed_timezone.dump_to(out); this->parsed_timezone.dump_to(out);
out.append("\n"); out.append("\n");
@@ -2644,6 +2665,12 @@ const char *ZWaveProxyRequest::dump_to(DumpBuffer &out) const {
dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len);
return out.c_str(); return out.c_str();
} }
const char *ZWaveProxyRequestResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("ZWaveProxyRequestResponse"));
dump_field(out, ESPHOME_PSTR("type"), static_cast<enums::ZWaveProxyRequestType>(this->type));
dump_field(out, ESPHOME_PSTR("status"), static_cast<enums::ZWaveProxyStatus>(this->status));
return out.c_str();
}
#endif #endif
#ifdef USE_INFRARED #ifdef USE_INFRARED
const char *ListEntitiesInfraredResponse::dump_to(DumpBuffer &out) const { const char *ListEntitiesInfraredResponse::dump_to(DumpBuffer &out) const {
@@ -2753,6 +2780,7 @@ const char *SerialProxyGetModemPinsResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetModemPinsResponse")); MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetModemPinsResponse"));
dump_field(out, ESPHOME_PSTR("instance"), this->instance); dump_field(out, ESPHOME_PSTR("instance"), this->instance);
dump_field(out, ESPHOME_PSTR("line_states"), this->line_states); dump_field(out, ESPHOME_PSTR("line_states"), this->line_states);
dump_field(out, ESPHOME_PSTR("status"), static_cast<enums::SerialProxyStatus>(this->status));
return out.c_str(); return out.c_str();
} }
const char *SerialProxyRequest::dump_to(DumpBuffer &out) const { const char *SerialProxyRequest::dump_to(DumpBuffer &out) const {
+1 -11
View File
@@ -423,12 +423,6 @@ void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_
API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel) API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
#endif #endif
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
void APIServer::set_port(uint16_t port) { this->port_ = port; }
void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) { void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) {
bool has_subscriber = false; bool has_subscriber = false;
@@ -553,10 +547,6 @@ const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_sta
} }
#endif #endif
uint16_t APIServer::get_port() const { return this->port_; }
void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg,
const LogString *fail_log_msg, bool make_active) { const LogString *fail_log_msg, bool make_active) {
@@ -598,7 +588,7 @@ bool APIServer::load_and_apply_noise_psk_() {
return true; return true;
} }
bool APIServer::save_noise_psk(psk_t psk, bool make_active) { bool APIServer::save_noise_psk(noise::psk_t psk, bool make_active) {
#ifdef USE_API_NOISE_PSK_FROM_YAML #ifdef USE_API_NOISE_PSK_FROM_YAML
// When PSK is set from YAML, this function should never be called // When PSK is set from YAML, this function should never be called
// but if it is, reject the change // but if it is, reject the change
+14 -11
View File
@@ -5,7 +5,10 @@
#include "api_buffer.h" #include "api_buffer.h"
// Must precede clients_ so APIConnection is complete for default_delete (libc++). // Must precede clients_ so APIConnection is complete for default_delete (libc++).
#include "api_connection.h" #include "api_connection.h"
#include "api_noise_context.h" #ifdef USE_API_NOISE
// Only present in the build when the noise component is loaded
#include "esphome/components/noise/noise.h"
#endif
#include "api_pb2.h" #include "api_pb2.h"
#include "api_pb2_service.h" #include "api_pb2_service.h"
#include "esphome/components/socket/socket.h" #include "esphome/components/socket/socket.h"
@@ -37,7 +40,7 @@ class UserServiceDescriptor;
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
struct SavedNoisePsk { struct SavedNoisePsk {
psk_t psk; noise::psk_t psk;
} PACKED; // NOLINT } PACKED; // NOLINT
#endif #endif
@@ -51,8 +54,8 @@ class APIServer final : public Component,
public: public:
APIServer(); APIServer();
void setup() override; void setup() override;
uint16_t get_port() const; uint16_t get_port() const { return this->port_; }
float get_setup_priority() const override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void loop() override; void loop() override;
void dump_config() override; void dump_config() override;
void on_shutdown() override; void on_shutdown() override;
@@ -63,9 +66,9 @@ class APIServer final : public Component,
#ifdef USE_CAMERA #ifdef USE_CAMERA
void on_camera_image(const std::shared_ptr<camera::CameraImage> &image) override; void on_camera_image(const std::shared_ptr<camera::CameraImage> &image) override;
#endif #endif
void set_port(uint16_t port); void set_port(uint16_t port) { this->port_ = port; }
void set_reboot_timeout(uint32_t reboot_timeout); void set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
void set_batch_delay(uint16_t batch_delay); void set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
uint16_t get_batch_delay() const { return batch_delay_; } uint16_t get_batch_delay() const { return batch_delay_; }
void set_listen_backlog(uint8_t listen_backlog) { this->listen_backlog_ = listen_backlog; } void set_listen_backlog(uint8_t listen_backlog) { this->listen_backlog_ = listen_backlog; }
@@ -73,10 +76,10 @@ class APIServer final : public Component,
APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; } APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool save_noise_psk(psk_t psk, bool make_active = true); bool save_noise_psk(noise::psk_t psk, bool make_active = true);
bool clear_noise_psk(bool make_active = true); bool clear_noise_psk(bool make_active = true);
void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); } void set_noise_psk(noise::psk_t psk) { this->noise_ctx_.set_psk(psk); }
APINoiseContext &get_noise_ctx() { return this->noise_ctx_; } noise::NoiseContext &get_noise_ctx() { return this->noise_ctx_; }
#endif // USE_API_NOISE #endif // USE_API_NOISE
void handle_disconnect(APIConnection *conn); void handle_disconnect(APIConnection *conn);
@@ -354,7 +357,7 @@ class APIServer final : public Component,
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
APINoiseContext noise_ctx_; noise::NoiseContext noise_ctx_;
ESPPreferenceObject noise_pref_; ESPPreferenceObject noise_pref_;
#endif // USE_API_NOISE #endif // USE_API_NOISE
}; };
+9 -1
View File
@@ -95,9 +95,17 @@ bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(
ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {}
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_USER_DEFINED_ACTIONS
// Yield after every Nth service; bounds direct (non-batched) writes per loop pass
static constexpr uint8_t SERVICE_YIELD_INTERVAL = 3;
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
auto resp = service->encode_list_service_response(); auto resp = service->encode_list_service_response();
return this->client_->send_message(resp); if (!this->client_->send_message(resp))
return false;
// at_ is this service's index
if ((this->at_ + 1) % SERVICE_YIELD_INTERVAL == 0)
this->yield_after_step_();
return true;
} }
#endif #endif
+3 -2
View File
@@ -7,6 +7,7 @@ from esphome.const import (
DEVICE_CLASS_AQI, DEVICE_CLASS_AQI,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
) )
from esphome.types import ConfigType
from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, CONF_EXTENDED_RANGE, aqi_ns from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, CONF_EXTENDED_RANGE, aqi_ns
@@ -16,7 +17,7 @@ DEPENDENCIES = ["sensor"]
AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component) AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component)
def _validate_extended_range(config): def _validate_extended_range(config: ConfigType) -> ConfigType:
if CONF_EXTENDED_RANGE in config and config[CONF_CALCULATION_TYPE] == "CAQI": if CONF_EXTENDED_RANGE in config and config[CONF_CALCULATION_TYPE] == "CAQI":
raise cv.Invalid( raise cv.Invalid(
f"'{CONF_EXTENDED_RANGE}' is not supported with 'calculation_type: CAQI'. " f"'{CONF_EXTENDED_RANGE}' is not supported with 'calculation_type: CAQI'. "
@@ -48,7 +49,7 @@ CONFIG_SCHEMA = cv.All(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config) var = await sensor.new_sensor(config)
await cg.register_component(var, config) await cg.register_component(var, config)
+3 -1
View File
@@ -14,6 +14,8 @@ from esphome.const import (
CONF_TUNE_ANTENNA, CONF_TUNE_ANTENNA,
CONF_WATCHDOG_THRESHOLD, CONF_WATCHDOG_THRESHOLD,
) )
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
MULTI_CONF = True MULTI_CONF = True
@@ -42,7 +44,7 @@ AS3935_SCHEMA = cv.Schema(
) )
async def setup_as3935(var, config): async def setup_as3935(var: MockObj, config: ConfigType) -> None:
await cg.register_component(var, config) await cg.register_component(var, config)
irq_pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN]) irq_pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN])
+2 -1
View File
@@ -1,6 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import binary_sensor from esphome.components import binary_sensor
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.types import ConfigType
from . import AS3935, CONF_AS3935_ID from . import AS3935, CONF_AS3935_ID
@@ -13,7 +14,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_AS3935_ID]) hub = await cg.get_variable(config[CONF_AS3935_ID])
var = await binary_sensor.new_binary_sensor(config) var = await binary_sensor.new_binary_sensor(config)
cg.add(hub.set_thunder_alert_binary_sensor(var)) cg.add(hub.set_thunder_alert_binary_sensor(var))
+2 -1
View File
@@ -9,6 +9,7 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_KILOMETER, UNIT_KILOMETER,
) )
from esphome.types import ConfigType
from . import AS3935, CONF_AS3935_ID from . import AS3935, CONF_AS3935_ID
@@ -31,7 +32,7 @@ CONFIG_SCHEMA = cv.Schema(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_AS3935_ID]) hub = await cg.get_variable(config[CONF_AS3935_ID])
if distance_config := config.get(CONF_DISTANCE): if distance_config := config.get(CONF_DISTANCE):
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import as3935, i2c from esphome.components import as3935, i2c
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID from esphome.const import CONF_ID
from esphome.types import ConfigType
AUTO_LOAD = ["as3935"] AUTO_LOAD = ["as3935"]
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
@@ -20,7 +21,7 @@ CONFIG_SCHEMA = cv.All(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await as3935.setup_as3935(var, config) await as3935.setup_as3935(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import as3935, spi from esphome.components import as3935, spi
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID from esphome.const import CONF_ID
from esphome.types import ConfigType
AUTO_LOAD = ["as3935"] AUTO_LOAD = ["as3935"]
DEPENDENCIES = ["spi"] DEPENDENCIES = ["spi"]
@@ -20,7 +21,7 @@ CONFIG_SCHEMA = cv.All(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await as3935.setup_as3935(var, config) await as3935.setup_as3935(var, config)
await spi.register_spi_device(var, config) await spi.register_spi_device(var, config)
+14 -10
View File
@@ -1,3 +1,6 @@
from collections.abc import Callable
from typing import Any
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import i2c from esphome.components import i2c
@@ -11,6 +14,7 @@ from esphome.const import (
CONF_RANGE, CONF_RANGE,
CONF_WATCHDOG, CONF_WATCHDOG,
) )
from esphome.types import ConfigType
CODEOWNERS = ["@ammmze"] CODEOWNERS = ["@ammmze"]
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
@@ -72,13 +76,13 @@ POSITION_TO_ANGLE = 360 / RESOLUTION
MIN_RANGE = round(18 * ANGLE_TO_POSITION) MIN_RANGE = round(18 * ANGLE_TO_POSITION)
def angle(min=-360, max=360): def angle(min: float = -360, max: float = 360) -> Callable[[Any], Any]:
return cv.All( return cv.All(
cv.float_with_unit("angle", "(°|deg)"), cv.float_range(min=min, max=max) cv.float_with_unit("angle", "(°|deg)"), cv.float_range(min=min, max=max)
) )
def angle_to_position(value, min=-360, max=360): def angle_to_position(value: Any, min: float = -360, max: float = 360) -> int:
try: try:
value = angle(min=min, max=max)(value) value = angle(min=min, max=max)(value)
return (RESOLUTION + round(value * ANGLE_TO_POSITION)) % RESOLUTION return (RESOLUTION + round(value * ANGLE_TO_POSITION)) % RESOLUTION
@@ -86,17 +90,17 @@ def angle_to_position(value, min=-360, max=360):
raise cv.Invalid(f"When using angle, {e.error_message}") from e raise cv.Invalid(f"When using angle, {e.error_message}") from e
def percent_to_position(value): def percent_to_position(value: Any) -> int:
value = cv.possibly_negative_percentage(value) value = cv.possibly_negative_percentage(value)
return (RESOLUTION + round(value * RESOLUTION)) % RESOLUTION return (RESOLUTION + round(value * RESOLUTION)) % RESOLUTION
def position(min=-MAX_POSITION, max=MAX_POSITION): def position(min: int = -MAX_POSITION, max: int = MAX_POSITION) -> Callable[[Any], Any]:
"""Validate that the config option is a position. """Validate that the config option is a position.
Accepts integers, degrees, or percentage (of 360 degrees). Accepts integers, degrees, or percentage (of 360 degrees).
""" """
def validator(value): def validator(value: Any) -> int:
if isinstance(value, str) and value.endswith("%"): if isinstance(value, str) and value.endswith("%"):
value = percent_to_position(value) value = percent_to_position(value)
@@ -112,7 +116,7 @@ def position(min=-MAX_POSITION, max=MAX_POSITION):
return validator return validator
def position_range(): def position_range() -> Callable[[Any], Any]:
"""Validate that value given is a valid range for the device. """Validate that value given is a valid range for the device.
A valid range is one of the following: A valid range is one of the following:
- a value of 0 (meaning full range) - a value of 0 (meaning full range)
@@ -129,7 +133,7 @@ def position_range():
zero_validator, zero_validator,
) )
def validator(value): def validator(value: Any) -> Any:
is_negative_str = isinstance(value, str) and value.startswith("-") is_negative_str = isinstance(value, str) and value.startswith("-")
is_negative_num = isinstance(value, (float, int)) and value < 0 is_negative_num = isinstance(value, (float, int)) and value < 0
if is_negative_str or is_negative_num: if is_negative_str or is_negative_num:
@@ -139,13 +143,13 @@ def position_range():
return validator return validator
def has_valid_range_config(): def has_valid_range_config() -> Callable[[ConfigType], ConfigType]:
"""Validate that that the config start + end position results in a valid """Validate that that the config start + end position results in a valid
positional range, which must be >= 18degrees positional range, which must be >= 18degrees
""" """
range_validator = position_range() range_validator = position_range()
def validator(config): def validator(config: ConfigType) -> ConfigType:
# if we don't have an end position, then there is nothing to do # if we don't have an end position, then there is nothing to do
if CONF_END_POSITION not in config: if CONF_END_POSITION not in config:
return config return config
@@ -203,7 +207,7 @@ CONFIG_SCHEMA = cv.All(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -11,6 +11,7 @@ from esphome.const import (
ICON_ROTATE_RIGHT, ICON_ROTATE_RIGHT,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
) )
from esphome.types import ConfigType
from .. import AS5600Component, as5600_ns from .. import AS5600Component, as5600_ns
@@ -77,7 +78,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_parented(var, config[CONF_AS5600_ID]) await cg.register_parented(var, config[CONF_AS5600_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
+2 -1
View File
@@ -9,6 +9,7 @@ from esphome.const import (
ICON_BRIGHTNESS_5, ICON_BRIGHTNESS_5,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
) )
from esphome.types import ConfigType
CODEOWNERS = ["@mrgnr"] CODEOWNERS = ["@mrgnr"]
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
@@ -96,7 +97,7 @@ SENSORS = {
} }
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
DEPENDENCIES = ["network"] DEPENDENCIES = ["network"]
@@ -25,7 +26,7 @@ CONFIG_SCHEMA = cv.Schema({})
@coroutine_with_priority(CoroPriority.NETWORK_TRANSPORT) @coroutine_with_priority(CoroPriority.NETWORK_TRANSPORT)
async def to_code(config): async def to_code(config: ConfigType) -> None:
if CORE.is_esp32: if CORE.is_esp32:
# https://github.com/ESP32Async/AsyncTCP # https://github.com/ESP32Async/AsyncTCP
from esphome.components.esp32 import add_idf_component from esphome.components.esp32 import add_idf_component
+16 -3
View File
@@ -4,6 +4,9 @@ import esphome.codegen as cg
from esphome.components import i2c from esphome.components import i2c
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_FREQUENCY, CONF_ID from esphome.const import CONF_FREQUENCY, CONF_ID
from esphome.core import ID
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
CODEOWNERS = ["@X-Ryl669"] CODEOWNERS = ["@X-Ryl669"]
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
@@ -70,7 +73,7 @@ CONFIG_SCHEMA = cv.All(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
@@ -91,7 +94,12 @@ AT581XSettingsAction = at581x_ns.class_("AT581XSettingsAction", automation.Actio
), ),
synchronous=True, synchronous=True,
) )
async def at581x_reset_to_code(config, action_id, template_arg, args): async def at581x_reset_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg) var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID]) await cg.register_parented(var, config[CONF_ID])
@@ -163,7 +171,12 @@ RADAR_SETTINGS_SCHEMA = cv.Schema(
RADAR_SETTINGS_SCHEMA, RADAR_SETTINGS_SCHEMA,
synchronous=True, synchronous=True,
) )
async def at581x_settings_to_code(config, action_id, template_arg, args): async def at581x_settings_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg) var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID]) await cg.register_parented(var, config[CONF_ID])
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import switch from esphome.components import switch
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import DEVICE_CLASS_SWITCH, ICON_WIFI from esphome.const import DEVICE_CLASS_SWITCH, ICON_WIFI
from esphome.types import ConfigType
from .. import CONF_AT581X_ID, AT581XComponent, at581x_ns from .. import CONF_AT581X_ID, AT581XComponent, at581x_ns
@@ -22,7 +23,7 @@ CONFIG_SCHEMA = switch.switch_schema(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
at581x_component = await cg.get_variable(config[CONF_AT581X_ID]) at581x_component = await cg.get_variable(config[CONF_AT581X_ID])
s = await switch.new_switch(config) s = await switch.new_switch(config)
await cg.register_parented(s, config[CONF_AT581X_ID]) await cg.register_parented(s, config[CONF_AT581X_ID])
@@ -21,6 +21,7 @@ from esphome.const import (
UNIT_PERCENT, UNIT_PERCENT,
UNIT_VOLT, UNIT_VOLT,
) )
from esphome.types import ConfigType
CODEOWNERS = ["@ahpohl"] CODEOWNERS = ["@ahpohl"]
@@ -77,7 +78,7 @@ CONFIG_SCHEMA = cv.All(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await ble_device_base.register_ble_device(var, config) await ble_device_base.register_ble_device(var, config)
+2 -1
View File
@@ -30,6 +30,7 @@ from esphome.const import (
UNIT_WATT, UNIT_WATT,
UNIT_WATT_HOURS, UNIT_WATT_HOURS,
) )
from esphome.types import ConfigType
CONF_METER_CONSTANT = "meter_constant" CONF_METER_CONSTANT = "meter_constant"
CONF_PL_CONST = "pl_const" CONF_PL_CONST = "pl_const"
@@ -123,7 +124,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await spi.register_spi_device(var, config) await spi.register_spi_device(var, config)
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import button from esphome.components import button
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_SCALE from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_SCALE
from esphome.types import ConfigType
from .. import atm90e32_ns from .. import atm90e32_ns
from ..sensor import ATM90E32Component from ..sensor import ATM90E32Component
@@ -67,7 +68,7 @@ CONFIG_SCHEMA = {
} }
async def to_code(config): async def to_code(config: ConfigType) -> None:
parent = await cg.get_variable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_ID])
if run_gain := config.get(CONF_RUN_GAIN_CALIBRATION): if run_gain := config.get(CONF_RUN_GAIN_CALIBRATION):
@@ -15,6 +15,7 @@ from esphome.const import (
UNIT_AMPERE, UNIT_AMPERE,
UNIT_VOLT, UNIT_VOLT,
) )
from esphome.types import ConfigType
from .. import atm90e32_ns from .. import atm90e32_ns
from ..sensor import ATM90E32Component from ..sensor import ATM90E32Component
@@ -90,7 +91,7 @@ CONFIG_SCHEMA = cv.Schema(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
parent = await cg.get_variable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_ID])
if voltage_cfg := config.get(CONF_REFERENCE_VOLTAGE): if voltage_cfg := config.get(CONF_REFERENCE_VOLTAGE):
+2 -1
View File
@@ -41,6 +41,7 @@ from esphome.const import (
UNIT_WATT, UNIT_WATT,
UNIT_WATT_HOURS, UNIT_WATT_HOURS,
) )
from esphome.types import ConfigType
from . import atm90e32_ns from . import atm90e32_ns
@@ -191,7 +192,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_instance_id(str(config[CONF_ID]))) cg.add(var.set_instance_id(str(config[CONF_ID])))
await cg.register_component(var, config) await cg.register_component(var, config)
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import text_sensor from esphome.components import text_sensor
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C from esphome.const import CONF_ID, CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C
from esphome.types import ConfigType
from ..sensor import ATM90E32Component from ..sensor import ATM90E32Component
@@ -34,7 +35,7 @@ CONFIG_SCHEMA = cv.Schema(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
parent = await cg.get_variable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_ID])
if phase_cfg := config.get(CONF_PHASE_STATUS): if phase_cfg := config.get(CONF_PHASE_STATUS):
+10 -7
View File
@@ -1,4 +1,6 @@
from collections.abc import Callable
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.esp32 import ( from esphome.components.esp32 import (
@@ -15,6 +17,7 @@ from esphome.const import (
) )
from esphome.core import CORE from esphome.core import CORE
import esphome.final_validate as fv import esphome.final_validate as fv
from esphome.types import ConfigType
AUTO_LOAD = ["ring_buffer"] AUTO_LOAD = ["ring_buffer"]
CODEOWNERS = ["@kahrendt"] CODEOWNERS = ["@kahrendt"]
@@ -125,10 +128,10 @@ CONF_THREADSAFE = "threadsafe"
_MEMORY_LOCATION_VALIDATOR = cv.one_of(*MEMORY_LOCATIONS, lower=True) _MEMORY_LOCATION_VALIDATOR = cv.one_of(*MEMORY_LOCATIONS, lower=True)
def _maybe_empty_codec(schema): def _maybe_empty_codec(schema: cv.Schema) -> Callable[[Any], Any]:
"""Wrap a codec dict schema so that a bare key (None value) is treated as an empty dict.""" """Wrap a codec dict schema so that a bare key (None value) is treated as an empty dict."""
def validator(value): def validator(value: Any) -> Any:
if value is None: if value is None:
value = {} value = {}
return schema(value) return schema(value)
@@ -200,14 +203,14 @@ def set_stream_limits(
max_channels: int = cv.UNDEFINED, max_channels: int = cv.UNDEFINED,
min_sample_rate: int = cv.UNDEFINED, min_sample_rate: int = cv.UNDEFINED,
max_sample_rate: int = cv.UNDEFINED, max_sample_rate: int = cv.UNDEFINED,
): ) -> Callable[[ConfigType], None]:
"""Sets the limits for the audio stream that audio component can handle """Sets the limits for the audio stream that audio component can handle
When the component sinks audio (e.g., a speaker), these indicate the limits to the audio it can receive. When the component sinks audio (e.g., a speaker), these indicate the limits to the audio it can receive.
When the component sources audio (e.g., a microphone), these indicate the limits to the audio it can send. When the component sources audio (e.g., a microphone), these indicate the limits to the audio it can send.
""" """
def set_limits_in_config(config): def set_limits_in_config(config: ConfigType) -> None:
if min_bits_per_sample is not cv.UNDEFINED: if min_bits_per_sample is not cv.UNDEFINED:
config[CONF_MIN_BITS_PER_SAMPLE] = min_bits_per_sample config[CONF_MIN_BITS_PER_SAMPLE] = min_bits_per_sample
if max_bits_per_sample is not cv.UNDEFINED: if max_bits_per_sample is not cv.UNDEFINED:
@@ -233,7 +236,7 @@ def final_validate_audio_schema(
sample_rate: int = cv.UNDEFINED, sample_rate: int = cv.UNDEFINED,
enabled_channels: list[int] = cv.UNDEFINED, enabled_channels: list[int] = cv.UNDEFINED,
audio_device_issue: bool = False, audio_device_issue: bool = False,
): ) -> cv.Schema:
"""Validates audio compatibility when passed between different components. """Validates audio compatibility when passed between different components.
The component derived from ``AUDIO_COMPONENT_SCHEMA`` should call ``set_stream_limits`` in a validator to specify its compatible settings The component derived from ``AUDIO_COMPONENT_SCHEMA`` should call ``set_stream_limits`` in a validator to specify its compatible settings
@@ -251,7 +254,7 @@ def final_validate_audio_schema(
audio_device_issue (bool, optional): Format the error message to indicate the problem is in the configuration for the ``audio_device`` component. Defaults to False. audio_device_issue (bool, optional): Format the error message to indicate the problem is in the configuration for the ``audio_device`` component. Defaults to False.
""" """
def validate_audio_compatiblity(audio_config): def validate_audio_compatiblity(audio_config: ConfigType) -> ConfigType:
audio_schema = {} audio_schema = {}
if bits_per_sample is not cv.UNDEFINED: if bits_per_sample is not cv.UNDEFINED:
@@ -329,7 +332,7 @@ def _emit_memory_pair(value: str | None, psram_key: str, internal_key: str) -> N
add_idf_sdkconfig_option(internal_key, True) add_idf_sdkconfig_option(internal_key, True)
async def to_code(config): async def to_code(config: ConfigType) -> None:
# Re-enable ESP-IDF's HTTP client (excluded by default to save compile time) # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time)
include_builtin_idf_component("esp_http_client") include_builtin_idf_component("esp_http_client")
+1 -1
View File
@@ -86,7 +86,7 @@ AudioFileType detect_audio_file_type(const char *content_type, const char *url)
// Match "audio/ogg" with a codecs parameter containing "opus" // Match "audio/ogg" with a codecs parameter containing "opus"
// Valid forms: audio/ogg;codecs=opus, audio/ogg; codecs="opus", etc. // Valid forms: audio/ogg;codecs=opus, audio/ogg; codecs="opus", etc.
// Plain "audio/ogg" without opus is not matched (almost always Ogg Vorbis) // Plain "audio/ogg" without opus is not matched (almost always Ogg Vorbis)
if (strncasecmp(content_type, "audio/ogg", 9) == 0 && strcasestr(content_type + 9, "opus") != nullptr) { if (strncasecmp(content_type, "audio/ogg", 9) == 0 && str_contains_ignore_case(content_type + 9, "opus")) {
return AudioFileType::OPUS; return AudioFileType::OPUS;
} }
#endif #endif
+10 -3
View File
@@ -2,7 +2,9 @@ from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_MIC_GAIN from esphome.const import CONF_ID, CONF_MIC_GAIN
from esphome.core import CoroPriority, coroutine_with_priority from esphome.core import ID, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
CODEOWNERS = ["@kbx81"] CODEOWNERS = ["@kbx81"]
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
@@ -28,7 +30,12 @@ SET_MIC_GAIN_ACTION_SCHEMA = cv.maybe_simple_value(
SET_MIC_GAIN_ACTION_SCHEMA, SET_MIC_GAIN_ACTION_SCHEMA,
synchronous=True, synchronous=True,
) )
async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args): async def audio_adc_set_mic_gain_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren) var = cg.new_Pvariable(action_id, template_arg, paren)
@@ -39,6 +46,6 @@ async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args):
@coroutine_with_priority(CoroPriority.CORE) @coroutine_with_priority(CoroPriority.CORE)
async def to_code(config): async def to_code(config: ConfigType) -> None:
cg.add_define("USE_AUDIO_ADC") cg.add_define("USE_AUDIO_ADC")
cg.add_global(audio_adc_ns.using) cg.add_global(audio_adc_ns.using)
+16 -4
View File
@@ -3,7 +3,9 @@ from esphome.automation import maybe_simple_id
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_VOLUME from esphome.const import CONF_ID, CONF_VOLUME
from esphome.core import CoroPriority, coroutine_with_priority from esphome.core import ID, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
CODEOWNERS = ["@kbx81"] CODEOWNERS = ["@kbx81"]
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
@@ -37,7 +39,12 @@ SET_VOLUME_ACTION_SCHEMA = cv.maybe_simple_value(
@automation.register_action( @automation.register_action(
"audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA, synchronous=True "audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA, synchronous=True
) )
async def audio_dac_mute_action_to_code(config, action_id, template_arg, args): async def audio_dac_mute_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren) return cg.new_Pvariable(action_id, template_arg, paren)
@@ -48,7 +55,12 @@ async def audio_dac_mute_action_to_code(config, action_id, template_arg, args):
SET_VOLUME_ACTION_SCHEMA, SET_VOLUME_ACTION_SCHEMA,
synchronous=True, synchronous=True,
) )
async def audio_dac_set_volume_to_code(config, action_id, template_arg, args): async def audio_dac_set_volume_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren) var = cg.new_Pvariable(action_id, template_arg, paren)
@@ -59,6 +71,6 @@ async def audio_dac_set_volume_to_code(config, action_id, template_arg, args):
@coroutine_with_priority(CoroPriority.CORE) @coroutine_with_priority(CoroPriority.CORE)
async def to_code(config): async def to_code(config: ConfigType) -> None:
cg.add_define("USE_AUDIO_DAC") cg.add_define("USE_AUDIO_DAC")
cg.add_global(audio_dac_ns.using) cg.add_global(audio_dac_ns.using)
@@ -30,8 +30,9 @@ void AudioHTTPMediaSource::dump_config() {
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
"Audio HTTP Media Source:\n" "Audio HTTP Media Source:\n"
" Buffer Size: %zu bytes\n" " Buffer Size: %zu bytes\n"
" Persistent Ring Buffer: %s\n"
" Decoder Task Stack in PSRAM: %s", " Decoder Task Stack in PSRAM: %s",
this->buffer_size_, YESNO(this->decoder_task_stack_in_psram_)); this->buffer_size_, YESNO(this->persistent_ring_buffer_), YESNO(this->decoder_task_stack_in_psram_));
} }
void AudioHTTPMediaSource::setup() { void AudioHTTPMediaSource::setup() {
@@ -39,6 +40,7 @@ void AudioHTTPMediaSource::setup() {
micro_decoder::DecoderConfig config; micro_decoder::DecoderConfig config;
config.ring_buffer_size = this->buffer_size_; config.ring_buffer_size = this->buffer_size_;
config.persistent_ring_buffer = this->persistent_ring_buffer_;
// Keep the transfer buffer smaller than the ring buffer so the reader can top up the ring // Keep the transfer buffer smaller than the ring buffer so the reader can top up the ring
// while the decoder is still draining it, instead of oscillating between empty and full. // while the decoder is still draining it, instead of oscillating between empty and full.
config.transfer_buffer_size = std::min(DEFAULT_TRANSFER_BUFFER_SIZE, this->buffer_size_ / 2); config.transfer_buffer_size = std::min(DEFAULT_TRANSFER_BUFFER_SIZE, this->buffer_size_ / 2);
@@ -33,6 +33,7 @@ class AudioHTTPMediaSource final : public Component,
void set_buffer_size(size_t buffer_size) { this->buffer_size_ = buffer_size; } void set_buffer_size(size_t buffer_size) { this->buffer_size_ = buffer_size; }
void set_task_stack_in_psram(bool task_stack_in_psram) { this->decoder_task_stack_in_psram_ = task_stack_in_psram; } void set_task_stack_in_psram(bool task_stack_in_psram) { this->decoder_task_stack_in_psram_ = task_stack_in_psram; }
void set_persistent_ring_buffer(bool persistent) { this->persistent_ring_buffer_ = persistent; }
// MediaSource interface implementation // MediaSource interface implementation
bool play_uri(const std::string &uri) override; bool play_uri(const std::string &uri) override;
@@ -54,6 +55,7 @@ class AudioHTTPMediaSource final : public Component,
// on_audio_write(). Must be atomic to avoid a data race. // on_audio_write(). Must be atomic to avoid a data race.
std::atomic<bool> pause_{false}; std::atomic<bool> pause_{false};
bool decoder_task_stack_in_psram_{false}; bool decoder_task_stack_in_psram_{false};
bool persistent_ring_buffer_{false};
}; };
} // namespace esphome::audio_http } // namespace esphome::audio_http
@@ -7,6 +7,8 @@ from esphome.types import ConfigType
CODEOWNERS = ["@kahrendt"] CODEOWNERS = ["@kahrendt"]
AUTO_LOAD = ["audio"] AUTO_LOAD = ["audio"]
CONF_PERSISTENT_RING_BUFFER = "persistent_ring_buffer"
audio_http_ns = cg.esphome_ns.namespace("audio_http") audio_http_ns = cg.esphome_ns.namespace("audio_http")
AudioHTTPMediaSource = audio_http_ns.class_( AudioHTTPMediaSource = audio_http_ns.class_(
"AudioHTTPMediaSource", cg.Component, media_source.MediaSource "AudioHTTPMediaSource", cg.Component, media_source.MediaSource
@@ -28,6 +30,7 @@ CONFIG_SCHEMA = cv.All(
min=5000, max=1000000 min=5000, max=1000000
), ),
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram, cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
cv.Optional(CONF_PERSISTENT_RING_BUFFER, default=False): cv.boolean,
} }
) )
.extend(cv.COMPONENT_SCHEMA), .extend(cv.COMPONENT_SCHEMA),
@@ -45,3 +48,4 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_task_stack_in_psram(True)) cg.add(var.set_task_stack_in_psram(True))
psram.request_external_task_stack() psram.request_external_task_stack()
cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE])) cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))
cg.add(var.set_persistent_ring_buffer(config[CONF_PERSISTENT_RING_BUFFER]))
@@ -3,6 +3,7 @@ import esphome.codegen as cg
from esphome.components import i2c, touchscreen from esphome.components import i2c, touchscreen
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
from esphome.types import ConfigType
from .. import axs15231_ns from .. import axs15231_ns
@@ -25,7 +26,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await touchscreen.register_touchscreen(var, config) await touchscreen.register_touchscreen(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -20,6 +20,7 @@ from esphome.const import (
UNIT_PERCENT, UNIT_PERCENT,
UNIT_VOLT, UNIT_VOLT,
) )
from esphome.types import ConfigType
CODEOWNERS = ["@rbaron"] CODEOWNERS = ["@rbaron"]
@@ -74,7 +75,7 @@ CONFIG_SCHEMA = cv.All(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await ble_device_base.register_ble_device(var, config) await ble_device_base.register_ble_device(var, config)
+2 -1
View File
@@ -1,5 +1,6 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate_ir from esphome.components import climate_ir
from esphome.types import ConfigType
AUTO_LOAD = ["climate_ir"] AUTO_LOAD = ["climate_ir"]
CODEOWNERS = ["@bazuchan"] CODEOWNERS = ["@bazuchan"]
@@ -10,5 +11,5 @@ BalluClimate = ballu_ns.class_("BalluClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(BalluClimate) CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(BalluClimate)
async def to_code(config): async def to_code(config: ConfigType) -> None:
await climate_ir.new_climate_ir(config) await climate_ir.new_climate_ir(config)
+2 -1
View File
@@ -12,6 +12,7 @@ from esphome.const import (
CONF_IDLE_ACTION, CONF_IDLE_ACTION,
CONF_SENSOR, CONF_SENSOR,
) )
from esphome.types import ConfigType
bang_bang_ns = cg.esphome_ns.namespace("bang_bang") bang_bang_ns = cg.esphome_ns.namespace("bang_bang")
BangBangClimate = bang_bang_ns.class_("BangBangClimate", climate.Climate, cg.Component) BangBangClimate = bang_bang_ns.class_("BangBangClimate", climate.Climate, cg.Component)
@@ -41,7 +42,7 @@ CONFIG_SCHEMA = cv.All(
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = await climate.new_climate(config) var = await climate.new_climate(config)
await cg.register_component(var, config) await cg.register_component(var, config)
+4 -2
View File
@@ -2,6 +2,8 @@ import esphome.codegen as cg
from esphome.components import ble_client, time from esphome.components import ble_client, time
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_RECEIVE_TIMEOUT, CONF_TIME_ID from esphome.const import CONF_ID, CONF_RECEIVE_TIMEOUT, CONF_TIME_ID
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
CODEOWNERS = ["@jhansche"] CODEOWNERS = ["@jhansche"]
DEPENDENCIES = ["ble_client"] DEPENDENCIES = ["ble_client"]
@@ -32,12 +34,12 @@ BEDJET_CLIENT_SCHEMA = cv.Schema(
) )
async def register_bedjet_child(var, config): async def register_bedjet_child(var: MockObj, config: ConfigType) -> None:
parent = await cg.get_variable(config[CONF_BEDJET_ID]) parent = await cg.get_variable(config[CONF_BEDJET_ID])
cg.add(parent.register_child(var)) cg.add(parent.register_child(var))
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await ble_client.register_ble_node(var, config) await ble_client.register_ble_node(var, config)
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import climate from esphome.components import climate
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_HEAT_MODE, CONF_TEMPERATURE_SOURCE from esphome.const import CONF_HEAT_MODE, CONF_TEMPERATURE_SOURCE
from esphome.types import ConfigType
from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child
@@ -37,7 +38,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = await climate.new_climate(config) var = await climate.new_climate(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await register_bedjet_child(var, config) await register_bedjet_child(var, config)
+2 -1
View File
@@ -1,6 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import fan from esphome.components import fan
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.types import ConfigType
from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child
@@ -16,7 +17,7 @@ CONFIG_SCHEMA = (
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = await fan.new_fan(config) var = await fan.new_fan(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await register_bedjet_child(var, config) await register_bedjet_child(var, config)
+2 -1
View File
@@ -9,6 +9,7 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS, UNIT_CELSIUS,
) )
from esphome.types import ConfigType
from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child
@@ -38,7 +39,7 @@ CONFIG_SCHEMA = cv.Schema(
).extend(BEDJET_CLIENT_SCHEMA) ).extend(BEDJET_CLIENT_SCHEMA)
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await register_bedjet_child(var, config) await register_bedjet_child(var, config)
@@ -56,7 +56,7 @@ SUPPORTED_PINS = {
} }
def _validate_pin(value): def _validate_pin(value: int) -> int:
family = libretiny.get_libretiny_family() family = libretiny.get_libretiny_family()
if family not in SUPPORTED_PINS: if family not in SUPPORTED_PINS:
raise cv.Invalid(f"Chip family {family} is not supported.") raise cv.Invalid(f"Chip family {family} is not supported.")

Some files were not shown because too many files have changed in this diff Show More