Merge branch 'dev' into jesserockz-2026-503

This commit is contained in:
Jesse Hills
2026-08-12 17:27:35 +12:00
committed by GitHub
984 changed files with 50402 additions and 7012 deletions
+1
View File
@@ -52,6 +52,7 @@ COMMON_BUS_PATH = (
# the packages on the right as well
PACKAGE_DEPENDENCIES = {
"modbus": ["uart"], # modbus packages include uart packages
"modbus_server": ["uart"], # modbus_server packages include uart packages
# Add more package dependencies here as needed
}
+4 -1
View File
@@ -2401,7 +2401,10 @@ def get_varint64_ifdef(
# At least one 64-bit varint field is unconditional, so the guard must be unconditional.
return True, None
ifdefs.discard(None)
return True, ifdefs.pop() if len(ifdefs) == 1 else None
# Several guards: the define is needed under any of them, so emit the union.
# Falling back to unconditional would pull 64-bit varint support into builds
# that have none of them.
return True, " || ".join(sorted(ifdefs))
def build_enum_type(desc, enum_ifdef_map) -> tuple[str, str, str]:
+18 -2
View File
@@ -1134,13 +1134,29 @@ def convert_keys(converted, schema, path):
else:
converted["key"] = "String"
key_string_match = re.search(
r"<function (\w*) at \w*>", str(k), re.IGNORECASE
r"<function ([^ ]+) at \w+>", str(k), re.IGNORECASE
)
if key_string_match:
converted["key_type"] = key_string_match.group(1)
else:
converted["key_type"] = str(k)
# A marker-wrapped callable key (e.g. script.execute's
# ``cv.Optional(validate_parameter_name)``) is a wildcard matcher;
# ``str(marker)`` is the function repr, whose heap address would
# churn the dump every build. Normalize like the bare-callable
# branch above: record the validator name in ``key_type`` and file
# the config var under ``string``.
key_name = str(k)
if isinstance(k, vol.Marker) and callable(k.schema):
key_string_match = re.search(
r"<function ([^ ]+) at \w+>", key_name, re.IGNORECASE
)
result["key_type"] = (
key_string_match.group(1) if key_string_match else key_name
)
key_name = "string"
# ``cv.OnlyWith`` / ``cv.OnlyWithout`` expose ``default`` as
# a property that returns ``vol.UNDEFINED`` when the gating
# component isn't loaded — and at schema-generation time
@@ -1220,7 +1236,7 @@ def convert_keys(converted, schema, path):
for base_k, base_v in get_overridden_config(k, converted).items():
if base_k in result and base_v == result[base_k]:
result.pop(base_k)
converted["schema"][S_CONFIG_VARS][str(k)] = result
converted["schema"][S_CONFIG_VARS][key_name] = result
if "key" in converted and converted["key"] == "String":
config_vars = converted["schema"]["config_vars"]
assert len(config_vars) == 1
+1 -1
View File
@@ -560,7 +560,7 @@ def lint_constants_usage():
# Maximum allowed CONF_ constants in esphome/const.py.
# This file is frozen — new constants go in esphome/components/const/__init__.py.
# Decrease this number when constants are moved out of const.py.
CONST_PY_MAX_CONF = 1015
CONST_PY_MAX_CONF = 1017
@lint_content_check(include=["esphome/const.py"])
+35
View File
@@ -5,11 +5,13 @@ import os
from pathlib import Path
import queue
import re
import shlex
import shutil
import subprocess
import sys
import tempfile
import threading
from typing import Any
import click
import colorama
@@ -29,6 +31,36 @@ from helpers import (
)
def gcc_multilib_directory(idedata: dict[str, Any]) -> str | None:
"""The toolchain's active multilib subdirectory (e.g. "thumb"), if any.
PlatformIO's idedata lists the generic toolchain include directories; GCC
resolves the active multilib subdirectory internally while searching them.
Toolchains without a default multilib (pico-quick-toolchain 5.0.0+) ship
the libstdc++ target config (bits/c++config.h) only inside the multilib
subdirectories, so clang needs the resolved directory spelled out.
"""
machine_flags = [f for f in idedata["cxx_flags"] if f.startswith("-m")]
cmd = [idedata["cxx_path"], *machine_flags, "-print-multi-directory"]
try:
multilib = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True,
).stdout.strip()
except (OSError, subprocess.CalledProcessError) as err:
# Without the multilib dir, toolchains lacking a default multilib fail
# later with "bits/c++config.h not found"; point at the probe instead.
stderr = getattr(err, "stderr", "") or ""
print(
f"WARNING: multilib probe failed ({shlex.join(cmd)}): {err} {stderr}".strip(),
file=sys.stderr,
)
return None
return None if multilib in ("", ".") else multilib
def clang_options(idedata, environment):
cmd = []
@@ -203,9 +235,12 @@ def clang_options(idedata, environment):
# toolchain include directories, using -isystem to suppress their errors
# idedata contains include directories for all toolchains of this platform, only use those from the one in use
toolchain_dir = os.path.normpath(f"{idedata['cxx_path']}/../../")
multilib = gcc_multilib_directory(idedata)
toolchain_includes = []
for directory in idedata["includes"]["toolchain"]:
if directory.startswith(toolchain_dir) and "picolibc" not in directory:
if multilib and (multilib_dir := Path(directory) / multilib).is_dir():
toolchain_includes.extend(["-isystem", str(multilib_dir)])
toolchain_includes.extend(["-isystem", directory])
# library include directories, using -isystem to suppress their errors
+1
View File
@@ -18,6 +18,7 @@ from pathlib import Path
# Root-relative paths whose contents affect clang-tidy results.
CLANG_TIDY_GLOBAL_FILES = (
".clang-tidy",
"script/clang-tidy",
"platformio.ini",
"requirements_dev.txt",
"esphome/idf_component.yml",
+19 -9
View File
@@ -23,7 +23,7 @@ what files have changed. It outputs JSON with the following structure:
}
The CI workflow uses this information to:
- Gate the unconditional jobs (ci-custom, pytest, pre-commit-ci-lite) via core_ci;
- Gate the unconditional jobs (ci-custom, pytest, lint-format) via core_ci;
false when a pull_request only touches CI-irrelevant meta paths (other workflow
files, .github/actions/build-image/*, .yamllint, .github/dependabot.yml, docker/**)
so workflow-only PRs satisfy the required CI Status check without running the
@@ -63,6 +63,7 @@ from helpers import (
CPP_FILE_EXTENSIONS,
ESPHOME_TESTS_COMPONENTS_PATH,
PYTHON_FILE_EXTENSIONS,
base_python_changed,
changed_files,
core_changed,
filter_component_and_test_cpp_files,
@@ -657,16 +658,20 @@ BENCHMARK_INFRASTRUCTURE_FILES = frozenset(
def should_run_benchmarks(branch: str | None = None) -> bool:
"""Determine if C++ benchmarks should run based on changed files.
"""Determine if benchmarks (C++ and Python) should run based on changed files.
Benchmarks run when any of the following conditions are met:
1. Core C++ files changed (esphome/core/*)
2. The host platform changed (esphome/components/host/*) — benchmarks
1. Core files changed (esphome/core/*, C++ or Python)
2. Top-level Python files changed (esphome/*.py and esphome/*.pyi) —
the Python benchmarks exercise config loading (config.py,
yaml_util.py, ...), so a slowdown there is invisible unless the
benchmarks job runs
3. The host platform changed (esphome/components/host/*) — benchmarks
are built and run on the host platform, so its implementations of
``millis()``/``micros()``/etc. affect every benchmark
3. A directly changed component has benchmark files (no dependency expansion)
4. Benchmark infrastructure changed (tests/benchmarks/*, script/cpp_benchmark.py,
4. A directly changed component has benchmark files (no dependency expansion)
5. Benchmark infrastructure changed (tests/benchmarks/*, script/cpp_benchmark.py,
script/build_helpers.py, script/setup_codspeed_lib.py)
Unlike unit tests, benchmarks do NOT expand to dependent components.
@@ -683,6 +688,11 @@ def should_run_benchmarks(branch: str | None = None) -> bool:
if core_changed(files):
return True
# Top-level esphome/*.py modules are what the Python benchmarks in
# tests/benchmarks/python/ exercise
if base_python_changed(files):
return True
# Host platform supplies the runtime that benchmarks execute on
if any(f.startswith("esphome/components/host/") for f in files):
return True
@@ -708,7 +718,7 @@ def should_run_benchmarks(branch: str | None = None) -> bool:
# Files / path patterns whose changes alone don't warrant running the
# unconditional CI jobs (`ci-custom`, `pytest`, `pre-commit-ci-lite`).
# unconditional CI jobs (`ci-custom`, `pytest`, `lint-format`).
# Single source of truth for what we treat as "CI-irrelevant" on
# pull_request events; ci.yml used to encode this in its own
# `pull_request.paths` filter, but that hid the required `CI Status`
@@ -752,7 +762,7 @@ def _is_ci_irrelevant_path(path: str) -> bool:
def should_run_core_ci(branch: str | None = None) -> bool:
"""Determine if the unconditional CI jobs (ci-custom/pytest/pre-commit-ci-lite) should run.
"""Determine if the unconditional CI jobs (ci-custom/pytest/lint-format) should run.
Returns False only when every changed file is in the CI-irrelevant set
above (see ``_is_ci_irrelevant_path``). Empty diffs return True so we
@@ -1177,7 +1187,7 @@ def main() -> None:
# Determine what should run
# core_ci gates the unconditional jobs in ci.yml (ci-custom, pytest,
# pre-commit-ci-lite). Non-pull_request events (push to dev/beta/release
# lint-format). Non-pull_request events (push to dev/beta/release
# and merge_group) always run them so behavior like venv-cache saves on
# push to dev is preserved.
event_name = os.environ.get("GITHUB_EVENT_NAME", "")
+21
View File
@@ -1380,6 +1380,27 @@ def core_changed(files: list[str]) -> bool:
)
def base_python_changed(files: list[str]) -> bool:
"""Check if any Python file directly in esphome/ has changed.
Matches top-level modules and stubs (.py and .pyi) like esphome/config.py
and esphome/yaml_util.py but not files in subdirectories such as
esphome/components/ or esphome/dashboard/.
Args:
files: List of file paths to check
Returns:
True if any top-level esphome Python file has changed
"""
return any(
f.startswith("esphome/")
and f.endswith(PYTHON_FILE_EXTENSIONS)
and "/" not in f.removeprefix("esphome/")
for f in files
)
def get_cpp_changed_components(files: list[str]) -> list[str]:
"""Get components that have changed C++ files or tests.
+5 -1
View File
@@ -25,7 +25,11 @@ fi
uv pip install setuptools wheel
uv pip install -e ".[dev,test]" --config-settings editable_mode=compat
pre-commit install
# --overwrite replaces any hook already in place. Without it, prek finds a
# previously installed pre-commit hook, moves it aside to
# .git/hooks/pre-commit.legacy and keeps calling it, so every commit would
# run both tools.
prek install --overwrite
mkdir -p .temp
+5 -1
View File
@@ -17,7 +17,11 @@ pip3 install -r requirements.txt -r requirements_test.txt -r requirements_dev.tx
pip3 install setuptools wheel
pip3 install -e ".[dev,test]" --config-settings editable_mode=compat
pre-commit install
rem --overwrite replaces any hook already in place. Without it, prek finds a
rem previously installed pre-commit hook, moves it aside to
rem .git/hooks/pre-commit.legacy and keeps calling it, so every commit would
rem run both tools.
prek install --overwrite
echo .
echo .