mirror of
https://github.com/esphome/esphome.git
synced 2026-08-23 22:56:19 +00:00
Compare commits
148
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57241e40b8 | ||
|
|
531bd8ec62 | ||
|
|
76ddbe5ebd | ||
|
|
40e6e406d7 | ||
|
|
6cfbe5b166 | ||
|
|
e7fc3937bf | ||
|
|
70f4555ec8 | ||
|
|
04a44d6e63 | ||
|
|
cb10904fa3 | ||
|
|
ba0d9e77db | ||
|
|
b0b174f3e2 | ||
|
|
d8857080f0 | ||
|
|
c285af3857 | ||
|
|
acf5ce718c | ||
|
|
5904928f0c | ||
|
|
dceeee69d3 | ||
|
|
8cac10d319 | ||
|
|
030a3b7ab3 | ||
|
|
dfc26e9f09 | ||
|
|
98f138f6b2 | ||
|
|
c395861fa3 | ||
|
|
30b2bbfe3a | ||
|
|
3af17d5c92 | ||
|
|
6eff668dba | ||
|
|
350202efa6 | ||
|
|
14388c859a | ||
|
|
7beb0078c2 | ||
|
|
e26dbb6d75 | ||
|
|
7aa0f30ceb | ||
|
|
3de58dae99 | ||
|
|
fd2971bd70 | ||
|
|
8a9bb7741a | ||
|
|
92b655c5a0 | ||
|
|
c474421399 | ||
|
|
ca611a3159 | ||
|
|
41b0fe6747 | ||
|
|
e580e267ce | ||
|
|
d3590ffd7c | ||
|
|
9179fe26ce | ||
|
|
e6c49e2bd9 | ||
|
|
8018a0d930 | ||
|
|
641c80b193 | ||
|
|
065867f815 | ||
|
|
5aebd7cb41 | ||
|
|
d84293931b | ||
|
|
062c542c36 | ||
|
|
2c32ac2221 | ||
|
|
0c97e2b354 | ||
|
|
8536d54998 | ||
|
|
10bccf3fc0 | ||
|
|
b070d1631c | ||
|
|
c822b80c95 | ||
|
|
2199fcc851 | ||
|
|
b4031dc53a | ||
|
|
eb958bd0a0 | ||
|
|
59fc108aa7 | ||
|
|
9d761102e5 | ||
|
|
1372ec8ad2 | ||
|
|
dde6906f98 | ||
|
|
c4ddad0237 | ||
|
|
6d4d9aa1ce | ||
|
|
caff71c17a | ||
|
|
8e4c9c31e6 | ||
|
|
a478ae4a46 | ||
|
|
b903c884fd | ||
|
|
9731c9527a | ||
|
|
035555b24c | ||
|
|
ea3ff0e372 | ||
|
|
7c0e03f309 | ||
|
|
4efd308345 | ||
|
|
f741c274d5 | ||
|
|
7b3330d5fe | ||
|
|
e681c441d9 | ||
|
|
355d4788b8 | ||
|
|
80b492f638 | ||
|
|
54cd17591d | ||
|
|
98b595c95d | ||
|
|
e16cfded5d | ||
|
|
746b294ed7 | ||
|
|
cf31c08a5c | ||
|
|
e7574a574b | ||
|
|
33484108a9 | ||
|
|
e697a40fda | ||
|
|
cfac36c647 | ||
|
|
d179f43e7c | ||
|
|
96a74cd217 | ||
|
|
dc64c93f39 | ||
|
|
9d0d1b42d5 | ||
|
|
8230c16f12 | ||
|
|
35bb8388a2 | ||
|
|
b08026e413 | ||
|
|
f6c6708f24 | ||
|
|
48c6948ae8 | ||
|
|
d1f065671e | ||
|
|
030e79fa5a | ||
|
|
02da5c6484 | ||
|
|
b2440cb655 | ||
|
|
160d8b8f0c | ||
|
|
8899713ef9 | ||
|
|
f3cdefce21 | ||
|
|
b115813fbe | ||
|
|
ab45ab316a | ||
|
|
5b3a6c05bf | ||
|
|
a1e6370e8b | ||
|
|
dada0f2c2b | ||
|
|
cd53681787 | ||
|
|
f0651e5c9b | ||
|
|
f24b731f95 | ||
|
|
01ad424d12 | ||
|
|
435d522683 | ||
|
|
a63c3bc0c7 | ||
|
|
b83ce91528 | ||
|
|
a30238aab6 | ||
|
|
47156c9a5b | ||
|
|
78240c9a46 | ||
|
|
5a9f06e584 | ||
|
|
ecb007da70 | ||
|
|
ad1a4fca36 | ||
|
|
832a738588 | ||
|
|
fedb3ac5c1 | ||
|
|
cb4e55e444 | ||
|
|
3ef5a8e6a4 | ||
|
|
0e915e9b8b | ||
|
|
dba3b287dd | ||
|
|
ce019f508d | ||
|
|
5c2286cc4a | ||
|
|
efc0a94112 | ||
|
|
c60062c418 | ||
|
|
763a1d9371 | ||
|
|
4db1666024 | ||
|
|
0dc69aab1e | ||
|
|
a282cb095e | ||
|
|
259e7182a3 | ||
|
|
74bdf275d2 | ||
|
|
14499223fd | ||
|
|
6aab523dd9 | ||
|
|
1f31e51446 | ||
|
|
c062d0c717 | ||
|
|
dcabaedff1 | ||
|
|
d119ad6c60 | ||
|
|
74fc2e367a | ||
|
|
ea10f94376 | ||
|
|
ef1d77885d | ||
|
|
dccf55eadc | ||
|
|
65704e881f | ||
|
|
a30e82459f | ||
|
|
5a300e92f1 | ||
|
|
8e9fb0f93c |
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.12.3
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.12.4
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
+18
-3
@@ -762,9 +762,11 @@ def _wrap_to_code(name, comp, yaml_util):
|
||||
async def wrapped(conf):
|
||||
cg.add(cg.LineComment(f"{name}:"))
|
||||
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("//", "")
|
||||
# remove tailing \ to avoid multi-line comment warning
|
||||
# remove trailing \ to avoid multi-line comment warning
|
||||
conf_str = conf_str.replace("\\\n", "\n")
|
||||
cg.add(cg.LineComment(indent(conf_str)))
|
||||
await coro(conf)
|
||||
@@ -855,7 +857,20 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
toolchain.create_factory_bin()
|
||||
toolchain.create_ota_bin()
|
||||
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:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
|
||||
@@ -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
|
||||
toolchain has no such command, but its CMake build emits
|
||||
``build/compile_commands.json`` (CMAKE_EXPORT_COMPILE_COMMANDS). This module
|
||||
turns that file into the same fields consumers (IDE integration, clang-tidy)
|
||||
expect:
|
||||
PlatformIO exposes a curated ``pio run -t idedata`` JSON; the native
|
||||
toolchains have no such command, but each build produces a
|
||||
``compile_commands.json`` (CMAKE_EXPORT_COMPILE_COMMANDS for ESP-IDF, ninja's
|
||||
compdb tool otherwise). This module turns that file into the same fields
|
||||
consumers (IDE integration, clang-tidy) expect:
|
||||
|
||||
{cc_path, cxx_path, cxx_flags, defines, includes: {build, toolchain}}
|
||||
"""
|
||||
@@ -18,6 +18,19 @@ from pathlib import Path
|
||||
import shlex
|
||||
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__)
|
||||
|
||||
# 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:
|
||||
"""Whether ``file`` is an ESPHome C++ translation unit.
|
||||
|
||||
``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.
|
||||
"""
|
||||
"""Whether ``file`` is an ESPHome C++ translation unit; normalized to
|
||||
``/`` first since Windows compile DBs use backslashes."""
|
||||
return _ESPHOME_SRC_MARKER in file.replace("\\", "/") and file.endswith(
|
||||
_CXX_SUFFIXES
|
||||
)
|
||||
@@ -106,11 +115,8 @@ def _expand_response_files(tokens: list[str], directory: Path) -> list[str]:
|
||||
|
||||
|
||||
def _pick_entry(entries: list[dict]) -> dict:
|
||||
"""Pick a representative ESPHome C++ translation unit.
|
||||
|
||||
All ESPHome sources share the same component flags/defines, so any one of
|
||||
them yields the cxx_path / cxx_flags / defines we need.
|
||||
"""
|
||||
"""Pick a representative ESPHome C++ TU; all share the same component
|
||||
flags/defines."""
|
||||
for entry in entries:
|
||||
if _is_esphome_src(entry["file"]):
|
||||
return entry
|
||||
@@ -120,25 +126,46 @@ def _pick_entry(entries: list[dict]) -> dict:
|
||||
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)."""
|
||||
directory = Path(entry["directory"])
|
||||
tokens = _expand_response_files(_split_command(entry["command"]), directory)
|
||||
|
||||
def _include(raw: str) -> str:
|
||||
# Include paths in compile_commands are interpreted relative to the
|
||||
# entry's ``directory`` (e.g. build-local ``-Iconfig``); resolve them
|
||||
# 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.
|
||||
# Resolve against the entry's ``directory`` so cached idedata works
|
||||
# from any cwd; emit forward slashes to match the JSON's own entries
|
||||
raw = raw.strip()
|
||||
if raw and not Path(raw).is_absolute():
|
||||
raw = os.path.normpath(directory / raw)
|
||||
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
|
||||
# slashes on Windows, so normalize it too for a consistent idedata file.
|
||||
cxx_path = tokens[0].replace("\\", "/")
|
||||
# Enforced here so no caller can record ccache as the compiler
|
||||
reject_launcher_compiler(cxx_path)
|
||||
defines: list[str] = []
|
||||
includes: 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
|
||||
|
||||
|
||||
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."""
|
||||
result = subprocess.run(
|
||||
[cxx_path, "-E", "-x", "c++", "-", "-v"],
|
||||
@@ -219,26 +246,128 @@ def _cc_path_from_cxx(cxx_path: str) -> str:
|
||||
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.
|
||||
|
||||
A single ESP-IDF compile entry only carries its own component's REQUIRES
|
||||
include set, but consumers (clang-tidy) analyze ESPHome headers that
|
||||
transitively pull in other components. So take cxx_path / cxx_flags /
|
||||
defines from a representative ESPHome TU, but union the include dirs across
|
||||
all ESPHome TUs to get a project-wide superset (as PlatformIO's idedata
|
||||
provides).
|
||||
A single compile entry only carries the include set its own translation
|
||||
unit was built with (per-component under ESP-IDF), but consumers
|
||||
(clang-tidy) analyze ESPHome headers that transitively pull in other
|
||||
components. So take cxx_path / cxx_flags / defines from a representative
|
||||
ESPHome TU, but union the include dirs across all ESPHome TUs to get a
|
||||
project-wide superset (as PlatformIO's idedata provides).
|
||||
"""
|
||||
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:
|
||||
if not _is_esphome_src(entry["file"]):
|
||||
if entry is representative or not _is_esphome_src(entry["file"]):
|
||||
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)
|
||||
|
||||
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 {
|
||||
"cc_path": _cc_path_from_cxx(cxx_path),
|
||||
"cxx_path": cxx_path,
|
||||
@@ -246,6 +375,6 @@ def idedata_from_build(compile_commands: Path) -> dict:
|
||||
"defines": defines,
|
||||
"includes": {
|
||||
"build": list(build_includes),
|
||||
"toolchain": _get_toolchain_includes(cxx_path),
|
||||
"toolchain": get_toolchain_includes(cxx_path),
|
||||
},
|
||||
}
|
||||
@@ -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,3 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import (
|
||||
@@ -16,6 +18,7 @@ from esphome.components.esp32 import (
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
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 CORE.is_rp2:
|
||||
return pins.internal_gpio_input_pin_schema(29)
|
||||
|
||||
@@ -52,7 +52,7 @@ _attenuation = cv.enum(ATTENUATION_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":
|
||||
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"
|
||||
|
||||
|
||||
def _overlay_io_channels():
|
||||
def _overlay_io_channels() -> str:
|
||||
channel_count = CORE.data[CONF_ADC_CHANNEL_ID]
|
||||
entries = ", ".join(f"<&adc {channel_id}>" for channel_id in range(channel_count))
|
||||
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])
|
||||
await cg.register_component(var, config)
|
||||
await sensor.register_sensor(var, config)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import base64
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
from esphome.automation import Condition
|
||||
@@ -129,7 +130,7 @@ def _register_provisioning_source(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def validate_encryption_key(value):
|
||||
def validate_encryption_key(value: Any) -> str:
|
||||
value = cv.string_strict(value)
|
||||
try:
|
||||
decoded = base64.b64decode(value, validate=True)
|
||||
@@ -217,7 +218,7 @@ def _auto_detect_supports_response(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def _validate_supports_response(value):
|
||||
def _validate_supports_response(value: Any) -> str:
|
||||
"""Validate supports_response after auto-detection has set the value."""
|
||||
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
|
||||
|
||||
@@ -256,7 +257,7 @@ ENCRYPTION_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
def _encryption_schema(config):
|
||||
def _encryption_schema(config: ConfigType | None) -> ConfigType:
|
||||
if config is None:
|
||||
config = {}
|
||||
return ENCRYPTION_SCHEMA(config)
|
||||
@@ -393,7 +394,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
if actions := config.get(CONF_ACTIONS, []):
|
||||
# Collect all triggers first, then register all at once with initializer_list
|
||||
triggers: list[cg.Pvariable] = []
|
||||
triggers: list[cg.MockObj] = []
|
||||
for conf in actions:
|
||||
func_args: list[tuple[MockObj, str]] = []
|
||||
service_template_args: list[MockObj] = [] # User service argument types
|
||||
@@ -581,7 +582,7 @@ async def homeassistant_service_to_code(
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
):
|
||||
) -> MockObj:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
||||
@@ -647,7 +648,7 @@ async def homeassistant_service_to_code(
|
||||
return var
|
||||
|
||||
|
||||
def validate_homeassistant_event(value):
|
||||
def validate_homeassistant_event(value: Any) -> str:
|
||||
value = cv.string(value)
|
||||
if not value.startswith("esphome."):
|
||||
raise cv.Invalid(
|
||||
@@ -676,7 +677,12 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
|
||||
HOMEASSISTANT_EVENT_ACTION_SCHEMA,
|
||||
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")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||
@@ -724,7 +730,12 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
|
||||
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")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||
@@ -740,7 +751,7 @@ CONF_SUCCESS = "success"
|
||||
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."""
|
||||
if CONF_DATA in config:
|
||||
CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True
|
||||
@@ -824,7 +835,12 @@ API_CONNECTED_CONDITION_SCHEMA = cv.Schema(
|
||||
@automation.register_condition(
|
||||
"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)
|
||||
templ = await cg.templatable(config[CONF_STATE_SUBSCRIPTION_ONLY], args, cg.bool_)
|
||||
cg.add(var.set_state_subscription_only(templ))
|
||||
|
||||
@@ -412,15 +412,15 @@ void APIConnection::finalize_iterator_sync_() {
|
||||
}
|
||||
|
||||
void APIConnection::process_iterator_batch_(ComponentIterator &iterator) {
|
||||
size_t initial_size = this->deferred_batch_.size();
|
||||
size_t max_batch = MAX_INITIAL_PER_BATCH;
|
||||
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
|
||||
iterator.advance();
|
||||
}
|
||||
// Budget by remaining batch capacity so a pass cannot overfill the batch;
|
||||
// stops early on a refused send and resumes next loop pass
|
||||
size_t batch_size = this->deferred_batch_.size();
|
||||
if (batch_size < MAX_INITIAL_BATCH_SIZE)
|
||||
iterator.try_advance(MAX_INITIAL_BATCH_SIZE - batch_size);
|
||||
|
||||
// If the batch is full, process it immediately
|
||||
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
|
||||
if (this->deferred_batch_.size() >= max_batch) {
|
||||
// Flush immediately once enough is queued (not guaranteed every pass);
|
||||
// partial batches go out via the batch timer or finalize_iterator_sync_()
|
||||
if (this->deferred_batch_.size() >= MAX_INITIAL_BATCH_SIZE) {
|
||||
this->process_batch_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,11 +53,11 @@ void log_dropped_message(const char *tag, int line, const LogString *what);
|
||||
|
||||
// Keepalive timeout in milliseconds
|
||||
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
|
||||
// Maximum number of entities to process in a single batch during initial state/info sending
|
||||
static constexpr size_t MAX_INITIAL_PER_BATCH = 34;
|
||||
// Deferred batch size cap during initial state/info sync
|
||||
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
|
||||
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH,
|
||||
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH");
|
||||
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_BATCH_SIZE,
|
||||
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_BATCH_SIZE");
|
||||
|
||||
#ifdef USE_BENCHMARK
|
||||
class APIConnection;
|
||||
|
||||
@@ -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;
|
||||
|
||||
// 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;
|
||||
|
||||
// Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars)
|
||||
|
||||
@@ -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)
|
||||
#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
|
||||
void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) {
|
||||
bool has_subscriber = false;
|
||||
@@ -553,10 +547,6 @@ const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_sta
|
||||
}
|
||||
#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
|
||||
bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg,
|
||||
const LogString *fail_log_msg, bool make_active) {
|
||||
|
||||
@@ -51,8 +51,8 @@ class APIServer final : public Component,
|
||||
public:
|
||||
APIServer();
|
||||
void setup() override;
|
||||
uint16_t get_port() const;
|
||||
float get_setup_priority() const override;
|
||||
uint16_t get_port() const { return this->port_; }
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
void on_shutdown() override;
|
||||
@@ -63,9 +63,9 @@ class APIServer final : public Component,
|
||||
#ifdef USE_CAMERA
|
||||
void on_camera_image(const std::shared_ptr<camera::CameraImage> &image) override;
|
||||
#endif
|
||||
void set_port(uint16_t port);
|
||||
void set_reboot_timeout(uint32_t reboot_timeout);
|
||||
void set_batch_delay(uint16_t batch_delay);
|
||||
void set_port(uint16_t port) { this->port_ = port; }
|
||||
void set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
void set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
|
||||
uint16_t get_batch_delay() const { return batch_delay_; }
|
||||
void set_listen_backlog(uint8_t listen_backlog) { this->listen_backlog_ = listen_backlog; }
|
||||
|
||||
|
||||
@@ -95,9 +95,17 @@ bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(
|
||||
ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {}
|
||||
|
||||
#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) {
|
||||
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
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from esphome.automation import Condition, maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import mqtt, web_server, zigbee
|
||||
from esphome.components.const import CONF_ON_STATE_CHANGE
|
||||
from esphome.config_helpers import filter_source_files_from_defines
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DELAY,
|
||||
@@ -560,6 +561,11 @@ _CALLBACK_AUTOMATIONS = (
|
||||
async def _build_binary_sensor_automations(var, config):
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
if config.get(CONF_ON_CLICK) or config.get(CONF_ON_DOUBLE_CLICK):
|
||||
cg.add_define("USE_BINARY_SENSOR_CLICK_TRIGGER")
|
||||
if config.get(CONF_ON_MULTI_CLICK):
|
||||
cg.add_define("USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER")
|
||||
|
||||
for conf in config.get(CONF_ON_CLICK, []):
|
||||
trigger = cg.new_Pvariable(
|
||||
conf[CONF_TRIGGER_ID], var, conf[CONF_MIN_LENGTH], conf[CONF_MAX_LENGTH]
|
||||
@@ -673,3 +679,15 @@ async def to_code(config):
|
||||
async def binary_sensor_invalidate_state_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
# automation.cpp only implements the click/double_click/multi_click triggers
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_defines(
|
||||
{
|
||||
"automation.cpp": (
|
||||
"USE_BINARY_SENSOR_CLICK_TRIGGER",
|
||||
"USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER",
|
||||
),
|
||||
"filter.cpp": "USE_BINARY_SENSOR_FILTER",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#if defined(USE_BINARY_SENSOR_CLICK_TRIGGER) || defined(USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER)
|
||||
|
||||
#include "automation.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
#ifdef USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER
|
||||
|
||||
static const char *const TAG = "binary_sensor.automation";
|
||||
|
||||
// MultiClickTrigger timeout IDs.
|
||||
@@ -120,6 +125,9 @@ void MultiClickTriggerBase::trigger_() {
|
||||
this->trigger();
|
||||
}
|
||||
|
||||
#endif // USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER
|
||||
|
||||
#ifdef USE_BINARY_SENSOR_CLICK_TRIGGER
|
||||
bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) {
|
||||
if (max_length == 0) {
|
||||
return length >= min_length;
|
||||
@@ -127,4 +135,8 @@ bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) {
|
||||
return length >= min_length && length <= max_length;
|
||||
}
|
||||
}
|
||||
#endif // USE_BINARY_SENSOR_CLICK_TRIGGER
|
||||
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
#endif // USE_BINARY_SENSOR_CLICK_TRIGGER || USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER
|
||||
|
||||
@@ -4,9 +4,12 @@ The platform analog of esp32_ble / rp2040_ble: owns the Beken BDK BLE stack
|
||||
bring-up and the controller BLE address. Consumers (bk72xx_ble_tracker) build
|
||||
on this component and contain no SDK calls of their own.
|
||||
|
||||
Supported SoCs (BLE 5.x): BK7231N/BK7236 (BLE 5.1), BK7238/BK7252N/BK7253
|
||||
(BLE 5.2), and any future BLE-5.x SoC. Known non-5.x families are rejected in
|
||||
to_code; unknown families are capability-checked at compile time via
|
||||
Supported SoCs (BLE 5.x): BK7231N/BK7236 (BLE 5.1), BK7252N/BK7253 (BLE 5.2),
|
||||
and any future BLE-5.x SoC. BK7238 (BLE 5.2) is blocked for now: with BLE
|
||||
compiled in, the Beken SDK erases the bootloader flash sector at boot because
|
||||
LibreTiny's partition table has no BLE bonding entry (esphome#18646,
|
||||
libretiny-eu/libretiny#408). Known non-5.x families and BK7238 are rejected in
|
||||
to_code. Unknown families are capability-checked at compile time via
|
||||
`__has_include("app_ble.h")`, a header only on the BLE 5.x include path
|
||||
(ble_api.h ships for every SoC, so it cannot be the probe). A non-5.x build
|
||||
fails with a clear #error.
|
||||
@@ -65,6 +68,14 @@ def _unsupported_family_message(family: str) -> str | None:
|
||||
)
|
||||
if family == FAMILY_BK7231Q:
|
||||
return "bk72xx_ble does not support BK7231Q: this SoC has no BLE"
|
||||
if family == FAMILY_BK7238:
|
||||
return (
|
||||
"bk72xx_ble is disabled on BK7238: with BLE compiled in, the Beken SDK "
|
||||
"erases the bootloader flash sector at boot and the device can no longer "
|
||||
"start (see https://github.com/esphome/esphome/issues/18646); support "
|
||||
"returns once the LibreTiny partition table fix "
|
||||
"(libretiny-eu/libretiny#408) is released"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@@ -113,18 +124,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
# BK7231N, but NOT on BK7238 (its BLE stack has no such symbol; the address is
|
||||
# derived from the WiFi MAC instead — the BDK's own fallback). Tell the C++
|
||||
# which path is available so it doesn't reference a missing symbol.
|
||||
family = libretiny.get_libretiny_family()
|
||||
if family == FAMILY_BK7231N:
|
||||
if libretiny.get_libretiny_family() == FAMILY_BK7231N:
|
||||
cg.add_define("BK72XX_BLE_HAS_COMMON_BDADDR")
|
||||
elif family == FAMILY_BK7238:
|
||||
# ESPHome's LibreTiny disables BLE on BK7238 because the SDK can hang at
|
||||
# WiFi STA startup when BLE init runs. This component re-enables BLE, so
|
||||
# warn loudly: BK7238 is accepted but not hardware-verified and may be
|
||||
# WiFi-unstable with BLE on.
|
||||
_LOGGER.warning(
|
||||
"bk72xx_ble on BK7238: enabling BLE is known to risk a WiFi STA startup "
|
||||
"hang on this family and is not yet hardware-verified. Expect possible "
|
||||
"instability."
|
||||
)
|
||||
|
||||
cg.add_define("USE_BK72XX_BLE")
|
||||
|
||||
@@ -206,32 +206,36 @@ def validate_scan_parameters(config: ConfigType) -> ConfigType:
|
||||
interval = config[CONF_INTERVAL]
|
||||
window = config[CONF_WINDOW]
|
||||
|
||||
if window > interval:
|
||||
raise cv.Invalid(
|
||||
f"Scan window ({window}) needs to be smaller than scan interval ({interval})"
|
||||
)
|
||||
# Labels are reused in every error below; the optional one names its key.
|
||||
windows = [("Scan window", window)]
|
||||
if (connection_window := config.get(CONF_CONNECTION_SCAN_WINDOW)) is not None:
|
||||
windows.append((CONF_CONNECTION_SCAN_WINDOW, connection_window))
|
||||
|
||||
for name, value in windows:
|
||||
if value > interval:
|
||||
raise cv.Invalid(
|
||||
f"{name} ({value}) needs to be smaller than scan interval ({interval})"
|
||||
)
|
||||
|
||||
# BLE scan interval/window are programmed in 0.625 ms units as a 16-bit value; the
|
||||
# controller only accepts 2.5 ms .. 10240 ms (0x0004 .. 0x4000). Reject out-of-range
|
||||
# values here instead of letting the unit conversion silently overflow.
|
||||
for name, value in (("interval", interval), ("window", window)):
|
||||
for name, value in (("Scan interval", interval), *windows):
|
||||
if value.total_microseconds < 2500 or value.total_microseconds > 10_240_000:
|
||||
raise cv.Invalid(
|
||||
f"Scan {name} ({value}) must be between 2.5 ms and 10240 ms"
|
||||
)
|
||||
raise cv.Invalid(f"{name} ({value}) must be between 2.5 ms and 10240 ms")
|
||||
|
||||
# Validate what actually reaches the controller: both values are truncated to
|
||||
# whole 0.625 ms units, so a window/interval pair that differs by less than one
|
||||
# unit collapses to the same value — silently programming a 100 % duty cycle
|
||||
# (radio permanently on) from a config that asked for less.
|
||||
interval_units = to_ble_units(interval)
|
||||
window_units = to_ble_units(window)
|
||||
if window_units == interval_units and window < interval:
|
||||
raise cv.Invalid(
|
||||
f"Scan window ({window}) and interval ({interval}) both truncate to "
|
||||
f"{interval_units} x 0.625 ms, which the controller scans at a 100 % duty "
|
||||
f"cycle. Separate them by at least 0.625 ms."
|
||||
)
|
||||
for name, value in windows:
|
||||
if to_ble_units(value) == interval_units and value < interval:
|
||||
raise cv.Invalid(
|
||||
f"{name} ({value}) and interval ({interval}) both truncate to "
|
||||
f"{interval_units} x 0.625 ms, which the controller scans at a 100 % duty "
|
||||
f"cycle. Separate them by at least 0.625 ms."
|
||||
)
|
||||
|
||||
if interval.total_microseconds * 3 > duration.total_microseconds:
|
||||
raise cv.Invalid(
|
||||
@@ -247,11 +251,14 @@ def validate_scan_parameters(config: ConfigType) -> ConfigType:
|
||||
# their own; also the fallback for esp32's conditional default.
|
||||
DEFAULT_SCAN_WINDOW = "30ms"
|
||||
|
||||
CONF_CONNECTION_SCAN_WINDOW = "connection_scan_window"
|
||||
|
||||
|
||||
def scan_parameters_schema(
|
||||
interval_default: str,
|
||||
*,
|
||||
window_default: str | Callable[[], TimePeriod] = DEFAULT_SCAN_WINDOW,
|
||||
connection_window: bool = False,
|
||||
) -> cv.All:
|
||||
"""Build the scan_parameters value schema shared by all BLE trackers.
|
||||
|
||||
@@ -263,7 +270,9 @@ def scan_parameters_schema(
|
||||
can adjust it once sibling keys are resolved). The `active` option
|
||||
(default on) is unconditional: active scanning is part of the tracker
|
||||
contract — every current proxy client assumes it, so a passive-only
|
||||
tracker must not share this schema.
|
||||
tracker must not share this schema. connection_window opts in to the
|
||||
`connection_scan_window` option for trackers that can fall back to a
|
||||
smaller window while a GATT connection is active.
|
||||
"""
|
||||
schema = {
|
||||
cv.Optional(CONF_DURATION, default="5min"): cv.positive_time_period_seconds,
|
||||
@@ -272,6 +281,8 @@ def scan_parameters_schema(
|
||||
cv.Optional(CONF_CONTINUOUS, default=True): cv.boolean,
|
||||
cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
|
||||
}
|
||||
if connection_window:
|
||||
schema[cv.Optional(CONF_CONNECTION_SCAN_WINDOW)] = cv.positive_time_period
|
||||
return cv.All(cv.Schema(schema), validate_scan_parameters)
|
||||
|
||||
|
||||
|
||||
@@ -16,14 +16,15 @@ from esphome.const import (
|
||||
DEVICE_CLASS_RESTART,
|
||||
DEVICE_CLASS_UPDATE,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
|
||||
from esphome.types import ConfigType, SafeExpType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
@@ -88,7 +89,7 @@ _CALLBACK_AUTOMATIONS = (
|
||||
|
||||
|
||||
@setup_entity("button")
|
||||
async def setup_button_core_(var, config):
|
||||
async def setup_button_core_(var: MockObj, config: ConfigType) -> None:
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
setup_device_class(config)
|
||||
@@ -101,7 +102,7 @@ async def setup_button_core_(var, config):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
|
||||
|
||||
async def register_button(var, config):
|
||||
async def register_button(var: MockObj, config: ConfigType) -> None:
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
queue_entity_register("button", config)
|
||||
@@ -109,7 +110,7 @@ async def register_button(var, config):
|
||||
await setup_button_core_(var, config)
|
||||
|
||||
|
||||
async def new_button(config, *args):
|
||||
async def new_button(config: ConfigType, *args: SafeExpType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
await register_button(var, config)
|
||||
return var
|
||||
@@ -125,11 +126,16 @@ BUTTON_PRESS_SCHEMA = maybe_simple_id(
|
||||
@automation.register_action(
|
||||
"button.press", PressAction, BUTTON_PRESS_SCHEMA, synchronous=True
|
||||
)
|
||||
async def button_press_to_code(config, action_id, template_arg, args):
|
||||
async def button_press_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_global(button_ns.using)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
#include "captive_index.h"
|
||||
#include "scan_list.h"
|
||||
|
||||
namespace esphome::captive_portal {
|
||||
|
||||
@@ -33,8 +34,10 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
|
||||
// Invariant: only bounded in-memory work under the lock; the network send
|
||||
// happens later in request->send()
|
||||
wifi::ScanResultsLock lock(wifi::global_wifi_component);
|
||||
for (const auto &scan : wifi::global_wifi_component->get_scan_result()) {
|
||||
if (scan.get_is_hidden())
|
||||
const auto &results = wifi::global_wifi_component->get_scan_result();
|
||||
for (const auto &scan : results) {
|
||||
bool with_auth = false;
|
||||
if (!should_show_scan_entry(results, scan, with_auth))
|
||||
continue;
|
||||
|
||||
json_escape_into_buffer(escaped_ssid, scan.get_ssid());
|
||||
@@ -44,10 +47,10 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
|
||||
stream->print(ESPHOME_F("\",\"rssi\":"));
|
||||
stream->print(scan.get_rssi());
|
||||
stream->print(ESPHOME_F(",\"lock\":"));
|
||||
stream->print(scan.get_with_auth());
|
||||
stream->print(with_auth);
|
||||
stream->print(ESPHOME_F("}"));
|
||||
#else
|
||||
stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", escaped_ssid, scan.get_rssi(), scan.get_with_auth());
|
||||
stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", escaped_ssid, scan.get_rssi(), with_auth);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::captive_portal {
|
||||
|
||||
// A scan lists every BSSID, so one SSID can appear several times. Returns true for
|
||||
// the strongest entry per SSID (earliest on ties), never for hidden entries. scan
|
||||
// must be an element of results. with_auth is written only when returning true and
|
||||
// is set if any entry with that SSID needs a key. Templated for host tests.
|
||||
template<typename Results, typename Entry>
|
||||
bool should_show_scan_entry(const Results &results, const Entry &scan, bool &with_auth) {
|
||||
if (scan.get_is_hidden())
|
||||
return false;
|
||||
const int8_t rssi = scan.get_rssi();
|
||||
bool any_auth = false;
|
||||
for (const auto &other : results) {
|
||||
if (other.get_is_hidden() || !other.ssid_equals(scan))
|
||||
continue;
|
||||
// Same array, so address order is index order. scan fails both checks against itself.
|
||||
if (other.get_rssi() > rssi || (other.get_rssi() == rssi && &other < &scan))
|
||||
return false;
|
||||
any_auth |= other.get_with_auth();
|
||||
}
|
||||
with_auth = any_auth;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::captive_portal
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import mqtt, web_server
|
||||
@@ -48,13 +50,19 @@ from esphome.const import (
|
||||
CONF_VISUAL,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority
|
||||
from esphome.core import CORE, ID, CoroPriority, Lambda, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import LambdaExpression, MockObjClass
|
||||
from esphome.cpp_generator import (
|
||||
LambdaExpression,
|
||||
MockObj,
|
||||
MockObjClass,
|
||||
TemplateArgsType,
|
||||
)
|
||||
from esphome.types import ConfigType, SafeExpType
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
@@ -132,7 +140,7 @@ VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
def visual_temperature_step(value):
|
||||
def visual_temperature_step(value: Any) -> ConfigType:
|
||||
# Allow defining target/current temperature steps separately
|
||||
if isinstance(value, dict):
|
||||
return VISUAL_TEMPERATURE_STEP_SCHEMA(value)
|
||||
@@ -273,7 +281,7 @@ def climate_schema(
|
||||
|
||||
|
||||
@setup_entity("climate")
|
||||
async def setup_climate_core_(var, config):
|
||||
async def setup_climate_core_(var: MockObj, config: ConfigType) -> None:
|
||||
visual = config.get(CONF_VISUAL, {})
|
||||
if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None:
|
||||
cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES")
|
||||
@@ -443,7 +451,7 @@ async def setup_climate_core_(var, config):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
|
||||
|
||||
async def register_climate(var, config):
|
||||
async def register_climate(var: MockObj, config: ConfigType) -> None:
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
queue_entity_register("climate", config)
|
||||
@@ -451,7 +459,7 @@ async def register_climate(var, config):
|
||||
await setup_climate_core_(var, config)
|
||||
|
||||
|
||||
async def new_climate(config, *args):
|
||||
async def new_climate(config: ConfigType, *args: SafeExpType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
await register_climate(var, config)
|
||||
return var
|
||||
@@ -485,7 +493,12 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
|
||||
CLIMATE_CONTROL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def climate_control_to_code(config, action_id, template_arg, args):
|
||||
async def climate_control_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
|
||||
# All configured fields are folded into a single stateless lambda whose
|
||||
@@ -549,5 +562,5 @@ async def climate_control_to_code(config, action_id, template_arg, args):
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_global(climate_ns.using)
|
||||
|
||||
@@ -511,29 +511,6 @@ ClimateTraits Climate::get_traits() {
|
||||
return traits;
|
||||
}
|
||||
|
||||
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
|
||||
void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) {
|
||||
this->visual_min_temperature_override_ = visual_min_temperature_override;
|
||||
}
|
||||
|
||||
void Climate::set_visual_max_temperature_override(float visual_max_temperature_override) {
|
||||
this->visual_max_temperature_override_ = visual_max_temperature_override;
|
||||
}
|
||||
|
||||
void Climate::set_visual_temperature_step_override(float target, float current) {
|
||||
this->visual_target_temperature_step_override_ = target;
|
||||
this->visual_current_temperature_step_override_ = current;
|
||||
}
|
||||
|
||||
void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) {
|
||||
this->visual_min_humidity_override_ = visual_min_humidity_override;
|
||||
}
|
||||
|
||||
void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) {
|
||||
this->visual_max_humidity_override_ = visual_max_humidity_override;
|
||||
}
|
||||
#endif
|
||||
|
||||
ClimateCall Climate::make_call() { return ClimateCall(this); }
|
||||
|
||||
ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
|
||||
|
||||
@@ -228,11 +228,22 @@ class Climate : public EntityBase {
|
||||
ClimateTraits get_traits();
|
||||
|
||||
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
|
||||
void set_visual_min_temperature_override(float visual_min_temperature_override);
|
||||
void set_visual_max_temperature_override(float visual_max_temperature_override);
|
||||
void set_visual_temperature_step_override(float target, float current);
|
||||
void set_visual_min_humidity_override(float visual_min_humidity_override);
|
||||
void set_visual_max_humidity_override(float visual_max_humidity_override);
|
||||
void set_visual_min_temperature_override(float visual_min_temperature_override) {
|
||||
this->visual_min_temperature_override_ = visual_min_temperature_override;
|
||||
}
|
||||
void set_visual_max_temperature_override(float visual_max_temperature_override) {
|
||||
this->visual_max_temperature_override_ = visual_max_temperature_override;
|
||||
}
|
||||
void set_visual_temperature_step_override(float target, float current) {
|
||||
this->visual_target_temperature_step_override_ = target;
|
||||
this->visual_current_temperature_step_override_ = current;
|
||||
}
|
||||
void set_visual_min_humidity_override(float visual_min_humidity_override) {
|
||||
this->visual_min_humidity_override_ = visual_min_humidity_override;
|
||||
}
|
||||
void set_visual_max_humidity_override(float visual_max_humidity_override) {
|
||||
this->visual_max_humidity_override_ = visual_max_humidity_override;
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Set the supported custom fan modes (stored on Climate, referenced by ClimateTraits).
|
||||
|
||||
@@ -46,7 +46,7 @@ from esphome.core.entity_helpers import (
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import LambdaExpression, MockObj, MockObjClass
|
||||
from esphome.types import ConfigType, TemplateArgsType
|
||||
from esphome.types import ConfigType, SafeExpType, TemplateArgsType
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
@@ -162,7 +162,7 @@ _COVER_SCHEMA = (
|
||||
_COVER_SCHEMA.add_extra(entity_duplicate_validator("cover"))
|
||||
|
||||
|
||||
def _validate_mqtt_state_topics(config):
|
||||
def _validate_mqtt_state_topics(config: ConfigType) -> ConfigType:
|
||||
if config.get(CONF_MQTT_JSON_STATE_PAYLOAD):
|
||||
if CONF_POSITION_STATE_TOPIC in config:
|
||||
raise cv.Invalid(
|
||||
@@ -201,7 +201,7 @@ def cover_schema(
|
||||
|
||||
|
||||
@setup_entity("cover")
|
||||
async def setup_cover_core_(var, config):
|
||||
async def setup_cover_core_(var: MockObj, config: ConfigType) -> None:
|
||||
setup_device_class(config)
|
||||
|
||||
if CONF_ON_OPEN in config:
|
||||
@@ -235,7 +235,7 @@ async def setup_cover_core_(var, config):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
|
||||
|
||||
async def register_cover(var, config):
|
||||
async def register_cover(var: MockObj, config: ConfigType) -> None:
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
queue_entity_register("cover", config)
|
||||
@@ -243,7 +243,7 @@ async def register_cover(var, config):
|
||||
await setup_cover_core_(var, config)
|
||||
|
||||
|
||||
async def new_cover(config, *args):
|
||||
async def new_cover(config: ConfigType, *args: SafeExpType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
await register_cover(var, config)
|
||||
return var
|
||||
@@ -259,7 +259,12 @@ COVER_ACTION_SCHEMA = maybe_simple_id(
|
||||
@automation.register_action(
|
||||
"cover.open", OpenAction, COVER_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def cover_open_to_code(config, action_id, template_arg, args):
|
||||
async def cover_open_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
@@ -267,7 +272,12 @@ async def cover_open_to_code(config, action_id, template_arg, args):
|
||||
@automation.register_action(
|
||||
"cover.close", CloseAction, COVER_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def cover_close_to_code(config, action_id, template_arg, args):
|
||||
async def cover_close_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
@@ -275,7 +285,12 @@ async def cover_close_to_code(config, action_id, template_arg, args):
|
||||
@automation.register_action(
|
||||
"cover.stop", StopAction, COVER_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def cover_stop_to_code(config, action_id, template_arg, args):
|
||||
async def cover_stop_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
@@ -283,7 +298,12 @@ async def cover_stop_to_code(config, action_id, template_arg, args):
|
||||
@automation.register_action(
|
||||
"cover.toggle", ToggleAction, COVER_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def cover_toggle_to_code(config, action_id, template_arg, args):
|
||||
async def cover_toggle_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
@@ -421,5 +441,5 @@ automation.register_condition(
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_global(cover_ns.using)
|
||||
|
||||
@@ -135,10 +135,6 @@ CoverCall &CoverCall::set_stop(bool stop) {
|
||||
this->stop_ = stop;
|
||||
return *this;
|
||||
}
|
||||
bool CoverCall::get_stop() const { return this->stop_; }
|
||||
|
||||
CoverCall Cover::make_call() { return {this}; }
|
||||
|
||||
void Cover::publish_state(bool save) {
|
||||
this->position = clamp(this->position, 0.0f, 1.0f);
|
||||
this->tilt = clamp(this->tilt, 0.0f, 1.0f);
|
||||
@@ -184,9 +180,6 @@ optional<CoverRestoreState> Cover::restore_state_() {
|
||||
return recovered;
|
||||
}
|
||||
|
||||
bool Cover::is_fully_open() const { return this->position == COVER_OPEN; }
|
||||
bool Cover::is_fully_closed() const { return this->position == COVER_CLOSED; }
|
||||
|
||||
CoverCall CoverRestoreState::to_call(Cover *cover) {
|
||||
auto call = cover->make_call();
|
||||
auto traits = cover->get_traits();
|
||||
|
||||
@@ -50,7 +50,7 @@ class CoverCall {
|
||||
void perform();
|
||||
|
||||
const optional<float> &get_position() const;
|
||||
bool get_stop() const;
|
||||
bool get_stop() const { return this->stop_; }
|
||||
const optional<float> &get_tilt() const;
|
||||
const optional<bool> &get_toggle() const;
|
||||
|
||||
@@ -123,7 +123,7 @@ class Cover : public EntityBase {
|
||||
float tilt{COVER_OPEN};
|
||||
|
||||
/// Construct a new cover call used to control the cover.
|
||||
CoverCall make_call();
|
||||
CoverCall make_call() { return {this}; }
|
||||
|
||||
template<typename F> void add_on_state_callback(F &&f) { this->state_callback_.add(std::forward<F>(f)); }
|
||||
|
||||
@@ -139,9 +139,9 @@ class Cover : public EntityBase {
|
||||
virtual CoverTraits get_traits() = 0;
|
||||
|
||||
/// Helper method to check if the cover is fully open. Equivalent to comparing .position against 1.0
|
||||
bool is_fully_open() const;
|
||||
bool is_fully_open() const { return this->position == COVER_OPEN; }
|
||||
/// Helper method to check if the cover is fully closed. Equivalent to comparing .position against 0.0
|
||||
bool is_fully_closed() const;
|
||||
bool is_fully_closed() const { return this->position == COVER_CLOSED; }
|
||||
|
||||
protected:
|
||||
friend CoverCall;
|
||||
|
||||
@@ -2,6 +2,7 @@ import base64
|
||||
from pathlib import Path
|
||||
import re
|
||||
import secrets
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from ruamel.yaml import YAML
|
||||
@@ -13,6 +14,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ESPHOME, CONF_PROJECT, CONF_REF, CONF_WIFI
|
||||
import esphome.final_validate as fv
|
||||
from esphome.happy_eyeballs import ensure_happy_eyeballs
|
||||
from esphome.types import ConfigType
|
||||
from esphome.yaml_util import dump
|
||||
|
||||
dashboard_import_ns = cg.esphome_ns.namespace("dashboard_import")
|
||||
@@ -23,14 +25,14 @@ DEPENDENCIES = ["api"]
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
|
||||
def validate_import_url(value):
|
||||
def validate_import_url(value: Any) -> str:
|
||||
value = cv.string_strict(value)
|
||||
value = cv.Length(max=255)(value)
|
||||
validate_source_shorthand(value)
|
||||
return value
|
||||
|
||||
|
||||
def validate_full_url(config):
|
||||
def validate_full_url(config: ConfigType) -> ConfigType:
|
||||
if not config[CONF_IMPORT_FULL_CONFIG]:
|
||||
return config
|
||||
source = validate_source_shorthand(config[CONF_PACKAGE_IMPORT_URL])
|
||||
@@ -55,7 +57,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
full_config = fv.full_config.get()[CONF_ESPHOME]
|
||||
if CONF_PROJECT not in full_config:
|
||||
raise cv.Invalid(
|
||||
@@ -73,7 +75,7 @@ wifi:
|
||||
"""
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_DASHBOARD_IMPORT")
|
||||
url = config[CONF_PACKAGE_IMPORT_URL]
|
||||
if config[CONF_IMPORT_FULL_CONFIG]:
|
||||
|
||||
@@ -37,8 +37,6 @@ void DateEntity::publish_state() {
|
||||
#endif
|
||||
}
|
||||
|
||||
DateCall DateEntity::make_call() { return DateCall(this); }
|
||||
|
||||
void DateCall::validate_() {
|
||||
if (this->year_.has_value() && (this->year_ < 1970 || this->year_ > 3000)) {
|
||||
ESP_LOGE(TAG, "Year must be between 1970 and 3000");
|
||||
|
||||
@@ -96,6 +96,8 @@ class DateCall {
|
||||
optional<uint8_t> day_;
|
||||
};
|
||||
|
||||
inline DateCall DateEntity::make_call() { return DateCall(this); }
|
||||
|
||||
template<typename... Ts> class DateSetAction final : public Action<Ts...>, public Parented<DateEntity> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(ESPTime, date)
|
||||
|
||||
@@ -53,8 +53,6 @@ void DateTimeEntity::publish_state() {
|
||||
#endif
|
||||
}
|
||||
|
||||
DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); }
|
||||
|
||||
ESPTime DateTimeEntity::state_as_esptime() const {
|
||||
ESPTime obj;
|
||||
obj.year = this->year_;
|
||||
|
||||
@@ -121,6 +121,8 @@ class DateTimeCall {
|
||||
optional<uint8_t> second_;
|
||||
};
|
||||
|
||||
inline DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); }
|
||||
|
||||
template<typename... Ts> class DateTimeSetAction final : public Action<Ts...>, public Parented<DateTimeEntity> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(ESPTime, datetime)
|
||||
|
||||
@@ -33,8 +33,6 @@ void TimeEntity::publish_state() {
|
||||
#endif
|
||||
}
|
||||
|
||||
TimeCall TimeEntity::make_call() { return TimeCall(this); }
|
||||
|
||||
void TimeCall::validate_() {
|
||||
if (this->hour_.has_value() && this->hour_ > 23) {
|
||||
ESP_LOGE(TAG, "Hour must be between 0 and 23");
|
||||
|
||||
@@ -98,6 +98,8 @@ class TimeCall {
|
||||
optional<uint8_t> second_;
|
||||
};
|
||||
|
||||
inline TimeCall TimeEntity::make_call() { return TimeCall(this); }
|
||||
|
||||
template<typename... Ts> class TimeSetAction final : public Action<Ts...>, public Parented<TimeEntity> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(ESPTime, time)
|
||||
|
||||
@@ -12,6 +12,7 @@ from esphome.const import (
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["logger"]
|
||||
@@ -45,7 +46,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
if CORE.using_zephyr:
|
||||
zephyr_add_prj_conf("HWINFO", True)
|
||||
# gdb thread support
|
||||
|
||||
@@ -21,6 +21,7 @@ from esphome.const import (
|
||||
UNIT_MILLISECOND,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import ( # noqa: F401 pylint: disable=unused-import
|
||||
CONF_DEBUG_ID,
|
||||
@@ -111,7 +112,7 @@ CONFIG_SCHEMA = {
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
debug_component = await cg.get_variable(config[CONF_DEBUG_ID])
|
||||
|
||||
if free_conf := config.get(CONF_FREE):
|
||||
|
||||
@@ -7,6 +7,7 @@ from esphome.const import (
|
||||
ICON_CHIP,
|
||||
ICON_RESTART,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import ( # noqa: F401 pylint: disable=unused-import
|
||||
CONF_DEBUG_ID,
|
||||
@@ -33,7 +34,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
debug_component = await cg.get_variable(config[CONF_DEBUG_ID])
|
||||
|
||||
if CONF_DEVICE in config:
|
||||
|
||||
@@ -163,6 +163,11 @@ def validate_config(config: ConfigType) -> ConfigType:
|
||||
"You need to remove the global wakeup_pin_mode and define it per pin"
|
||||
)
|
||||
if wakeup_pins:
|
||||
if CONF_WAKEUP_PIN_MODE in wakeup_pins[0]:
|
||||
raise cv.Invalid(
|
||||
"Specify wakeup_pin_mode either at the top level under deep_sleep "
|
||||
"or under the pin entry, not both"
|
||||
)
|
||||
wakeup_pins[0][CONF_WAKEUP_PIN_MODE] = config.pop(CONF_WAKEUP_PIN_MODE)
|
||||
elif (
|
||||
isinstance(config.get(CONF_WAKEUP_PIN), list)
|
||||
|
||||
@@ -43,10 +43,6 @@ void DeepSleepComponent::loop() {
|
||||
this->begin_sleep();
|
||||
}
|
||||
|
||||
void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; }
|
||||
|
||||
void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; }
|
||||
|
||||
void DeepSleepComponent::begin_sleep(bool manual) {
|
||||
if (this->prevent_ && !manual) {
|
||||
this->next_enter_deep_sleep_ = true;
|
||||
@@ -76,8 +72,4 @@ void DeepSleepComponent::begin_sleep(bool manual) {
|
||||
|
||||
float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; }
|
||||
|
||||
void DeepSleepComponent::prevent_deep_sleep() { this->prevent_ = true; }
|
||||
|
||||
void DeepSleepComponent::allow_deep_sleep() { this->prevent_ = false; }
|
||||
|
||||
} // namespace esphome::deep_sleep
|
||||
|
||||
@@ -132,7 +132,7 @@ template<typename... Ts> class PreventDeepSleepAction;
|
||||
class DeepSleepComponent final : public Component {
|
||||
public:
|
||||
/// Set the duration in ms the component should sleep once it's in deep sleep mode.
|
||||
void set_sleep_duration(uint32_t time_ms);
|
||||
void set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; }
|
||||
#if defined(USE_ESP32)
|
||||
/** Set the pin to wake up to on the ESP32 once it's in deep sleep mode.
|
||||
* Use the inverted property to set the wakeup level.
|
||||
@@ -157,7 +157,7 @@ class DeepSleepComponent final : public Component {
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
void set_touch_wakeup(bool touch_wakeup);
|
||||
void set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; }
|
||||
#endif
|
||||
|
||||
// Set the duration in ms for how long the code should run before entering
|
||||
@@ -166,7 +166,7 @@ class DeepSleepComponent final : public Component {
|
||||
#endif // USE_ESP32
|
||||
|
||||
/// Set a duration in ms for how long the code should run before entering deep sleep mode.
|
||||
void set_run_duration(uint32_t time_ms);
|
||||
void set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; }
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
@@ -176,8 +176,8 @@ class DeepSleepComponent final : public Component {
|
||||
/// Helper to enter deep sleep mode
|
||||
void begin_sleep(bool manual = false);
|
||||
|
||||
void prevent_deep_sleep();
|
||||
void allow_deep_sleep();
|
||||
void prevent_deep_sleep() { this->prevent_ = true; }
|
||||
void allow_deep_sleep() { this->prevent_ = false; }
|
||||
|
||||
protected:
|
||||
// Returns nullopt if no run duration is set. Otherwise, returns the run
|
||||
|
||||
@@ -74,12 +74,6 @@ void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) {
|
||||
void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; }
|
||||
#endif
|
||||
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; }
|
||||
#endif
|
||||
|
||||
void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) {
|
||||
wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration;
|
||||
}
|
||||
|
||||
@@ -685,9 +685,6 @@ void Display::show_page(DisplayPage *page) {
|
||||
}
|
||||
}
|
||||
|
||||
void Display::show_next_page() { this->page_->show_next(); }
|
||||
void Display::show_prev_page() { this->page_->show_prev(); }
|
||||
|
||||
void Display::do_update_() {
|
||||
if (this->auto_clear_enabled_) {
|
||||
this->clear();
|
||||
@@ -892,9 +889,6 @@ void DisplayPage::show_prev() {
|
||||
this->prev_->show();
|
||||
}
|
||||
|
||||
void DisplayPage::set_parent(Display *parent) { this->parent_ = parent; }
|
||||
void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; }
|
||||
void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; }
|
||||
const display_writer_t &DisplayPage::get_writer() const { return this->writer_; }
|
||||
|
||||
const LogString *text_align_to_string(TextAlign textalign) {
|
||||
|
||||
@@ -802,9 +802,9 @@ class DisplayPage final {
|
||||
void show();
|
||||
void show_next();
|
||||
void show_prev();
|
||||
void set_parent(Display *parent);
|
||||
void set_prev(DisplayPage *prev);
|
||||
void set_next(DisplayPage *next);
|
||||
void set_parent(Display *parent) { this->parent_ = parent; }
|
||||
void set_prev(DisplayPage *prev) { this->prev_ = prev; }
|
||||
void set_next(DisplayPage *next) { this->next_ = next; }
|
||||
const display_writer_t &get_writer() const;
|
||||
|
||||
protected:
|
||||
@@ -814,6 +814,9 @@ class DisplayPage final {
|
||||
DisplayPage *next_{nullptr};
|
||||
};
|
||||
|
||||
inline void Display::show_next_page() { this->page_->show_next(); }
|
||||
inline void Display::show_prev_page() { this->page_->show_prev(); }
|
||||
|
||||
template<typename... Ts> class DisplayPageShowAction final : public Action<Ts...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(DisplayPage *, page)
|
||||
|
||||
@@ -12,6 +12,7 @@ from typing import Any
|
||||
from esphome import yaml_util
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_ENABLE_OTA_DOWNGRADE_PROTECTION
|
||||
from esphome.config_helpers import filter_source_files_from_defines
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADVANCED,
|
||||
@@ -233,6 +234,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
|
||||
"esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch
|
||||
"esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component
|
||||
"esp_eth", # Ethernet driver - only needed by ethernet component
|
||||
"esp_gdbstub", # GDB stub panic handler - unused by ESPHome; bt pulls it back
|
||||
"esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality
|
||||
"esp_http_client", # HTTP client - only needed by http_request component
|
||||
"esp_https_ota", # ESP-IDF HTTPS OTA - ESPHome has its own OTA implementation
|
||||
@@ -3450,3 +3452,10 @@ def process_stacktrace(config, line, backtrace_state):
|
||||
_decode_pc(config, addr.group())
|
||||
|
||||
return backtrace_state
|
||||
|
||||
|
||||
# gpio.cpp only implements ESP32InternalGPIOPin and its ISR helpers, which
|
||||
# are instantiated solely by the pin schema codegen (esp32_pin_to_code)
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_defines(
|
||||
{"gpio.cpp": "USE_ESP32_INTERNAL_GPIO"}
|
||||
)
|
||||
|
||||
@@ -124,6 +124,15 @@ static uint8_t IRAM_ATTR capture_riscv_backtrace(RvExcFrame *frame, uint32_t *ou
|
||||
// Version is uint32_t because it would be padded to 4 bytes anyway before the next
|
||||
// uint32_t field, so we use the full width rather than wasting 3 bytes of padding.
|
||||
static constexpr uint32_t CRASH_DATA_VERSION = 4;
|
||||
#if CONFIG_IDF_TARGET_ARCH_XTENSA
|
||||
// EXCCAUSE is a 6-bit register; larger recorded values mean the frame's
|
||||
// cause/vaddr slots were never written (not a real exception frame).
|
||||
static constexpr uint32_t XTENSA_EXCCAUSE_COUNT = XCHAL_EXCCAUSE_NUM;
|
||||
#elif CONFIG_IDF_TARGET_ARCH_RISCV
|
||||
// Synchronous mcause exception codes are small and have no interrupt bit;
|
||||
// anything else in a non-pseudo record is a stale slot.
|
||||
static constexpr uint32_t RISCV_EXCEPTION_CAUSE_COUNT = 32;
|
||||
#endif
|
||||
struct RawCrashData {
|
||||
uint32_t version;
|
||||
uint32_t magic;
|
||||
@@ -198,10 +207,28 @@ void crash_handler_clear() {
|
||||
s_raw_crash_data.magic = 0;
|
||||
}
|
||||
|
||||
// Whether the cause slot was written by a real exception frame.
|
||||
static bool cause_slot_was_written() {
|
||||
#if CONFIG_IDF_TARGET_ARCH_XTENSA
|
||||
return s_raw_crash_data.cause < XTENSA_EXCCAUSE_COUNT;
|
||||
#else
|
||||
return s_raw_crash_data.cause < RISCV_EXCEPTION_CAUSE_COUNT;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Look up the exception cause as a human-readable string.
|
||||
// Tables mirror ESP-IDF's panic_arch_fill_info() which uses local static arrays
|
||||
// not exposed via any public API.
|
||||
static const char *get_exception_reason() {
|
||||
uint8_t exception = s_raw_crash_data.exception;
|
||||
if (exception == PANIC_EXCEPTION_ABORT || exception == PANIC_EXCEPTION_TWDT) {
|
||||
// Abort-class panics carry no cause register
|
||||
return nullptr;
|
||||
}
|
||||
if (!cause_slot_was_written()) {
|
||||
// Garbage from old-build or corrupt records; report just the type
|
||||
return nullptr;
|
||||
}
|
||||
#if CONFIG_IDF_TARGET_ARCH_XTENSA
|
||||
if (s_raw_crash_data.pseudo_excause) {
|
||||
// SoC-level panic: watchdog, cache error, etc.
|
||||
@@ -354,10 +381,11 @@ static const char *const FAULT_ADDR_REG = "MTVAL";
|
||||
static const char *const FAULT_ADDR_REG_LOWER = "mtval";
|
||||
#endif
|
||||
|
||||
// Whether the fault address is meaningful — real CPU faults only, not
|
||||
// aborts/watchdogs or SoC-level pseudo exceptions.
|
||||
// Whether the fault address is meaningful: real CPU faults with a validly
|
||||
// written frame only.
|
||||
static bool has_fault_addr() {
|
||||
return s_raw_crash_data.exception == PANIC_EXCEPTION_FAULT && !s_raw_crash_data.pseudo_excause;
|
||||
return s_raw_crash_data.exception == PANIC_EXCEPTION_FAULT && !s_raw_crash_data.pseudo_excause &&
|
||||
cause_slot_was_written();
|
||||
}
|
||||
|
||||
// The record was captured by a different firmware build (it survives soft
|
||||
@@ -458,6 +486,10 @@ void crash_handler_log() {
|
||||
// into NOINIT memory before the normal panic handler runs.
|
||||
//
|
||||
extern "C" {
|
||||
// Set by IDF's task watchdog (task_wdt.c, no header) before it simulates an
|
||||
// abort; weak so builds without the task watchdog still link.
|
||||
extern bool g_twdt_isr __attribute__((weak));
|
||||
|
||||
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
// Names are mandated by the --wrap linker mechanism
|
||||
extern void __real_esp_panic_handler(panic_info_t *info);
|
||||
@@ -470,6 +502,14 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) {
|
||||
s_raw_crash_data.exception = (uint8_t) info->exception;
|
||||
s_raw_crash_data.pseudo_excause = info->pseudo_excause ? 1 : 0;
|
||||
s_raw_crash_data.crashed_core = (uint8_t) info->core;
|
||||
if (g_panic_abort) {
|
||||
// IDF reclassifies to ABORT only inside esp_panic_handler(), after this
|
||||
// wrapper captured info->exception; correct it here. TWDT is our own
|
||||
// distinction (IDF never assigns PANIC_EXCEPTION_TWDT). The abort text is
|
||||
// not stored; the symbolized backtrace already identifies the site.
|
||||
bool is_twdt = &g_twdt_isr != nullptr && g_twdt_isr;
|
||||
s_raw_crash_data.exception = (uint8_t) (is_twdt ? PANIC_EXCEPTION_TWDT : PANIC_EXCEPTION_ABORT);
|
||||
}
|
||||
// Zero unconditionally so a null frame doesn't leave stale .noinit data from a previous boot
|
||||
s_raw_crash_data.cause = 0;
|
||||
s_raw_crash_data.fault_addr = 0;
|
||||
@@ -487,8 +527,12 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) {
|
||||
// Xtensa: walk the backtrace using the public API
|
||||
if (info->frame != nullptr) {
|
||||
auto *xt_frame = (XtExcFrame *) info->frame;
|
||||
s_raw_crash_data.cause = xt_frame->exccause;
|
||||
s_raw_crash_data.fault_addr = xt_frame->excvaddr;
|
||||
if (!g_panic_abort) {
|
||||
// Abort-class frames carry no useful cause/vaddr: TWDT task snapshots
|
||||
// never wrote them and abort() traps describe only the synthetic trap.
|
||||
s_raw_crash_data.cause = xt_frame->exccause;
|
||||
s_raw_crash_data.fault_addr = xt_frame->excvaddr;
|
||||
}
|
||||
s_raw_crash_data.backtrace_count = walk_xtensa_backtrace(xt_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE);
|
||||
}
|
||||
|
||||
@@ -510,8 +554,11 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) {
|
||||
// RISC-V: capture MEPC + RA, then scan stack for code addresses
|
||||
if (info->frame != nullptr) {
|
||||
auto *rv_frame = (RvExcFrame *) info->frame;
|
||||
s_raw_crash_data.cause = rv_frame->mcause;
|
||||
s_raw_crash_data.fault_addr = rv_frame->mtval;
|
||||
if (!g_panic_abort) {
|
||||
// See the Xtensa branch: abort-class frames carry no valid cause/vaddr.
|
||||
s_raw_crash_data.cause = rv_frame->mcause;
|
||||
s_raw_crash_data.fault_addr = rv_frame->mtval;
|
||||
}
|
||||
s_raw_crash_data.backtrace_count =
|
||||
capture_riscv_backtrace(rv_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE, &s_raw_crash_data.reg_frame_count);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#ifdef USE_ESP32
|
||||
#include "esphome/core/defines.h"
|
||||
// Also defines the core ISRInternalGPIOPin methods; those are only reachable
|
||||
// via ESP32InternalGPIOPin::to_isr(), so the same define gates both safely.
|
||||
#if defined(USE_ESP32) && defined(USE_ESP32_INTERNAL_GPIO)
|
||||
|
||||
#include "gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -204,4 +207,4 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32
|
||||
#endif // USE_ESP32 && USE_ESP32_INTERNAL_GPIO
|
||||
|
||||
@@ -257,6 +257,7 @@ ESP32_PIN_SCHEMA = cv.All(
|
||||
|
||||
@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP32, ESP32_PIN_SCHEMA)
|
||||
async def esp32_pin_to_code(config):
|
||||
cg.add_define("USE_ESP32_INTERNAL_GPIO")
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
num = config[CONF_NUMBER]
|
||||
cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}")))
|
||||
|
||||
@@ -643,8 +643,28 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
|
||||
App.wake_loop_threadsafe();
|
||||
return;
|
||||
|
||||
// Log the result of connection parameter updates: a peer can reject or
|
||||
// never answer an update, and without this the link silently stays on the
|
||||
// old parameters (visible only as unexplained supervision timeouts).
|
||||
case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: {
|
||||
if (param->update_conn_params.status != ESP_BT_STATUS_SUCCESS) {
|
||||
char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
format_mac_addr_upper(param->update_conn_params.bda, mac_s);
|
||||
ESP_LOGW(TAG, "[%s] Conn param update failed, status=%d", mac_s, param->update_conn_params.status);
|
||||
}
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
else {
|
||||
char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
format_mac_addr_upper(param->update_conn_params.bda, mac_s);
|
||||
ESP_LOGV(TAG, "[%s] Conn params updated: interval=%u (x1.25ms) latency=%u timeout=%u (x10ms)", mac_s,
|
||||
param->update_conn_params.conn_int, param->update_conn_params.latency,
|
||||
param->update_conn_params.timeout);
|
||||
}
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore these GAP events as they are not relevant for our use case
|
||||
case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:
|
||||
case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT:
|
||||
case ESP_GAP_BLE_PHY_UPDATE_COMPLETE_EVT: // BLE 5.0 PHY update complete
|
||||
case ESP_GAP_BLE_CHANNEL_SELECT_ALGORITHM_EVT: // BLE 5.0 channel selection algorithm
|
||||
|
||||
@@ -7,6 +7,7 @@ import logging
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import ble_device_base, esp32_ble, ota
|
||||
from esphome.components.ble_device_base import CONF_CONNECTION_SCAN_WINDOW
|
||||
from esphome.components.const import CONF_ON_SCAN_END, CONF_SCAN_PARAMETERS, CONF_WINDOW
|
||||
from esphome.components.esp32 import (
|
||||
add_idf_sdkconfig_option,
|
||||
@@ -73,8 +74,9 @@ def _get_required_features() -> set[BLEFeatures]:
|
||||
|
||||
# Slot counters sizing the tracker's StaticVector storage; one request per
|
||||
# registered listener or client.
|
||||
CLIENT_COUNT_DEFINE = "ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT"
|
||||
_request_listener_slot = cg.slot_counter("ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT")
|
||||
_request_client_slot = cg.slot_counter("ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT")
|
||||
_request_client_slot = cg.slot_counter(CLIENT_COUNT_DEFINE)
|
||||
|
||||
|
||||
def register_ble_features(features: set[BLEFeatures]) -> None:
|
||||
@@ -147,6 +149,7 @@ class TrackerData:
|
||||
"""Per-run validation state, namespaced under DOMAIN in CORE.data."""
|
||||
|
||||
scan_window_defaulted: bool = False
|
||||
connection_window_injected: bool = False
|
||||
|
||||
|
||||
def _get_data() -> TrackerData:
|
||||
@@ -175,17 +178,34 @@ def _raise_defaulted_scan_window(config: ConfigType) -> ConfigType:
|
||||
honors the window strictly (>= 5.5.5); without the arbiter a full-duty
|
||||
scan would starve wifi outright, and a user-set window is never touched.
|
||||
Raising to the interval cannot invalidate the already-validated
|
||||
parameters, so no re-validation is needed.
|
||||
parameters, so no re-validation is needed. The connection window is
|
||||
checked against the window here, after the raise.
|
||||
"""
|
||||
params = config[CONF_SCAN_PARAMETERS]
|
||||
if (
|
||||
_get_data().scan_window_defaulted
|
||||
and config.get(CONF_SOFTWARE_COEXISTENCE)
|
||||
and idf_version() >= IDF_SCAN_WINDOW_FIX_VERSION
|
||||
):
|
||||
params = config[CONF_SCAN_PARAMETERS]
|
||||
# Copy so the config dump shows a plain value instead of a YAML
|
||||
# anchor/alias pair pointing at the interval.
|
||||
params[CONF_WINDOW] = copy.copy(params[CONF_INTERVAL])
|
||||
# Arm the connection-time fallback unless the user set one. Injected
|
||||
# after validation; safe because it equals the validated window default.
|
||||
if CONF_CONNECTION_SCAN_WINDOW not in params:
|
||||
params[CONF_CONNECTION_SCAN_WINDOW] = cv.positive_time_period(
|
||||
ble_device_base.DEFAULT_SCAN_WINDOW
|
||||
)
|
||||
_get_data().connection_window_injected = True
|
||||
if (
|
||||
connection_window := params.get(CONF_CONNECTION_SCAN_WINDOW)
|
||||
) is not None and connection_window > params[CONF_WINDOW]:
|
||||
# A larger value would widen the scan during connections.
|
||||
raise cv.Invalid(
|
||||
f"{CONF_CONNECTION_SCAN_WINDOW} ({connection_window}) needs to be "
|
||||
f"smaller than the scan window ({params[CONF_WINDOW]})",
|
||||
path=[CONF_SCAN_PARAMETERS, CONF_CONNECTION_SCAN_WINDOW],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
@@ -194,7 +214,7 @@ def _raise_defaulted_scan_window(config: ConfigType) -> ConfigType:
|
||||
# window/interval pairs that collapse to the same 0.625 ms unit count.
|
||||
# The window default is conditional (see _scan_window_default above).
|
||||
SCAN_PARAMETERS_SCHEMA = ble_device_base.scan_parameters_schema(
|
||||
"320ms", window_default=_scan_window_default
|
||||
"320ms", window_default=_scan_window_default, connection_window=True
|
||||
)
|
||||
|
||||
# Codegen helpers are owned by ble_device_base; kept under the historical names
|
||||
@@ -288,6 +308,25 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_scan_duration(params[CONF_DURATION]))
|
||||
cg.add(var.set_scan_interval(ble_device_base.to_ble_units(params[CONF_INTERVAL])))
|
||||
cg.add(var.set_scan_window(ble_device_base.to_ble_units(params[CONF_WINDOW])))
|
||||
if (connection_window := params.get(CONF_CONNECTION_SCAN_WINDOW)) is not None:
|
||||
# Emitted at FINAL so a scan-only build, where the guarded C++ path
|
||||
# compiles out, skips the call entirely.
|
||||
window_units = ble_device_base.to_ble_units(connection_window)
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _emit_connection_scan_window() -> None:
|
||||
if cg.get_slot_count(CLIENT_COUNT_DEFINE):
|
||||
cg.add(var.set_connection_scan_window(window_units))
|
||||
elif not _get_data().connection_window_injected:
|
||||
# Warn only for a user-set value; the injected default drops silently.
|
||||
_LOGGER.warning(
|
||||
"'%s' has no effect because this build has no BLE client "
|
||||
"components (for example bluetooth_proxy with active "
|
||||
"connections, or ble_client)",
|
||||
CONF_CONNECTION_SCAN_WINDOW,
|
||||
)
|
||||
|
||||
CORE.add_job(_emit_connection_scan_window)
|
||||
cg.add(var.set_scan_active(params[CONF_ACTIVE]))
|
||||
cg.add(var.set_scan_continuous(params[CONF_CONTINUOUS]))
|
||||
|
||||
|
||||
@@ -122,6 +122,9 @@ void ESP32BLETracker::loop() {
|
||||
// - start_scan_(): scanner_state_ becomes IDLE via set_scanner_state_() in cleanup_scan_state_()
|
||||
// - try_promote_discovered_clients_(): client enters DISCOVERED via set_state(), or
|
||||
// connecting client finishes (state change), or scanner reaches RUNNING/IDLE
|
||||
// - connection-window restart: scan_params_ is only written in start_scan_()
|
||||
// (which changes scanner state via set_scanner_state_()), and
|
||||
// counts.active/disconnecting only change on client state changes
|
||||
//
|
||||
// All conditions that affect the logic below are tied to state changes that increment
|
||||
// state_version_, so the fast path is safe.
|
||||
@@ -144,6 +147,19 @@ void ESP32BLETracker::loop() {
|
||||
(this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
|
||||
this->handle_scanner_failure_();
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
// The programmed window no longer matches the connection state (typically
|
||||
// the last connection dropped): restart so the right window applies now
|
||||
// instead of at the end of the scan period. Continuous only (a user-started
|
||||
// scan would not restart); !disconnecting matches the restart gate below.
|
||||
if (this->scanner_state_ == ScannerState::RUNNING && this->scan_continuous_ && !counts.disconnecting &&
|
||||
this->scan_params_.scan_window != this->desired_scan_window_(counts.active)) {
|
||||
// Same logical scan period continues: no on_scan_end sweeps for this
|
||||
// restart. Only armed when the stop was issued.
|
||||
this->skip_next_scan_end_ = this->stop_scan_();
|
||||
}
|
||||
#endif
|
||||
/*
|
||||
|
||||
Avoid starting the scanner if:
|
||||
@@ -195,19 +211,23 @@ void ESP32BLETracker::stop_scan() {
|
||||
// reason at D themselves, and the user-facing stop action is deliberate.
|
||||
ESP_LOGV(TAG, "Stopping scan.");
|
||||
this->scan_continuous_ = false;
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
// The window-change restart is abandoned with continuous scanning.
|
||||
this->skip_next_scan_end_ = false;
|
||||
#endif
|
||||
this->stop_scan_();
|
||||
}
|
||||
|
||||
void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_(); }
|
||||
|
||||
void ESP32BLETracker::stop_scan_() {
|
||||
bool ESP32BLETracker::stop_scan_() {
|
||||
if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
|
||||
// IDLE means there is nothing to stop; STOPPING means a stop is already in
|
||||
// flight and will finish on its own. Neither is an error.
|
||||
if (this->scanner_state_ != ScannerState::IDLE && this->scanner_state_ != ScannerState::STOPPING) {
|
||||
ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
|
||||
}
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
// Reset timeout state machine when stopping scan
|
||||
this->scan_timeout_state_ = ScanTimeoutState::INACTIVE;
|
||||
@@ -215,8 +235,9 @@ void ESP32BLETracker::stop_scan_() {
|
||||
esp_err_t err = esp_ble_gap_stop_scanning();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ble_gap_stop_scanning failed: %d", err);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ESP32BLETracker::start_scan_(bool first) {
|
||||
@@ -230,16 +251,11 @@ void ESP32BLETracker::start_scan_(bool first) {
|
||||
}
|
||||
this->set_scanner_state_(ScannerState::STARTING);
|
||||
ESP_LOGV(TAG, "Starting scan, set scanner state to STARTING.");
|
||||
if (!first) {
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
|
||||
for (auto *listener : this->listeners_)
|
||||
listener->on_scan_end();
|
||||
if (!first)
|
||||
this->notify_scan_end_();
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
this->skip_next_scan_end_ = false;
|
||||
#endif
|
||||
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
|
||||
for (auto *listener : this->neutral_listeners_)
|
||||
listener->on_scan_end();
|
||||
#endif
|
||||
}
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
this->discovered_log_.clear();
|
||||
#endif
|
||||
@@ -247,7 +263,17 @@ void ESP32BLETracker::start_scan_(bool first) {
|
||||
this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC;
|
||||
this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL;
|
||||
this->scan_params_.scan_interval = this->scan_interval_;
|
||||
this->scan_params_.scan_window = this->scan_window_;
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
// Count fresh: an automation can start a scan before loop() refreshes the counts.
|
||||
const uint32_t window = this->desired_scan_window_(this->count_client_states_().active);
|
||||
if (window != this->scan_window_) {
|
||||
// Guarantee the connection airtime instead of scanning wall to wall.
|
||||
ESP_LOGV(TAG, "Connection active, using %" PRIu32 " unit scan window", window);
|
||||
}
|
||||
#else
|
||||
const uint32_t window = this->scan_window_;
|
||||
#endif
|
||||
this->scan_params_.scan_window = window;
|
||||
|
||||
// Start timeout monitoring in loop() instead of using scheduler
|
||||
// This prevents false reboots when the loop is blocked
|
||||
@@ -408,6 +434,11 @@ void ESP32BLETracker::dump_config() {
|
||||
" Continuous Scanning: %s",
|
||||
this->scan_duration_, this->scan_interval_ * 0.625f, this->scan_window_ * 0.625f,
|
||||
this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_));
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
if (this->connection_scan_window_ != 0) {
|
||||
ESP_LOGCONFIG(TAG, " Connection Scan Window: %.1f ms", this->connection_scan_window_ * 0.625f);
|
||||
}
|
||||
#endif
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Scanner State: %s\n"
|
||||
" Connecting: %d, discovered: %d, disconnecting: %d, active: %d",
|
||||
@@ -487,6 +518,18 @@ void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
|
||||
// Reset timeout state machine instead of cancelling scheduler timeout
|
||||
this->scan_timeout_state_ = ScanTimeoutState::INACTIVE;
|
||||
|
||||
this->notify_scan_end_();
|
||||
|
||||
this->set_scanner_state_(ScannerState::IDLE);
|
||||
}
|
||||
|
||||
void ESP32BLETracker::notify_scan_end_() {
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
// Window-change restart continues the same scan period; the flag stays set
|
||||
// across the stop and is cleared by the restart in start_scan_.
|
||||
if (this->skip_next_scan_end_)
|
||||
return;
|
||||
#endif
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
|
||||
for (auto *listener : this->listeners_)
|
||||
listener->on_scan_end();
|
||||
@@ -495,8 +538,6 @@ void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
|
||||
for (auto *listener : this->neutral_listeners_)
|
||||
listener->on_scan_end();
|
||||
#endif
|
||||
|
||||
this->set_scanner_state_(ScannerState::IDLE);
|
||||
}
|
||||
|
||||
void ESP32BLETracker::handle_scanner_failure_() {
|
||||
@@ -534,6 +575,8 @@ void ESP32BLETracker::try_promote_discovered_clients_() {
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Promoting client to connect");
|
||||
// A connect ends the scan period a window-change restart was continuing.
|
||||
this->skip_next_scan_end_ = false;
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
this->update_coex_preference_(true);
|
||||
#endif
|
||||
|
||||
@@ -169,6 +169,9 @@ class ESP32BLETracker final : public Component,
|
||||
void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; }
|
||||
void set_scan_interval(uint32_t scan_interval) { scan_interval_ = scan_interval; }
|
||||
void set_scan_window(uint32_t scan_window) { scan_window_ = scan_window; }
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
void set_connection_scan_window(uint32_t scan_window) { connection_scan_window_ = scan_window; }
|
||||
#endif
|
||||
void set_scan_active(bool scan_active) { scan_active_ = scan_active; }
|
||||
bool get_scan_active() const { return scan_active_; }
|
||||
void set_scan_continuous(bool scan_continuous) { scan_continuous_ = scan_continuous; }
|
||||
@@ -226,7 +229,10 @@ class ESP32BLETracker final : public Component,
|
||||
ScannerState get_scanner_state() const { return this->scanner_state_; }
|
||||
|
||||
protected:
|
||||
void stop_scan_();
|
||||
/// Returns true when a stop was issued to the controller.
|
||||
bool stop_scan_();
|
||||
/// Fire on_scan_end on every listener unless a window-change restart suppressed it.
|
||||
void notify_scan_end_();
|
||||
/// Start a single scan by setting up the parameters and doing some esp-idf calls.
|
||||
void start_scan_(bool first);
|
||||
/// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received.
|
||||
@@ -313,6 +319,15 @@ class ESP32BLETracker final : public Component,
|
||||
uint32_t scan_duration_;
|
||||
uint32_t scan_interval_;
|
||||
uint32_t scan_window_;
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
/// Window used while a GATT connection is active; set by the user, or
|
||||
/// defaulted when the window was raised to full duty (0 = no fallback).
|
||||
uint32_t connection_scan_window_{0};
|
||||
/// The window to scan at for the given number of active GATT connections.
|
||||
uint32_t desired_scan_window_(uint8_t active) const {
|
||||
return (this->connection_scan_window_ != 0 && active > 0) ? this->connection_scan_window_ : this->scan_window_;
|
||||
}
|
||||
#endif
|
||||
esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
|
||||
esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
|
||||
|
||||
@@ -330,15 +345,20 @@ class ESP32BLETracker final : public Component,
|
||||
/// state_version_ to detect if any state changed since last iteration.
|
||||
uint8_t last_processed_version_{0};
|
||||
ScannerState scanner_state_{ScannerState::IDLE};
|
||||
bool scan_continuous_;
|
||||
bool scan_active_;
|
||||
// Packed 1-bit flags.
|
||||
bool scan_continuous_ : 1;
|
||||
bool scan_active_ : 1;
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
bool scan_continuous_before_ota_{false};
|
||||
bool scan_continuous_before_ota_ : 1 {false};
|
||||
#endif
|
||||
bool ble_was_disabled_ : 1 {true};
|
||||
bool parse_advertisements_ : 1 {false};
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
/// Suppress the window-change restart's on_scan_end sweeps (stop and start).
|
||||
bool skip_next_scan_end_ : 1 {false};
|
||||
#endif
|
||||
bool ble_was_disabled_{true};
|
||||
bool parse_advertisements_{false};
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
bool coex_prefer_ble_{false};
|
||||
bool coex_prefer_ble_ : 1 {false};
|
||||
#endif
|
||||
// Scan timeout state machine
|
||||
enum class ScanTimeoutState : uint8_t {
|
||||
@@ -346,10 +366,10 @@ class ESP32BLETracker final : public Component,
|
||||
MONITORING, // Actively monitoring for timeout
|
||||
EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot
|
||||
};
|
||||
ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE};
|
||||
uint32_t scan_start_time_{0};
|
||||
/// Precomputed timeout value: scan_duration_ * 2000
|
||||
uint32_t scan_timeout_ms_{0};
|
||||
ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE};
|
||||
};
|
||||
|
||||
// NOLINTNEXTLINE
|
||||
|
||||
@@ -3,6 +3,7 @@ from pathlib import Path
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
@@ -31,6 +32,7 @@ from esphome.core import (
|
||||
from esphome.core.config import BOARD_MAX_LENGTH
|
||||
from esphome.helpers import IS_MACOS, copy_file_if_changed
|
||||
from esphome.platformio.toolchain import copy_ccache_script
|
||||
from esphome.storage_json import StorageJSON
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .boards import BOARDS, ESP8266_LD_SCRIPTS
|
||||
@@ -88,7 +90,7 @@ def lambdas_use_scanf_float(config: ConfigType) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def set_core_data(config):
|
||||
def set_core_data(config: ConfigType) -> ConfigType:
|
||||
CORE.data[KEY_ESP8266] = {}
|
||||
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP8266
|
||||
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino"
|
||||
@@ -102,7 +104,7 @@ def set_core_data(config):
|
||||
return config
|
||||
|
||||
|
||||
def get_download_types(storage_json):
|
||||
def get_download_types(storage_json: StorageJSON) -> list[dict[str, str]]:
|
||||
"""Binary-download entries for a built ESP8266 firmware.
|
||||
|
||||
Used by device-builder (esphome/device-builder), via
|
||||
@@ -157,7 +159,7 @@ ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0)
|
||||
ARDUINO_4_PLATFORM_VERSION = cv.Version(4, 2, 1)
|
||||
|
||||
|
||||
def _arduino_check_versions(value):
|
||||
def _arduino_check_versions(value: ConfigType) -> ConfigType:
|
||||
value = value.copy()
|
||||
lookups = {
|
||||
"dev": (cv.Version(3, 1, 2), "https://github.com/esp8266/Arduino.git"),
|
||||
@@ -200,7 +202,7 @@ def _arduino_check_versions(value):
|
||||
return value
|
||||
|
||||
|
||||
def _parse_platform_version(value):
|
||||
def _parse_platform_version(value: Any) -> str:
|
||||
try:
|
||||
# if platform version is a valid version constraint, prefix the default package
|
||||
cv.platformio_version_constraint(value)
|
||||
@@ -275,7 +277,7 @@ def check_rosetta() -> None:
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.PLATFORM)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add(esp8266_ns.setup_preferences())
|
||||
|
||||
cg.add_platformio_option("lib_ldf_mode", "off")
|
||||
@@ -504,7 +506,7 @@ ESP8266_EXCEPTION_CODES = {
|
||||
}
|
||||
|
||||
|
||||
def _decode_pc(config, addr):
|
||||
def _decode_pc(config: ConfigType, addr: str) -> None:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
idedata = toolchain.get_idedata(config)
|
||||
@@ -525,7 +527,7 @@ def _decode_pc(config, addr):
|
||||
_LOGGER.warning("Decoded %s", translation)
|
||||
|
||||
|
||||
def _parse_register(config, regex, line):
|
||||
def _parse_register(config: ConfigType, regex: re.Pattern[str], line: str) -> None:
|
||||
match = regex.match(line)
|
||||
if match is not None:
|
||||
_decode_pc(config, match.group(1))
|
||||
@@ -549,7 +551,7 @@ STACKTRACE_BAD_ALLOC_RE = re.compile(
|
||||
STACKTRACE_ESP8266_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}")
|
||||
|
||||
|
||||
def process_stacktrace(config, line, backtrace_state):
|
||||
def process_stacktrace(config: ConfigType, line: str, backtrace_state: bool) -> bool:
|
||||
line = line.strip()
|
||||
# ESP8266 Exception type
|
||||
match = re.match(STACKTRACE_ESP8266_EXCEPTION_TYPE_RE, line)
|
||||
|
||||
@@ -118,8 +118,6 @@ static const LogString *get_exception_cause(uint32_t cause) {
|
||||
}
|
||||
|
||||
static const LogString *get_reset_reason(uint32_t reason) {
|
||||
if (reason == REASON_WDT_RST)
|
||||
return LOG_STR("Hardware WDT");
|
||||
if (reason == REASON_EXCEPTION_RST)
|
||||
return LOG_STR("Exception");
|
||||
if (reason == REASON_SOFT_WDT_RST)
|
||||
@@ -162,13 +160,20 @@ void crash_handler_log() {
|
||||
if (!is_crash_reason(resetInfo.reason))
|
||||
return;
|
||||
|
||||
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
|
||||
if (resetInfo.reason == REASON_WDT_RST) {
|
||||
// A hardware WDT reset happens entirely in hardware: the postmortem hook
|
||||
// never runs, so rst_info epc1/exccause and the RTC backtrace are
|
||||
// leftovers from an earlier crash. Don't misattribute them (#18596).
|
||||
ESP_LOGE(TAG, " Reason: Hardware WDT (no crash state is recorded for hardware WDT resets)");
|
||||
return;
|
||||
}
|
||||
|
||||
// Read and filter backtrace from RTC into stack-local buffer (no persistent RAM cost).
|
||||
// Both resetInfo and RTC data survive until the next reset, so this can be
|
||||
// called multiple times (logger init + API subscribe) with the same result.
|
||||
uint32_t backtrace[MAX_BACKTRACE];
|
||||
uint8_t bt_count = read_rtc_backtrace(backtrace, MAX_BACKTRACE);
|
||||
|
||||
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
|
||||
// GCC's ROM divide routine triggers IllegalInstruction (exccause=0) at specific
|
||||
// ROM addresses instead of IntegerDivideByZero (exccause=6). Patch to match
|
||||
// the Arduino core's postmortem handler behavior.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
@@ -18,6 +19,8 @@ from esphome.const import (
|
||||
PLATFORM_ESP8266,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import boards
|
||||
from .const import KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, esp8266_ns
|
||||
@@ -27,7 +30,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ESP8266GPIOPin = esp8266_ns.class_("ESP8266GPIOPin", cg.InternalGPIOPin)
|
||||
|
||||
|
||||
def _lookup_pin(value):
|
||||
def _lookup_pin(value: str) -> int:
|
||||
board = CORE.data[KEY_ESP8266][KEY_BOARD]
|
||||
board_pins = boards.ESP8266_BOARD_PINS.get(board, {})
|
||||
|
||||
@@ -42,7 +45,7 @@ def _lookup_pin(value):
|
||||
raise cv.Invalid(f"Cannot resolve pin name '{value}' for board {board}.")
|
||||
|
||||
|
||||
def _translate_pin(value):
|
||||
def _translate_pin(value: Any) -> int:
|
||||
if isinstance(value, dict) or value is None:
|
||||
raise cv.Invalid(
|
||||
"This variable only supports pin numbers, not full pin schemas "
|
||||
@@ -69,7 +72,7 @@ _ESP_SDIO_PINS = {
|
||||
}
|
||||
|
||||
|
||||
def validate_gpio_pin(value):
|
||||
def validate_gpio_pin(value: Any) -> int:
|
||||
value = _translate_pin(value)
|
||||
if value < 0 or value > 17:
|
||||
raise cv.Invalid(f"ESP8266: Invalid pin number: {value}")
|
||||
@@ -86,7 +89,7 @@ def validate_gpio_pin(value):
|
||||
return value
|
||||
|
||||
|
||||
def validate_supports(value):
|
||||
def validate_supports(value: ConfigType) -> ConfigType:
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_input = mode[CONF_INPUT]
|
||||
@@ -160,7 +163,7 @@ class PinInitialState:
|
||||
|
||||
|
||||
@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP8266, ESP8266_PIN_SCHEMA)
|
||||
async def esp8266_pin_to_code(config):
|
||||
async def esp8266_pin_to_code(config: ConfigType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
num = config[CONF_NUMBER]
|
||||
mode = config[CONF_MODE]
|
||||
@@ -192,7 +195,7 @@ async def esp8266_pin_to_code(config):
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.WORKAROUNDS)
|
||||
async def add_pin_initial_states_array():
|
||||
async def add_pin_initial_states_array() -> None:
|
||||
# Add includes at the very end, so that they override everything
|
||||
initial_states: list[PinInitialState] = CORE.data[KEY_ESP8266][
|
||||
KEY_PIN_INITIAL_STATES
|
||||
|
||||
@@ -398,7 +398,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
this->notify_state_(ota::OTA_STARTED, 0.0f, 0);
|
||||
#endif
|
||||
|
||||
// begin() may block for a few seconds while it locks flash.
|
||||
// begin() returns quickly; flash sectors are erased incrementally during write().
|
||||
error_code = this->backend_->begin(ota_size, ota_type);
|
||||
if (error_code != ota::OTA_RESPONSE_OK)
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
@@ -588,8 +588,6 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) {
|
||||
}
|
||||
|
||||
float ESPHomeOTAComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
|
||||
uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; }
|
||||
void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; }
|
||||
|
||||
void ESPHomeOTAComponent::log_socket_error_(const LogString *msg) {
|
||||
ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
|
||||
|
||||
@@ -39,14 +39,14 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
#endif // USE_OTA_PASSWORD
|
||||
|
||||
/// Manually set the port OTA should listen on
|
||||
void set_port(uint16_t port);
|
||||
void set_port(uint16_t port) { this->port_ = port; }
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void loop() override;
|
||||
|
||||
uint16_t get_port() const;
|
||||
uint16_t get_port() const { return this->port_; }
|
||||
|
||||
protected:
|
||||
void handle_handshake_();
|
||||
|
||||
@@ -10,14 +10,6 @@ EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-
|
||||
|
||||
EthernetComponent::EthernetComponent() { global_eth_component = this; }
|
||||
|
||||
float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; }
|
||||
|
||||
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
|
||||
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
|
||||
#endif
|
||||
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
void EthernetComponent::notify_ip_state_listeners_() {
|
||||
auto ips = this->get_ip_addresses();
|
||||
|
||||
@@ -125,7 +125,7 @@ class EthernetComponent final : public Component {
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
float get_setup_priority() const override { return setup_priority::ETHERNET; }
|
||||
void on_powerdown() override { powerdown(); }
|
||||
bool is_connected() { return this->state_ == EthernetComponentState::CONNECTED; }
|
||||
|
||||
@@ -146,9 +146,9 @@ class EthernetComponent final : public Component {
|
||||
esp_netif_t *get_esp_netif() { return this->eth_netif_; }
|
||||
#endif
|
||||
|
||||
void set_type(EthernetType type);
|
||||
void set_type(EthernetType type) { this->type_ = type; }
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
void set_manual_ip(const ManualIP &manual_ip);
|
||||
void set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
|
||||
#endif
|
||||
void set_fixed_mac(const std::array<uint8_t, MAC_ADDRESS_SIZE> &mac) { this->fixed_mac_ = mac; }
|
||||
|
||||
@@ -159,9 +159,6 @@ class EthernetComponent final : public Component {
|
||||
const char *get_use_address() const { return this->use_address_; }
|
||||
void set_use_address(const char *use_address) { this->use_address_ = use_address; }
|
||||
void get_eth_mac_address_raw(uint8_t *mac);
|
||||
// Remove before 2026.9.0
|
||||
ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0")
|
||||
std::string get_eth_mac_address_pretty();
|
||||
const char *get_eth_mac_address_pretty_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
|
||||
eth_duplex_t get_duplex_mode();
|
||||
eth_speed_t get_link_speed();
|
||||
@@ -171,35 +168,35 @@ class EthernetComponent final : public Component {
|
||||
esp_eth_handle_t get_eth_handle() const { return this->eth_handle_; }
|
||||
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
void set_clk_pin(uint8_t clk_pin);
|
||||
void set_miso_pin(uint8_t miso_pin);
|
||||
void set_mosi_pin(uint8_t mosi_pin);
|
||||
void set_cs_pin(uint8_t cs_pin);
|
||||
void set_interrupt_pin(uint8_t interrupt_pin);
|
||||
void set_reset_pin(uint8_t reset_pin);
|
||||
void set_clock_speed(int clock_speed);
|
||||
void set_interface(spi_host_device_t interface);
|
||||
void set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
|
||||
void set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
|
||||
void set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
|
||||
void set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
|
||||
void set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
void set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; }
|
||||
void set_interface(spi_host_device_t interface) { this->interface_ = interface; }
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
void set_polling_interval(uint32_t polling_interval);
|
||||
void set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; }
|
||||
#endif
|
||||
#else
|
||||
void set_phy_addr(uint8_t phy_addr);
|
||||
void set_power_pin(int power_pin);
|
||||
void set_mdc_pin(uint8_t mdc_pin);
|
||||
void set_mdio_pin(uint8_t mdio_pin);
|
||||
void set_clk_pin(uint8_t clk_pin);
|
||||
void set_clk_mode(emac_rmii_clock_mode_t clk_mode);
|
||||
void set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; }
|
||||
void set_power_pin(int power_pin) { this->power_pin_ = power_pin; }
|
||||
void set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; }
|
||||
void set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; }
|
||||
void set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; }
|
||||
void add_phy_register(PHYRegister register_value);
|
||||
#endif // USE_ETHERNET_SPI
|
||||
#endif // USE_ESP32
|
||||
|
||||
#ifdef USE_RP2
|
||||
void set_clk_pin(uint8_t clk_pin);
|
||||
void set_miso_pin(uint8_t miso_pin);
|
||||
void set_mosi_pin(uint8_t mosi_pin);
|
||||
void set_cs_pin(uint8_t cs_pin);
|
||||
void set_interrupt_pin(int8_t interrupt_pin);
|
||||
void set_reset_pin(int8_t reset_pin);
|
||||
void set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
|
||||
void set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
|
||||
void set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
|
||||
void set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
|
||||
void set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
#endif // USE_RP2
|
||||
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
|
||||
@@ -908,25 +908,7 @@ void EthernetComponent::dump_connect_params_() {
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
}
|
||||
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
|
||||
void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
|
||||
void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
|
||||
void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
|
||||
void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; }
|
||||
void EthernetComponent::set_interface(spi_host_device_t interface) { this->interface_ = interface; }
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; }
|
||||
#endif
|
||||
#else
|
||||
void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; }
|
||||
void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; }
|
||||
void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; }
|
||||
void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; }
|
||||
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; }
|
||||
#ifndef USE_ETHERNET_SPI
|
||||
void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); }
|
||||
#endif
|
||||
|
||||
@@ -946,11 +928,6 @@ void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
|
||||
ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error");
|
||||
}
|
||||
|
||||
std::string EthernetComponent::get_eth_mac_address_pretty() {
|
||||
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
return std::string(this->get_eth_mac_address_pretty_into_buffer(buf));
|
||||
}
|
||||
|
||||
const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
|
||||
std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
|
||||
uint8_t mac[MAC_ADDRESS_SIZE];
|
||||
|
||||
@@ -249,11 +249,6 @@ void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
|
||||
}
|
||||
}
|
||||
|
||||
std::string EthernetComponent::get_eth_mac_address_pretty() {
|
||||
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
return std::string(this->get_eth_mac_address_pretty_into_buffer(buf));
|
||||
}
|
||||
|
||||
const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
|
||||
std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
|
||||
uint8_t mac[MAC_ADDRESS_SIZE];
|
||||
@@ -355,13 +350,6 @@ void EthernetComponent::dump_connect_params_() {
|
||||
this->get_eth_mac_address_pretty_into_buffer(mac_buf));
|
||||
}
|
||||
|
||||
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
|
||||
void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
|
||||
void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
|
||||
void EthernetComponent::set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
|
||||
void EthernetComponent::set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
|
||||
void EthernetComponent::enable() {
|
||||
// RP2040 uses arduino-pico's LwipIntfDev which manages link state internally;
|
||||
// there is no clean enable/disable hook today. The YAML option is accepted on
|
||||
|
||||
@@ -153,11 +153,6 @@ void FanRestoreState::apply(Fan &fan) {
|
||||
fan.publish_state();
|
||||
}
|
||||
|
||||
FanCall Fan::turn_on() { return this->make_call().set_state(true); }
|
||||
FanCall Fan::turn_off() { return this->make_call().set_state(false); }
|
||||
FanCall Fan::toggle() { return this->make_call().set_state(!this->state); }
|
||||
FanCall Fan::make_call() { return FanCall(*this); }
|
||||
|
||||
const char *Fan::find_preset_mode_(const char *preset_mode) {
|
||||
return this->find_preset_mode_(preset_mode, preset_mode ? strlen(preset_mode) : 0);
|
||||
}
|
||||
|
||||
@@ -115,10 +115,10 @@ class Fan : public EntityBase {
|
||||
/// The current direction of the fan
|
||||
FanDirection direction{FanDirection::FORWARD};
|
||||
|
||||
FanCall turn_on();
|
||||
FanCall turn_off();
|
||||
FanCall toggle();
|
||||
FanCall make_call();
|
||||
FanCall turn_on() { return this->make_call().set_state(true); }
|
||||
FanCall turn_off() { return this->make_call().set_state(false); }
|
||||
FanCall toggle() { return this->make_call().set_state(!this->state); }
|
||||
FanCall make_call() { return FanCall(*this); }
|
||||
|
||||
/// Register a callback that will be called each time the state changes.
|
||||
template<typename F> void add_on_state_callback(F &&callback) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import io
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
@@ -75,12 +76,12 @@ def compute_local_image_path(value: str | ConfigType) -> Path:
|
||||
return external_files.compute_local_file_path(DOMAIN, url)
|
||||
|
||||
|
||||
def local_path(value):
|
||||
def local_path(value: str | ConfigType) -> str:
|
||||
value = value[CONF_PATH] if isinstance(value, dict) else value
|
||||
return str(CORE.relative_config_path(value))
|
||||
|
||||
|
||||
def download_file(url, path):
|
||||
def download_file(url: str, path: Path) -> str:
|
||||
# The shared NETWORK_TIMEOUT applies; a per-caller timeout would be
|
||||
# silently ignored on a per-run memo hit anyway (memos key by path).
|
||||
external_files.download_content(url, path)
|
||||
@@ -98,7 +99,7 @@ def download_gh_svg(value: str | ConfigType, source: str) -> str:
|
||||
return download_file(url, path)
|
||||
|
||||
|
||||
def download_image(value):
|
||||
def download_image(value: str | ConfigType) -> str:
|
||||
value = value[CONF_URL] if isinstance(value, dict) else value
|
||||
return download_file(value, compute_local_image_path(value))
|
||||
|
||||
@@ -146,7 +147,7 @@ def _extract_entry_ref(entry: ConfigType) -> RemoteFile | None:
|
||||
PREFETCH_FILES = external_files.single_stage_prefetch(_extract_entry_ref)
|
||||
|
||||
|
||||
def validate_file_shorthand(value):
|
||||
def validate_file_shorthand(value: Any) -> str:
|
||||
value = cv.string_strict(value)
|
||||
if (remote := _parse_remote_shorthand(value)) is not None:
|
||||
return download_file(remote.url, remote.path)
|
||||
@@ -163,8 +164,8 @@ LOCAL_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def mdi_schema(source):
|
||||
def validate_mdi(value):
|
||||
def mdi_schema(source: str) -> cv.All:
|
||||
def validate_mdi(value: ConfigType) -> str:
|
||||
return download_gh_svg(value, source)
|
||||
|
||||
return cv.All(
|
||||
@@ -259,7 +260,9 @@ async def new_image(config: ConfigType) -> MockObj:
|
||||
return var
|
||||
|
||||
|
||||
async def write_image(config, all_frames=False):
|
||||
async def write_image(
|
||||
config: ConfigType, all_frames: bool = False
|
||||
) -> tuple[MockObj, int, int, MockObj, MockObj, int]:
|
||||
path = Path(config[CONF_FILE])
|
||||
if not path.is_file():
|
||||
raise core.EsphomeError(f"Could not load image file {path}")
|
||||
|
||||
@@ -8,7 +8,8 @@ from esphome.const import (
|
||||
CONF_TYPE,
|
||||
CONF_VALUE,
|
||||
)
|
||||
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 = ["@esphome/core"]
|
||||
@@ -62,7 +63,7 @@ CONFIG_SCHEMA = _globals_schema
|
||||
|
||||
# Run with low priority so that namespaces are registered first
|
||||
@coroutine_with_priority(CoroPriority.LATE)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
type_ = cg.RawExpression(config[CONF_TYPE])
|
||||
restore = config[CONF_RESTORE_VALUE]
|
||||
|
||||
@@ -104,7 +105,12 @@ async def to_code(config):
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def globals_set_to_code(config, action_id, template_arg, args):
|
||||
async def globals_set_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
|
||||
template_arg = cg.TemplateArguments(full_id.type, *template_arg)
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
@@ -12,6 +12,7 @@ from esphome.const import (
|
||||
CONF_PIN,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import gpio_ns
|
||||
|
||||
@@ -68,7 +69,7 @@ def _pin_shared_only_with_deep_sleep(pin_num: int) -> bool:
|
||||
return any(path and path[0] == "deep_sleep" for path, _, _ in pin_users)
|
||||
|
||||
|
||||
def _final_validate(config) -> None:
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
use_interrupt = config[CONF_USE_INTERRUPT]
|
||||
if not use_interrupt:
|
||||
return
|
||||
@@ -124,7 +125,7 @@ def _final_validate(config) -> None:
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components.one_wire import OneWireBus
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_PIN
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import gpio_ns
|
||||
|
||||
@@ -18,7 +19,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import output
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_PIN
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import gpio_ns
|
||||
|
||||
@@ -16,7 +17,7 @@ CONFIG_SCHEMA = output.BINARY_OUTPUT_SCHEMA.extend(
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await output.register_output(var, config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INTERLOCK, CONF_PIN
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import gpio_ns
|
||||
|
||||
@@ -24,7 +25,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await switch.new_switch(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
from collections.abc import Callable, Iterable
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_INTERNAL
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@OttoWinter", "@esphome/core"]
|
||||
homeassistant_ns = cg.esphome_ns.namespace("homeassistant")
|
||||
|
||||
|
||||
def validate_entity_domain(platform, supported_domains):
|
||||
def validator(config):
|
||||
def validate_entity_domain(
|
||||
platform: str, supported_domains: Iterable[str]
|
||||
) -> Callable[[ConfigType], ConfigType]:
|
||||
def validator(config: ConfigType) -> ConfigType:
|
||||
domain = config[CONF_ENTITY_ID].split(".", 1)[0]
|
||||
if domain not in supported_domains:
|
||||
raise cv.Invalid(
|
||||
@@ -34,7 +40,7 @@ HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
def setup_home_assistant_entity(var, config):
|
||||
def setup_home_assistant_entity(var: MockObj, config: ConfigType) -> None:
|
||||
cg.add(var.set_entity_id(config[CONF_ENTITY_ID]))
|
||||
if CONF_ATTRIBUTE in config:
|
||||
cg.add(var.set_attribute(config[CONF_ATTRIBUTE]))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
HOME_ASSISTANT_IMPORT_SCHEMA,
|
||||
@@ -18,7 +19,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(HomeassistantBinarySensor).ex
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
setup_home_assistant_entity(var, config)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import number
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA,
|
||||
@@ -22,7 +23,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
var = await number.new_number(
|
||||
config,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
HOME_ASSISTANT_IMPORT_SCHEMA,
|
||||
@@ -18,7 +19,7 @@ CONFIG_SCHEMA = sensor.sensor_schema(HomeassistantSensor, accuracy_decimals=1).e
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
setup_home_assistant_entity(var, config)
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA,
|
||||
@@ -36,7 +37,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
HOME_ASSISTANT_IMPORT_SCHEMA,
|
||||
@@ -18,7 +19,7 @@ CONFIG_SCHEMA = text_sensor.text_sensor_schema(HomeassistantTextSensor).extend(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await text_sensor.new_text_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
setup_home_assistant_entity(var, config)
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import time as time_
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_TIMEZONE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import homeassistant_ns
|
||||
|
||||
@@ -16,7 +17,7 @@ CONFIG_SCHEMA = time_.TIME_SCHEMA.extend(
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await time_.register_time(var, config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.platformio.toolchain import copy_ccache_script
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .const import KEY_HOST
|
||||
|
||||
@@ -22,7 +23,7 @@ AUTO_LOAD = ["network", "preferences"]
|
||||
IS_TARGET_PLATFORM = True
|
||||
|
||||
|
||||
def set_core_data(config):
|
||||
def set_core_data(config: ConfigType) -> ConfigType:
|
||||
CORE.data[KEY_HOST] = {}
|
||||
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_HOST
|
||||
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "host"
|
||||
@@ -40,7 +41,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_build_flag("-DUSE_HOST")
|
||||
cg.add_define("USE_NATIVE_64BIT_TIME")
|
||||
# The prefs file finds stored preferences by key, so key migration is possible
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
@@ -14,6 +15,8 @@ from esphome.const import (
|
||||
CONF_PULLDOWN,
|
||||
CONF_PULLUP,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .const import host_ns
|
||||
|
||||
@@ -22,7 +25,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
HostGPIOPin = host_ns.class_("HostGPIOPin", cg.InternalGPIOPin)
|
||||
|
||||
|
||||
def _translate_pin(value):
|
||||
def _translate_pin(value: Any) -> int | str:
|
||||
if isinstance(value, dict) or value is None:
|
||||
raise cv.Invalid(
|
||||
"This variable only supports pin numbers, not full pin schemas "
|
||||
@@ -41,7 +44,7 @@ def _translate_pin(value):
|
||||
return value
|
||||
|
||||
|
||||
def validate_gpio_pin(value):
|
||||
def validate_gpio_pin(value: Any) -> int | str:
|
||||
return _translate_pin(value)
|
||||
|
||||
|
||||
@@ -53,7 +56,7 @@ HOST_PIN_SCHEMA = pins.gpio_base_schema(
|
||||
|
||||
|
||||
@pins.PIN_SCHEMA_REGISTRY.register("host", HOST_PIN_SCHEMA)
|
||||
async def host_pin_to_code(config):
|
||||
async def host_pin_to_code(config: ConfigType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
num = config[CONF_NUMBER]
|
||||
cg.add(var.set_pin(num))
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import time as time_
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
|
||||
@@ -14,7 +15,7 @@ CONFIG_SCHEMA = time_.TIME_SCHEMA.extend(
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await time_.register_time(var, config)
|
||||
|
||||
@@ -196,6 +196,9 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
||||
}
|
||||
|
||||
container->feed_wdt();
|
||||
// IDF is the only backend reusing the container across redirect hops;
|
||||
// drop the previous hop's headers (Arduino/host collect only the final response)
|
||||
container->response_headers_.clear();
|
||||
container->content_length = esp_http_client_fetch_headers(client);
|
||||
container->set_chunked(esp_http_client_is_chunked_response(client));
|
||||
container->feed_wdt();
|
||||
|
||||
@@ -64,8 +64,9 @@ void OtaHttpRequestComponent::flash() {
|
||||
}
|
||||
}
|
||||
|
||||
void OtaHttpRequestComponent::cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container) {
|
||||
if (this->update_started_) {
|
||||
void OtaHttpRequestComponent::cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container,
|
||||
bool abort_backend) {
|
||||
if (abort_backend) {
|
||||
ESP_LOGV(TAG, "Aborting OTA backend");
|
||||
backend->abort();
|
||||
}
|
||||
@@ -106,7 +107,8 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
auto error_code = backend->begin(container->content_length);
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
ESP_LOGW(TAG, "backend->begin error: %d", error_code);
|
||||
this->cleanup_(std::move(backend), container);
|
||||
// Nothing to abort: begin() failed, so no OTA handle was opened
|
||||
this->cleanup_(std::move(backend), container, /*abort_backend=*/false);
|
||||
return error_code;
|
||||
}
|
||||
|
||||
@@ -140,7 +142,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading data: %d", bufsize_or_error);
|
||||
}
|
||||
this->cleanup_(std::move(backend), container);
|
||||
this->cleanup_(std::move(backend), container, /*abort_backend=*/true);
|
||||
return OTA_CONNECTION_ERROR;
|
||||
}
|
||||
|
||||
@@ -150,14 +152,13 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
md5_receive.add(buf, bufsize_or_error);
|
||||
|
||||
// write bytes to OTA backend
|
||||
this->update_started_ = true;
|
||||
error_code = backend->write(buf, bufsize_or_error);
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
// error code explanation available at
|
||||
// https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h
|
||||
ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code,
|
||||
container->get_bytes_read() - bufsize_or_error, container->content_length);
|
||||
this->cleanup_(std::move(backend), container);
|
||||
this->cleanup_(std::move(backend), container, /*abort_backend=*/true);
|
||||
return error_code;
|
||||
}
|
||||
}
|
||||
@@ -181,7 +182,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
this->md5_computed_ = md5_receive_str;
|
||||
if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) {
|
||||
ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str());
|
||||
this->cleanup_(std::move(backend), container);
|
||||
this->cleanup_(std::move(backend), container, /*abort_backend=*/true);
|
||||
return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH;
|
||||
} else {
|
||||
backend->set_update_md5(md5_receive_str);
|
||||
@@ -197,7 +198,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
error_code = backend->end();
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
ESP_LOGW(TAG, "Error ending update! error_code: %d", error_code);
|
||||
this->cleanup_(std::move(backend), container);
|
||||
this->cleanup_(std::move(backend), container, /*abort_backend=*/true);
|
||||
return error_code;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented<
|
||||
void flash();
|
||||
|
||||
protected:
|
||||
void cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container);
|
||||
void cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container, bool abort_backend);
|
||||
uint8_t do_ota_();
|
||||
std::string get_url_with_auth_(const std::string &url);
|
||||
bool http_get_md5_();
|
||||
@@ -51,7 +51,6 @@ class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented<
|
||||
std::string username_{};
|
||||
std::string url_{};
|
||||
int status_ = -1;
|
||||
bool update_started_ = false;
|
||||
static const uint16_t HTTP_RECV_BUFFER = 256; // the firmware GET chunk size
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
@@ -52,9 +53,10 @@ from esphome.const import (
|
||||
PLATFORM_RP2,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -96,13 +98,13 @@ CONF_SCL_PULLUP_ENABLED = "scl_pullup_enabled"
|
||||
MULTI_CONF = True
|
||||
|
||||
|
||||
def validate_device(value):
|
||||
def validate_device(value: str) -> str:
|
||||
if not re.match(r"^/(?:[^/]+/)*[^/]+$", value):
|
||||
raise cv.Invalid("Device must be an absolute device path (e.g., /dev/i2c-0)")
|
||||
return value
|
||||
|
||||
|
||||
def _bus_declare_type(value):
|
||||
def _bus_declare_type(value: Any) -> ID:
|
||||
if CORE.is_esp32:
|
||||
return cv.declare_id(IDFI2CBus)(value)
|
||||
if CORE.using_arduino:
|
||||
@@ -114,7 +116,7 @@ def _bus_declare_type(value):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def _rp2040_i2c_controller(pin):
|
||||
def _rp2040_i2c_controller(pin: int) -> int:
|
||||
"""Return the I2C controller number (0 or 1) for a given RP2040/RP2350 GPIO pin.
|
||||
|
||||
See RP2040 datasheet Table 2 (section 1.4.3, "GPIO Functions"):
|
||||
@@ -125,7 +127,7 @@ def _rp2040_i2c_controller(pin):
|
||||
return (pin // 2) % 2
|
||||
|
||||
|
||||
def validate_config(config):
|
||||
def validate_config(config: ConfigType) -> ConfigType:
|
||||
if CORE.is_esp32:
|
||||
return cv.require_framework_version(
|
||||
esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1)
|
||||
@@ -142,7 +144,7 @@ def validate_config(config):
|
||||
return config
|
||||
|
||||
|
||||
def validate_host_config(config):
|
||||
def validate_host_config(config: ConfigType) -> ConfigType:
|
||||
if CORE.is_host:
|
||||
# Host I2C is currently only supported on Linux
|
||||
if not sys.platform.lower().startswith("linux"):
|
||||
@@ -229,7 +231,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
full_config = fv.full_config.get()[CONF_I2C]
|
||||
if CORE.using_zephyr and len(full_config) > 1:
|
||||
raise cv.Invalid("Second i2c is not implemented on Zephyr yet")
|
||||
@@ -281,7 +283,7 @@ FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.BUS)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_global(i2c_ns.using)
|
||||
cg.add_define("USE_I2C")
|
||||
if CORE.is_esp32:
|
||||
@@ -358,7 +360,7 @@ async def to_code(config):
|
||||
cg.add(var.set_lp_mode(bool(config[CONF_LOW_POWER_MODE])))
|
||||
|
||||
|
||||
def i2c_device_schema(default_address):
|
||||
def i2c_device_schema(default_address: int | None) -> cv.Schema:
|
||||
"""Create a schema for a i2c device.
|
||||
|
||||
:param default_address: The default address of the i2c device, can be None to represent
|
||||
@@ -375,7 +377,7 @@ def i2c_device_schema(default_address):
|
||||
return cv.Schema(schema)
|
||||
|
||||
|
||||
async def register_i2c_device(var, config):
|
||||
async def register_i2c_device(var: MockObj, config: ConfigType) -> None:
|
||||
"""Register an i2c device with the given config.
|
||||
|
||||
Sets the i2c bus to use and the i2c address.
|
||||
@@ -390,11 +392,11 @@ async def register_i2c_device(var, config):
|
||||
def final_validate_device_schema(
|
||||
name: str,
|
||||
*,
|
||||
min_frequency: cv.frequency = None,
|
||||
max_frequency: cv.frequency = None,
|
||||
min_timeout: cv.time_period = None,
|
||||
max_timeout: cv.time_period = None,
|
||||
):
|
||||
min_frequency: Any = None,
|
||||
max_frequency: Any = None,
|
||||
min_timeout: Any = None,
|
||||
max_timeout: Any = None,
|
||||
) -> cv.Schema:
|
||||
hub_schema = {}
|
||||
if (min_frequency is not None) and (max_frequency is not None):
|
||||
hub_schema[cv.Required(CONF_FREQUENCY)] = cv.Range(
|
||||
|
||||
@@ -75,8 +75,6 @@ void Infrared::dump_config() {
|
||||
YESNO(this->traits_.get_supports_receiver()));
|
||||
}
|
||||
|
||||
InfraredCall Infrared::make_call() { return InfraredCall(this); }
|
||||
|
||||
void Infrared::control(const InfraredCall &call) {
|
||||
if (this->transmitter_ == nullptr) {
|
||||
ESP_LOGW(TAG, "No transmitter configured");
|
||||
|
||||
@@ -134,7 +134,7 @@ class Infrared : public Component, public EntityBase, public remote_base::Remote
|
||||
const InfraredTraits &get_traits() const { return this->traits_; }
|
||||
|
||||
/// Create a call object for transmitting
|
||||
InfraredCall make_call();
|
||||
InfraredCall make_call() { return InfraredCall(this); }
|
||||
|
||||
/// Get capability flags for this infrared instance
|
||||
uint32_t get_capability_flags() const;
|
||||
|
||||
@@ -13,8 +13,6 @@ ESPColorView ESPRangeView::operator[](int32_t index) const {
|
||||
index = interpret_index(index, this->size()) + this->begin_;
|
||||
return (*this->parent_)[index];
|
||||
}
|
||||
ESPRangeIterator ESPRangeView::begin() { return {*this, this->begin_}; }
|
||||
ESPRangeIterator ESPRangeView::end() { return {*this, this->end_}; }
|
||||
|
||||
void ESPRangeView::set(const Color &color) {
|
||||
for (int32_t i = this->begin_; i < this->end_; i++) {
|
||||
|
||||
@@ -75,4 +75,7 @@ class ESPRangeIterator {
|
||||
int32_t i_;
|
||||
};
|
||||
|
||||
inline ESPRangeIterator ESPRangeView::begin() { return {*this, this->begin_}; }
|
||||
inline ESPRangeIterator ESPRangeView::end() { return {*this, this->end_}; }
|
||||
|
||||
} // namespace esphome::light
|
||||
|
||||
@@ -157,8 +157,6 @@ void LightState::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; }
|
||||
|
||||
void LightState::publish_state() {
|
||||
if (this->remote_values_listeners_) {
|
||||
for (auto *listener : *this->remote_values_listeners_) {
|
||||
@@ -194,25 +192,11 @@ void LightState::add_target_state_reached_listener(LightTargetStateReachedListen
|
||||
this->target_state_reached_listeners_->push_back(listener);
|
||||
}
|
||||
|
||||
void LightState::set_default_transition_length(uint32_t default_transition_length) {
|
||||
this->default_transition_length_ = default_transition_length;
|
||||
}
|
||||
uint32_t LightState::get_default_transition_length() const { return this->default_transition_length_; }
|
||||
void LightState::set_flash_transition_length(uint32_t flash_transition_length) {
|
||||
this->flash_transition_length_ = flash_transition_length;
|
||||
}
|
||||
uint32_t LightState::get_flash_transition_length() const { return this->flash_transition_length_; }
|
||||
void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; }
|
||||
void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
||||
void LightState::set_initial_state(void (*callback)(LightStateRTCState &)) { this->initial_state_callback_ = callback; }
|
||||
bool LightState::supports_effects() { return !this->effects_.empty(); }
|
||||
const FixedVector<LightEffect *> &LightState::get_effects() const { return this->effects_; }
|
||||
void LightState::add_effects(const std::initializer_list<LightEffect *> &effects) {
|
||||
// Called once from Python codegen during setup with all effects from YAML config
|
||||
this->effects_ = effects;
|
||||
}
|
||||
|
||||
void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); }
|
||||
void LightState::current_values_as_brightness(float *brightness) {
|
||||
this->current_values.as_brightness(brightness);
|
||||
*brightness = this->gamma_correct_lut(*brightness);
|
||||
@@ -333,8 +317,6 @@ float LightState::gamma_uncorrect_lut(float value) const {
|
||||
}
|
||||
#endif // USE_LIGHT_GAMMA_LUT
|
||||
|
||||
bool LightState::is_transformer_active() { return this->is_transformer_active_; }
|
||||
|
||||
void LightState::start_effect_(uint32_t effect_index) {
|
||||
this->stop_effect_();
|
||||
if (effect_index == 0)
|
||||
|
||||
@@ -109,7 +109,7 @@ class LightState : public EntityBase, public Component {
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
/// Shortly after HARDWARE.
|
||||
float get_setup_priority() const override;
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE - 1.0f; }
|
||||
|
||||
/** The current values of the light as outputted to the light.
|
||||
*
|
||||
@@ -157,15 +157,19 @@ class LightState : public EntityBase, public Component {
|
||||
void add_target_state_reached_listener(LightTargetStateReachedListener *listener);
|
||||
|
||||
/// Set the default transition length, i.e. the transition length when no transition is provided.
|
||||
void set_default_transition_length(uint32_t default_transition_length);
|
||||
uint32_t get_default_transition_length() const;
|
||||
void set_default_transition_length(uint32_t default_transition_length) {
|
||||
this->default_transition_length_ = default_transition_length;
|
||||
}
|
||||
uint32_t get_default_transition_length() const { return this->default_transition_length_; }
|
||||
|
||||
/// Set the flash transition length
|
||||
void set_flash_transition_length(uint32_t flash_transition_length);
|
||||
uint32_t get_flash_transition_length() const;
|
||||
void set_flash_transition_length(uint32_t flash_transition_length) {
|
||||
this->flash_transition_length_ = flash_transition_length;
|
||||
}
|
||||
uint32_t get_flash_transition_length() const { return this->flash_transition_length_; }
|
||||
|
||||
/// Set the gamma correction factor
|
||||
void set_gamma_correct(float gamma_correct);
|
||||
void set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; }
|
||||
float get_gamma_correct() const { return this->gamma_correct_; }
|
||||
|
||||
#ifdef USE_LIGHT_GAMMA_LUT
|
||||
@@ -186,17 +190,17 @@ class LightState : public EntityBase, public Component {
|
||||
#endif // USE_LIGHT_GAMMA_LUT
|
||||
|
||||
/// Set the restore mode of this light
|
||||
void set_restore_mode(LightRestoreMode restore_mode);
|
||||
void set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
||||
|
||||
/// Set a callback to populate the initial state defaults during setup.
|
||||
/// The callback is called once, then cleared. Values live in flash as code.
|
||||
void set_initial_state(void (*callback)(LightStateRTCState &));
|
||||
void set_initial_state(void (*callback)(LightStateRTCState &)) { this->initial_state_callback_ = callback; }
|
||||
|
||||
/// Return whether the light has any effects that meet the trait requirements.
|
||||
bool supports_effects();
|
||||
bool supports_effects() const { return !this->effects_.empty(); }
|
||||
|
||||
/// Get all effects for this light state.
|
||||
const FixedVector<LightEffect *> &get_effects() const;
|
||||
const FixedVector<LightEffect *> &get_effects() const { return this->effects_; }
|
||||
|
||||
/// Add effects for this light state.
|
||||
void add_effects(const std::initializer_list<LightEffect *> &effects);
|
||||
@@ -254,7 +258,7 @@ class LightState : public EntityBase, public Component {
|
||||
}
|
||||
|
||||
/// The result of all the current_values_as_* methods have gamma correction applied.
|
||||
void current_values_as_binary(bool *binary);
|
||||
void current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); }
|
||||
|
||||
void current_values_as_brightness(float *brightness);
|
||||
|
||||
@@ -281,7 +285,7 @@ class LightState : public EntityBase, public Component {
|
||||
* return;
|
||||
* }
|
||||
*/
|
||||
bool is_transformer_active();
|
||||
bool is_transformer_active() const { return this->is_transformer_active_; }
|
||||
|
||||
protected:
|
||||
friend LightOutput;
|
||||
|
||||
@@ -12,13 +12,14 @@ from esphome.const import (
|
||||
CONF_ON_UNLOCK,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
|
||||
from esphome.types import ConfigType, SafeExpType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
@@ -102,7 +103,7 @@ _CALLBACK_AUTOMATIONS = (
|
||||
|
||||
|
||||
@setup_entity("lock")
|
||||
async def _setup_lock_core(var, config):
|
||||
async def _setup_lock_core(var: MockObj, config: ConfigType) -> None:
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
if mqtt_id := config.get(CONF_MQTT_ID):
|
||||
@@ -113,7 +114,7 @@ async def _setup_lock_core(var, config):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
|
||||
|
||||
async def register_lock(var, config):
|
||||
async def register_lock(var: MockObj, config: ConfigType) -> None:
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
queue_entity_register("lock", config)
|
||||
@@ -121,7 +122,7 @@ async def register_lock(var, config):
|
||||
await _setup_lock_core(var, config)
|
||||
|
||||
|
||||
async def new_lock(config, *args):
|
||||
async def new_lock(config: ConfigType, *args: SafeExpType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
await register_lock(var, config)
|
||||
return var
|
||||
@@ -143,23 +144,38 @@ LOCK_ACTION_SCHEMA = maybe_simple_id(
|
||||
@automation.register_action(
|
||||
"lock.open", OpenAction, LOCK_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def lock_action_to_code(config, action_id, template_arg, args):
|
||||
async def lock_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_condition("lock.is_locked", LockCondition, LOCK_ACTION_SCHEMA)
|
||||
async def lock_is_on_to_code(config, condition_id, template_arg, args):
|
||||
async def lock_is_on_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren, True)
|
||||
|
||||
|
||||
@automation.register_condition("lock.is_unlocked", LockCondition, LOCK_ACTION_SCHEMA)
|
||||
async def lock_is_off_to_code(config, condition_id, template_arg, args):
|
||||
async def lock_is_off_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren, False)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_global(lock_ns.using)
|
||||
|
||||
@@ -201,17 +201,10 @@ void Logger::process_messages_() {
|
||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||
}
|
||||
|
||||
void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
|
||||
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
|
||||
void Logger::set_log_level(const char *tag, uint8_t log_level) { this->log_levels_[tag] = log_level; }
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
UARTSelection Logger::get_uart() const { return this->uart_; }
|
||||
#endif
|
||||
|
||||
float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; }
|
||||
|
||||
// Log level strings - packed into flash on ESP8266, indexed by log level (0-7)
|
||||
PROGMEM_STRING_TABLE(LogLevelStrings, "NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE");
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ class Logger final : public Component {
|
||||
void loop() override;
|
||||
#endif
|
||||
/// Manually set the baud rate for serial, set to 0 to disable.
|
||||
void set_baud_rate(uint32_t baud_rate);
|
||||
void set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
|
||||
uint32_t get_baud_rate() const { return baud_rate_; }
|
||||
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
|
||||
Stream *get_hw_serial() const { return hw_serial_; }
|
||||
@@ -163,7 +163,7 @@ class Logger final : public Component {
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; }
|
||||
/// Get the UART used by the logger.
|
||||
UARTSelection get_uart() const;
|
||||
UARTSelection get_uart() const { return this->uart_; }
|
||||
#endif
|
||||
|
||||
/// Set the default log level for this logger.
|
||||
@@ -197,7 +197,7 @@ class Logger final : public Component {
|
||||
void add_level_listener(LoggerLevelListener *listener) { this->level_listeners_.push_back(listener); }
|
||||
#endif
|
||||
|
||||
float get_setup_priority() const override;
|
||||
float get_setup_priority() const override { return setup_priority::BUS + 500.0f; }
|
||||
|
||||
void log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args); // NOLINT
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
|
||||
@@ -8,6 +8,7 @@ from esphome.const import (
|
||||
CONF_ON_STATE,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
CONF_USE_FAHRENHEIT,
|
||||
)
|
||||
from esphome.core import ID, Lambda
|
||||
from esphome.cpp_generator import LambdaExpression, MockObj
|
||||
@@ -71,6 +72,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(
|
||||
CONF_TELEMETRY_REQUEST_MIN_INTERVAL, default="60s"
|
||||
): cv.update_interval,
|
||||
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
|
||||
cv.Optional(CONF_VANE): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
|
||||
@@ -114,6 +116,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
config[CONF_TELEMETRY_REQUEST_MIN_INTERVAL]
|
||||
)
|
||||
)
|
||||
cg.add(var.set_use_fahrenheit(config[CONF_USE_FAHRENHEIT]))
|
||||
if on_state := config.get(CONF_VANE, {}).get(CONF_ON_STATE):
|
||||
cg.add_global(mitsubishi_ns.using)
|
||||
for conf in on_state:
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
namespace esphome::mitsubishi_cn105 {
|
||||
|
||||
template<typename... Ts>
|
||||
class SetRemoteTemperatureAction : public Action<Ts...>, public Parented<MitsubishiCN105Component> {
|
||||
class SetRemoteTemperatureAction final : public Action<Ts...>, public Parented<MitsubishiCN105Component> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(float, temperature)
|
||||
|
||||
@@ -17,12 +17,12 @@ class SetRemoteTemperatureAction : public Action<Ts...>, public Parented<Mitsubi
|
||||
};
|
||||
|
||||
template<typename... Ts>
|
||||
class ClearRemoteTemperatureAction : public Action<Ts...>, public Parented<MitsubishiCN105Component> {
|
||||
class ClearRemoteTemperatureAction final : public Action<Ts...>, public Parented<MitsubishiCN105Component> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->clear_remote_temperature(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class VaneControlAction : public Action<Ts...> {
|
||||
template<typename... Ts> class VaneControlAction final : public Action<Ts...> {
|
||||
public:
|
||||
using ApplyFn = void (*)(VaneCall &, const std::remove_cvref_t<Ts> &...);
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ class MitsubishiCN105 {
|
||||
return this->is_telemetry_polling_enabled() ? !std::isnan(this->status_.room_temperature)
|
||||
: !std::isnan(this->status_.target_temperature);
|
||||
}
|
||||
bool is_temperature_encoding_b() const { return this->property_context_.use_temperature_encoding_b; }
|
||||
|
||||
void set_power(bool power_on);
|
||||
void set_target_temperature(float target_temperature);
|
||||
|
||||
@@ -50,7 +50,11 @@ static constexpr std::optional<Left> reverse_map_lookup(const std::array<std::pa
|
||||
return key.has_value() ? reverse_map_lookup(map, *key) : std::nullopt;
|
||||
}
|
||||
|
||||
void MitsubishiCN105Climate::dump_config() { LOG_CLIMATE("", "Mitsubishi CN105 Climate", this); }
|
||||
void MitsubishiCN105Climate::dump_config() {
|
||||
LOG_CLIMATE("", "Mitsubishi CN105 Climate", this);
|
||||
ESP_LOGCONFIG(TAG, " Temperature unit: °%c",
|
||||
this->parent_->get_temperature_mapping().get_use_fahrenheit() ? 'F' : 'C');
|
||||
}
|
||||
|
||||
void MitsubishiCN105Climate::setup() {
|
||||
this->parent_->add_on_status_callback([this]() { this->apply_values_(); });
|
||||
@@ -70,15 +74,17 @@ climate::ClimateTraits MitsubishiCN105Climate::traits() {
|
||||
traits.add_supported_fan_mode(p.second);
|
||||
}
|
||||
|
||||
traits.set_supported_swing_modes(this->supported_swing_modes_);
|
||||
traits.set_supported_swing_modes(this->swing_mode_manager_.supported_swing_modes());
|
||||
|
||||
traits.set_visual_min_temperature(16.0f);
|
||||
traits.set_visual_max_temperature(31.0f);
|
||||
const bool use_fahrenheit = this->parent_->get_temperature_mapping().get_use_fahrenheit();
|
||||
traits.set_temperature_unit(use_fahrenheit ? TemperatureUnit::FAHRENHEIT : TemperatureUnit::CELSIUS);
|
||||
traits.set_visual_min_temperature(use_fahrenheit ? 61.0f : 16.0f);
|
||||
traits.set_visual_max_temperature(use_fahrenheit ? 88.0f : 31.0f);
|
||||
traits.set_visual_temperature_step(1.0f);
|
||||
|
||||
if (this->parent_->is_telemetry_polling_enabled()) {
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
traits.set_visual_current_temperature_step(0.5f);
|
||||
traits.set_visual_current_temperature_step(use_fahrenheit ? 1.0f : 0.5f);
|
||||
}
|
||||
|
||||
return traits;
|
||||
@@ -86,7 +92,7 @@ climate::ClimateTraits MitsubishiCN105Climate::traits() {
|
||||
|
||||
void MitsubishiCN105Climate::control(const climate::ClimateCall &call) {
|
||||
if (const auto target_temperature = call.get_target_temperature()) {
|
||||
this->parent_->set_target_temperature(*target_temperature);
|
||||
this->parent_->set_target_temperature(this->parent_->get_temperature_mapping().to_mitsubishi(*target_temperature));
|
||||
}
|
||||
|
||||
if (const auto mode = call.get_mode()) {
|
||||
@@ -103,33 +109,11 @@ void MitsubishiCN105Climate::control(const climate::ClimateCall &call) {
|
||||
}
|
||||
|
||||
if (const auto swing_mode = call.get_swing_mode()) {
|
||||
auto vane = this->last_non_swing_vane_mode_;
|
||||
auto wide = this->last_non_swing_wide_vane_mode_;
|
||||
|
||||
switch (*swing_mode) {
|
||||
case climate::CLIMATE_SWING_BOTH:
|
||||
vane = MitsubishiCN105::VaneMode::SWING;
|
||||
wide = MitsubishiCN105::WideVaneMode::SWING;
|
||||
break;
|
||||
|
||||
case climate::CLIMATE_SWING_VERTICAL:
|
||||
vane = MitsubishiCN105::VaneMode::SWING;
|
||||
break;
|
||||
|
||||
case climate::CLIMATE_SWING_HORIZONTAL:
|
||||
wide = MitsubishiCN105::WideVaneMode::SWING;
|
||||
break;
|
||||
|
||||
case climate::CLIMATE_SWING_OFF:
|
||||
default:
|
||||
break;
|
||||
if (const auto vane = this->swing_mode_manager_.vane_from(*swing_mode)) {
|
||||
this->parent_->set_vane_mode(*vane);
|
||||
}
|
||||
|
||||
if (this->supported_swing_modes_.count(climate::CLIMATE_SWING_VERTICAL)) {
|
||||
this->parent_->set_vane_mode(vane);
|
||||
}
|
||||
if (this->supported_swing_modes_.count(climate::CLIMATE_SWING_HORIZONTAL)) {
|
||||
this->parent_->set_wide_vane_mode(wide);
|
||||
if (const auto wide = this->swing_mode_manager_.wide_vane_from(*swing_mode)) {
|
||||
this->parent_->set_wide_vane_mode(*wide);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,10 +123,10 @@ void MitsubishiCN105Climate::control(const climate::ClimateCall &call) {
|
||||
void MitsubishiCN105Climate::apply_values_() {
|
||||
const auto &status = this->parent_->status();
|
||||
|
||||
this->target_temperature = status.target_temperature;
|
||||
this->target_temperature = this->parent_->get_temperature_mapping().from_mitsubishi(status.target_temperature);
|
||||
|
||||
if (this->parent_->is_telemetry_polling_enabled()) {
|
||||
this->current_temperature = status.room_temperature;
|
||||
this->current_temperature = this->parent_->get_temperature_mapping().from_mitsubishi(status.room_temperature);
|
||||
}
|
||||
|
||||
if (status.power_on) {
|
||||
@@ -160,64 +144,39 @@ void MitsubishiCN105Climate::apply_values_() {
|
||||
ESP_LOGD(TAG, "Unable to map fan mode");
|
||||
}
|
||||
|
||||
if (!this->supported_swing_modes_.empty()) {
|
||||
bool vertical_swinging = false;
|
||||
bool horizontal_swinging = false;
|
||||
|
||||
if (this->supported_swing_modes_.count(climate::CLIMATE_SWING_VERTICAL)) {
|
||||
if (status.vane_mode == MitsubishiCN105::VaneMode::SWING) {
|
||||
vertical_swinging = true;
|
||||
} else if (status.vane_mode != MitsubishiCN105::VaneMode::UNKNOWN) {
|
||||
this->last_non_swing_vane_mode_ = status.vane_mode;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->supported_swing_modes_.count(climate::CLIMATE_SWING_HORIZONTAL)) {
|
||||
if (status.wide_vane_mode == MitsubishiCN105::WideVaneMode::SWING) {
|
||||
horizontal_swinging = true;
|
||||
} else if (status.wide_vane_mode != MitsubishiCN105::WideVaneMode::UNKNOWN) {
|
||||
this->last_non_swing_wide_vane_mode_ = status.wide_vane_mode;
|
||||
}
|
||||
}
|
||||
|
||||
if (vertical_swinging && horizontal_swinging) {
|
||||
this->swing_mode = climate::CLIMATE_SWING_BOTH;
|
||||
} else if (vertical_swinging) {
|
||||
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
|
||||
} else if (horizontal_swinging) {
|
||||
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
|
||||
} else {
|
||||
this->swing_mode = climate::CLIMATE_SWING_OFF;
|
||||
}
|
||||
if (const auto swing_mode =
|
||||
this->swing_mode_manager_.update_and_get_swing_mode(status.vane_mode, status.wide_vane_mode)) {
|
||||
this->swing_mode = *swing_mode;
|
||||
}
|
||||
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
void MitsubishiCN105Climate::set_supported_swing_mode(climate::ClimateSwingMode mode) {
|
||||
this->supported_swing_modes_.clear();
|
||||
climate::ClimateSwingModeMask supported_swing_modes;
|
||||
switch (mode) {
|
||||
case climate::CLIMATE_SWING_VERTICAL:
|
||||
this->supported_swing_modes_.insert(climate::CLIMATE_SWING_OFF);
|
||||
this->supported_swing_modes_.insert(climate::CLIMATE_SWING_VERTICAL);
|
||||
supported_swing_modes.insert(climate::CLIMATE_SWING_OFF);
|
||||
supported_swing_modes.insert(climate::CLIMATE_SWING_VERTICAL);
|
||||
break;
|
||||
|
||||
case climate::CLIMATE_SWING_HORIZONTAL:
|
||||
this->supported_swing_modes_.insert(climate::CLIMATE_SWING_OFF);
|
||||
this->supported_swing_modes_.insert(climate::CLIMATE_SWING_HORIZONTAL);
|
||||
supported_swing_modes.insert(climate::CLIMATE_SWING_OFF);
|
||||
supported_swing_modes.insert(climate::CLIMATE_SWING_HORIZONTAL);
|
||||
break;
|
||||
|
||||
case climate::CLIMATE_SWING_BOTH:
|
||||
this->supported_swing_modes_.insert(climate::CLIMATE_SWING_OFF);
|
||||
this->supported_swing_modes_.insert(climate::CLIMATE_SWING_VERTICAL);
|
||||
this->supported_swing_modes_.insert(climate::CLIMATE_SWING_HORIZONTAL);
|
||||
this->supported_swing_modes_.insert(climate::CLIMATE_SWING_BOTH);
|
||||
supported_swing_modes.insert(climate::CLIMATE_SWING_OFF);
|
||||
supported_swing_modes.insert(climate::CLIMATE_SWING_VERTICAL);
|
||||
supported_swing_modes.insert(climate::CLIMATE_SWING_HORIZONTAL);
|
||||
supported_swing_modes.insert(climate::CLIMATE_SWING_BOTH);
|
||||
break;
|
||||
|
||||
case climate::CLIMATE_SWING_OFF:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this->swing_mode_manager_.set_supported_swing_modes(supported_swing_modes);
|
||||
}
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/climate/climate.h"
|
||||
#include "mitsubishi_cn105_swing_mode_manager.h"
|
||||
|
||||
namespace esphome::mitsubishi_cn105 {
|
||||
|
||||
class MitsubishiCN105Climate : public climate::Climate, public Component, public Parented<MitsubishiCN105Component> {
|
||||
class MitsubishiCN105Climate final : public climate::Climate,
|
||||
public Component,
|
||||
public Parented<MitsubishiCN105Component> {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
@@ -25,14 +28,12 @@ class MitsubishiCN105Climate : public climate::Climate, public Component, public
|
||||
protected:
|
||||
void apply_values_();
|
||||
|
||||
climate::ClimateSwingModeMask supported_swing_modes_{};
|
||||
MitsubishiCN105::VaneMode last_non_swing_vane_mode_{MitsubishiCN105::VaneMode::AUTO};
|
||||
MitsubishiCN105::WideVaneMode last_non_swing_wide_vane_mode_{MitsubishiCN105::WideVaneMode::CENTER};
|
||||
SwingModeManager swing_mode_manager_;
|
||||
};
|
||||
|
||||
// Legacy climate action compatibility. Remove in 2027.2.0.
|
||||
template<typename... Ts>
|
||||
class LegacySetRemoteTemperatureAction : public Action<Ts...>, public Parented<MitsubishiCN105Climate> {
|
||||
class LegacySetRemoteTemperatureAction final : public Action<Ts...>, public Parented<MitsubishiCN105Climate> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(float, temperature)
|
||||
|
||||
@@ -41,7 +42,7 @@ class LegacySetRemoteTemperatureAction : public Action<Ts...>, public Parented<M
|
||||
|
||||
// Legacy climate action compatibility. Remove in 2027.2.0.
|
||||
template<typename... Ts>
|
||||
class LegacyClearRemoteTemperatureAction : public Action<Ts...>, public Parented<MitsubishiCN105Climate> {
|
||||
class LegacyClearRemoteTemperatureAction final : public Action<Ts...>, public Parented<MitsubishiCN105Climate> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->clear_remote_temperature(); }
|
||||
};
|
||||
|
||||
@@ -27,6 +27,13 @@ void MitsubishiCN105Component::setup() { this->hp_.initialize(); }
|
||||
|
||||
void MitsubishiCN105Component::loop() {
|
||||
if (this->hp_.update()) {
|
||||
// Encoding A only supports whole °C values and cannot represent native °F setpoints accurately.
|
||||
// See https://github.com/esphome/esphome/pull/15488#issuecomment-5268304343
|
||||
if (this->temperature_mapping_.get_use_fahrenheit() && !this->hp_.is_temperature_encoding_b()) {
|
||||
ESP_LOGE(TAG, "Unit reports encoding A, which cannot accurately convert °F setpoints; disable 'use_fahrenheit'");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->notify_status_listeners_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,43 @@
|
||||
#include "mitsubishi_cn105.h"
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
#include <utility>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
namespace esphome::mitsubishi_cn105 {
|
||||
|
||||
struct TemperatureMapping {
|
||||
float to_mitsubishi(float value) const {
|
||||
if (!this->use_fahrenheit_) {
|
||||
return value;
|
||||
}
|
||||
const int fahrenheit = std::clamp(static_cast<int>(std::round(value)), 61, 88);
|
||||
return 0.5f * (fahrenheit - 28 + (fahrenheit > 68) - (fahrenheit < 68));
|
||||
}
|
||||
|
||||
float from_mitsubishi(float value) const {
|
||||
if (!this->use_fahrenheit_) {
|
||||
return value;
|
||||
}
|
||||
if (value < 16.0f || value > 30.5f) {
|
||||
return celsius_to_fahrenheit(value);
|
||||
}
|
||||
const int mitsubishi_half_degrees = static_cast<int>(std::round(value * 2.0f));
|
||||
return mitsubishi_half_degrees + 29 - (mitsubishi_half_degrees >= 40) - (mitsubishi_half_degrees > 40);
|
||||
}
|
||||
|
||||
bool get_use_fahrenheit() const { return this->use_fahrenheit_; }
|
||||
void set_use_fahrenheit(bool value) { this->use_fahrenheit_ = value; }
|
||||
|
||||
protected:
|
||||
bool use_fahrenheit_{false};
|
||||
};
|
||||
|
||||
enum VerticalVaneMode : uint8_t {
|
||||
VERTICAL_VANE_MODE_AUTO = static_cast<uint8_t>(MitsubishiCN105::VaneMode::AUTO),
|
||||
VERTICAL_VANE_MODE_POSITION_1 = static_cast<uint8_t>(MitsubishiCN105::VaneMode::POSITION_1),
|
||||
@@ -50,7 +80,7 @@ struct VaneCall {
|
||||
MitsubishiCN105Component *parent_;
|
||||
};
|
||||
|
||||
class MitsubishiCN105Component : public Component, public uart::UARTDevice {
|
||||
class MitsubishiCN105Component final : public Component, public uart::UARTDevice {
|
||||
public:
|
||||
explicit MitsubishiCN105Component() : hp_(*this) {}
|
||||
|
||||
@@ -60,6 +90,7 @@ class MitsubishiCN105Component : public Component, public uart::UARTDevice {
|
||||
|
||||
void set_update_interval(uint32_t ms) { this->hp_.set_update_interval(ms); }
|
||||
void set_telemetry_request_min_interval(uint32_t ms) { this->hp_.set_telemetry_request_min_interval(ms); }
|
||||
void set_use_fahrenheit(bool value) { this->temperature_mapping_.set_use_fahrenheit(value); }
|
||||
|
||||
void set_remote_temperature(float temperature) { this->hp_.set_remote_temperature(temperature); }
|
||||
void clear_remote_temperature() { this->hp_.clear_remote_temperature(); }
|
||||
@@ -75,6 +106,7 @@ class MitsubishiCN105Component : public Component, public uart::UARTDevice {
|
||||
const MitsubishiCN105::Status &status() const { return this->hp_.status(); }
|
||||
bool is_status_initialized() const { return this->hp_.is_status_initialized(); }
|
||||
bool is_telemetry_polling_enabled() const { return this->hp_.is_telemetry_polling_enabled(); }
|
||||
const TemperatureMapping &get_temperature_mapping() const { return this->temperature_mapping_; }
|
||||
|
||||
template<typename F> void add_on_status_callback(F &&callback) {
|
||||
this->status_callback_.add(std::forward<F>(callback));
|
||||
@@ -99,6 +131,7 @@ class MitsubishiCN105Component : public Component, public uart::UARTDevice {
|
||||
}
|
||||
|
||||
MitsubishiCN105 hp_;
|
||||
TemperatureMapping temperature_mapping_;
|
||||
CallbackManager<void()> status_callback_;
|
||||
LazyCallbackManager<void(const VaneState &)> vane_state_callback_;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "esphome/components/climate/climate.h"
|
||||
#include "mitsubishi_cn105.h"
|
||||
|
||||
namespace esphome::mitsubishi_cn105 {
|
||||
|
||||
class SwingModeManager final {
|
||||
public:
|
||||
const climate::ClimateSwingModeMask &supported_swing_modes() const { return this->supported_swing_modes_; }
|
||||
void set_supported_swing_modes(const climate::ClimateSwingModeMask &supported_swing_modes) {
|
||||
this->supported_swing_modes_ = supported_swing_modes;
|
||||
}
|
||||
|
||||
std::optional<MitsubishiCN105::VaneMode> vane_from(climate::ClimateSwingMode swing_mode) const {
|
||||
if (!this->supported_swing_modes_.count(climate::CLIMATE_SWING_VERTICAL)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
switch (swing_mode) {
|
||||
case climate::CLIMATE_SWING_BOTH:
|
||||
case climate::CLIMATE_SWING_VERTICAL:
|
||||
return MitsubishiCN105::VaneMode::SWING;
|
||||
default:
|
||||
return this->last_non_swing_vane_mode_;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<MitsubishiCN105::WideVaneMode> wide_vane_from(climate::ClimateSwingMode swing_mode) const {
|
||||
if (!this->supported_swing_modes_.count(climate::CLIMATE_SWING_HORIZONTAL)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
switch (swing_mode) {
|
||||
case climate::CLIMATE_SWING_BOTH:
|
||||
case climate::CLIMATE_SWING_HORIZONTAL:
|
||||
return MitsubishiCN105::WideVaneMode::SWING;
|
||||
default:
|
||||
return this->last_non_swing_wide_vane_mode_;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<climate::ClimateSwingMode> update_and_get_swing_mode(MitsubishiCN105::VaneMode vane_mode,
|
||||
MitsubishiCN105::WideVaneMode wide_vane_mode) {
|
||||
if (this->supported_swing_modes_.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool vertical_swinging = false;
|
||||
bool horizontal_swinging = false;
|
||||
if (this->supported_swing_modes_.count(climate::CLIMATE_SWING_VERTICAL)) {
|
||||
if (vane_mode == MitsubishiCN105::VaneMode::SWING) {
|
||||
vertical_swinging = true;
|
||||
} else if (vane_mode != MitsubishiCN105::VaneMode::UNKNOWN) {
|
||||
this->last_non_swing_vane_mode_ = vane_mode;
|
||||
}
|
||||
}
|
||||
if (this->supported_swing_modes_.count(climate::CLIMATE_SWING_HORIZONTAL)) {
|
||||
if (wide_vane_mode == MitsubishiCN105::WideVaneMode::SWING) {
|
||||
horizontal_swinging = true;
|
||||
} else if (wide_vane_mode != MitsubishiCN105::WideVaneMode::UNKNOWN) {
|
||||
this->last_non_swing_wide_vane_mode_ = wide_vane_mode;
|
||||
}
|
||||
}
|
||||
|
||||
if (vertical_swinging && horizontal_swinging) {
|
||||
return climate::CLIMATE_SWING_BOTH;
|
||||
}
|
||||
if (vertical_swinging) {
|
||||
return climate::CLIMATE_SWING_VERTICAL;
|
||||
}
|
||||
if (horizontal_swinging) {
|
||||
return climate::CLIMATE_SWING_HORIZONTAL;
|
||||
}
|
||||
return climate::CLIMATE_SWING_OFF;
|
||||
}
|
||||
|
||||
private:
|
||||
climate::ClimateSwingModeMask supported_swing_modes_{};
|
||||
MitsubishiCN105::VaneMode last_non_swing_vane_mode_{MitsubishiCN105::VaneMode::AUTO};
|
||||
MitsubishiCN105::WideVaneMode last_non_swing_wide_vane_mode_{MitsubishiCN105::WideVaneMode::CENTER};
|
||||
};
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105
|
||||
@@ -7,9 +7,9 @@
|
||||
|
||||
namespace esphome::mitsubishi_cn105 {
|
||||
|
||||
class MitsubishiCN105VerticalVaneDirectionSelect : public select::Select,
|
||||
public Component,
|
||||
public Parented<MitsubishiCN105Component> {
|
||||
class MitsubishiCN105VerticalVaneDirectionSelect final : public select::Select,
|
||||
public Component,
|
||||
public Parented<MitsubishiCN105Component> {
|
||||
public:
|
||||
void setup() override;
|
||||
void publish_vane_state(MitsubishiCN105::VaneMode mode);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user