Compare commits

..
Author SHA1 Message Date
J. Nick Koston f096b56c64 Bypass the cache when the resolved config enables secure options the snapshot cannot see 2026-09-30 04:17:41 +02:00
J. Nick Koston daf693c3c9 Fail the parity guard when the cached path never ran, and rebuild parity from a clean sub-build 2026-09-30 04:14:16 +02:00
J. Nick Koston 19e3565c42 Trim the hooks docstring and fix the added-lines count in a comment 2026-09-30 04:05:51 +02:00
J. Nick Koston 85fc70186f Record bootloader hooks from the configured build, make config name sets immutable, never delete an entry on a transient read error 2026-09-30 03:59:04 +02:00
J. Nick Koston 53d67aec68 Fail safe the hook scan and size check, key env flags, bypass custom IDF sources, add missing annotations 2026-09-30 03:30:29 +02:00
J. Nick Koston d519e1bbd8 Update stale comment: the override adds two lines now 2026-09-30 02:56:05 +02:00
J. Nick Koston 13b75006ea [espidf] Keep the idf_target variable IDF's bt component reads by accident 2026-09-30 02:36:16 +02:00
J. Nick Koston dcfa8fb675 [espidf] Recheck managed bootloader hooks after configure downloads them 2026-09-30 01:26:37 +02:00
J. Nick Koston 94b6d3e19c [espidf] Bypass on managed bootloader hooks and announce a skipped parity check 2026-09-30 01:08:03 +02:00
J. Nick Koston e08b546035 [espidf] Prove the masked-error test by exception type for Windows 2026-09-30 00:56:06 +02:00
J. Nick Koston e7243c3dc8 [espidf] Isolate prune races and fail the parity check closed 2026-09-30 00:25:24 +02:00
J. Nick Koston 7a12b36168 [espidf] Assert the masked-error test via the exception filename for Windows 2026-09-30 00:21:23 +02:00
J. Nick Koston 7a1b393347 [espidf] Keep the name union monotonic and the original publish error 2026-09-30 00:13:58 +02:00
J. Nick Koston 429cb0c469 [espidf] Prove cached equals in-tree in CI and trim round five findings 2026-09-29 23:55:55 +02:00
J. Nick Koston 6482ce4b07 [espidf] Trace entry reuse in publish 2026-09-29 23:50:08 +02:00
J. Nick Koston d4bc6066c9 [espidf] Trace failed best-effort deletes 2026-09-29 23:32:30 +02:00
J. Nick Koston e77ae52d00 [espidf] Warn with the expected path when the build yields no sdkconfig 2026-09-29 23:30:31 +02:00
J. Nick Koston efaaaa90ef [espidf] Name the missing configure output when the cache is unusable 2026-09-29 23:21:19 +02:00
J. Nick Koston e335c9abd8 [espidf] Verify a cache hit against the published bin sha 2026-09-29 21:59:39 +02:00
J. Nick Koston 573aa7cbd1 [espidf] Log lasting cache fallback reasons at info 2026-09-29 21:46:41 +02:00
J. Nick Koston 40dd86c2f0 [espidf] Clean up work and stage dirs on failure 2026-09-29 21:28:08 +02:00
J. Nick Koston 387df1ca77 [espidf] Require the partition table offset and complete cache entries 2026-09-29 21:12:51 +02:00
J. Nick Koston 7a182ccddb [espidf] Share one text sdkconfig parser 2026-09-29 21:06:21 +02:00
J. Nick Koston 61e7c2583a [espidf] Reuse file_compare and centralize the offset and stamp lookups 2026-09-29 20:55:37 +02:00
J. Nick Koston 8e96bf4e99 [espidf] Log likely regressions in the enable check at warning 2026-09-29 20:47:17 +02:00
J. Nick Koston 91b0b8669e [espidf] Fail the factory image when its bootloader is missing 2026-09-29 20:42:35 +02:00
J. Nick Koston 273bafd988 [espidf] Block reproducible-build-off up front and require the flash offset 2026-09-29 20:34:25 +02:00
J. Nick Koston c6f0e5caae [espidf] Drop the failure stamp; rare failures just fall back with a warning 2026-09-29 20:26:07 +02:00
J. Nick Koston 1aafe0d2d2 [espidf] Bypass the cache when the project has bootloader_components 2026-09-29 20:23:48 +02:00
J. Nick Koston 56038b18e8 [espidf] Probe the bootloader for byte reproducibility before caching it 2026-09-29 20:18:08 +02:00
J. Nick Koston 254ed144a1 [espidf] Share the cached bootloader tree predicate and return bool from ensure 2026-09-29 20:05:22 +02:00
J. Nick Koston 3044af34e3 [espidf] Move generic helpers out of the bootloader cache and gate the factory injection on the cache mode 2026-09-29 19:59:29 +02:00
J. Nick Koston f60d3adb52 [espidf] Share one JSON loader 2026-09-29 19:55:27 +02:00
J. Nick Koston 10a4f74fff [espidf] Trim the bootloader override comment 2026-09-29 19:55:27 +02:00
J. Nick Koston 5985230408 [espidf] Pin the bootloader subproject contract and trim comments 2026-09-29 19:55:27 +02:00
J. Nick Koston a7bb31be6a [espidf] Fall back on any cache error and make work dirs collision free 2026-09-29 19:55:27 +02:00
J. Nick Koston c0793563de [espidf] Simplify the bootloader cache and fix Windows and lint failures 2026-09-29 19:55:27 +02:00
J. Nick Koston 2a45f5de3a [espidf] Cache the bootloader across builds 2026-09-29 19:55:27 +02:00
535 changed files with 5800 additions and 18721 deletions

No files matched your search

-1
View File
@@ -219,6 +219,5 @@ jobs:
run: |
docker run --rm \
-v "${{ github.workspace }}/docker/test_configs:/config" \
-e ESPHOME_LDGEN_STRICT=1 \
"ghcr.io/esphome/esphome-amd64:${{ needs.check-docker.outputs.tag }}" \
compile "${{ matrix.id }}.yaml"
+3 -10
View File
@@ -299,7 +299,7 @@ jobs:
# Pushes any fixes the hooks made back to the pull request. This step
# must keep its default name: the GitHub App that performs the push
# locates the workflow run by that name.
- uses: pre-commit-ci/lite-action@062bca0919bc9d6e66755cc05074b70c77e111fc # v1.2.0
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
if: always()
with:
msg: apply automatic formatting fixes
@@ -1225,19 +1225,12 @@ jobs:
if [ -n "$compile_csv" ]; then
# Run compilation with grouping and isolation
# The bootloader has no ESPHome code and these builds never
# flash; the check_idf_py batch keeps the full build so the
# native sub-build and the equivalence check stay covered.
skip_flag="--skip-bootloader"
if [[ "${{ matrix.batch.check_idf_py }}" == "true" ]]; then
skip_flag=""
fi
python3 script/test_build_components.py -e compile -c "$compile_csv" -f --isolate "$directly_changed_csv" $skip_flag
python3 script/test_build_components.py -e compile -c "$compile_csv" -f --isolate "$directly_changed_csv"
if [[ "${{ matrix.batch.check_idf_py }}" == "true" ]]; then
# The real idf.py must find nothing to configure or build in a
# tree built above; catches drift on ESP-IDF bumps.
echo "Checking the native ESP-IDF build matches idf.py"
python3 script/check_idf_py_equivalence.py
python3 script/check_idf_py_equivalence.py --require-cached-bootloader
fi
else
echo "All components in this batch are validate-only -- skipping compile stage."
-4
View File
@@ -541,7 +541,6 @@ esphome/components/st7735/* @SenexCrenshaw
esphome/components/st7789v/* @kbx81
esphome/components/st7920/* @marsjan155
esphome/components/statsd/* @Links2004
esphome/components/stcc4/* @j9brown
esphome/components/stts22h/* @B48D81EFCC
esphome/components/substitutions/* @esphome/core
esphome/components/sun/* @OttoWinter
@@ -555,12 +554,10 @@ esphome/components/syslog/* @clydebarrow
esphome/components/systa_bus/* @Mat931
esphome/components/t6615/* @tylermenezes
esphome/components/tas2780/* @remcom
esphome/components/tas58xx/* @mrtoy-me @remcom
esphome/components/tc74/* @sethgirvan
esphome/components/tca9548a/* @andreashergert1984
esphome/components/tca9555/* @mobrembski
esphome/components/tcl112/* @glmnet
esphome/components/tcp_uart/* @Bascht74
esphome/components/tee501/* @Stock-M
esphome/components/teleinfo/* @0hax
esphome/components/tem3200/* @bakerkj
@@ -602,7 +599,6 @@ esphome/components/uart/button/* @ssieb
esphome/components/uart/event/* @eoasmxd
esphome/components/uart/packet_transport/* @clydebarrow
esphome/components/uart_mux/* @kbx81
esphome/components/uart_tcp/* @Bascht74
esphome/components/udp/* @clydebarrow
esphome/components/ufire_ec/* @pvizeli
esphome/components/ufire_ise/* @pvizeli
+1 -1
View File
@@ -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.19.0
RUN uv pip install --no-cache-dir esphome-device-builder==1.17.2
RUN \
platformio settings set enable_telemetry No \
+163 -60
View File
@@ -18,6 +18,7 @@ from typing import TYPE_CHECKING, Protocol
from esphome import const, platform_hooks
from esphome.build_helpers.native import analysis_backend, native_backend
from esphome.const import (
ALLOWED_NAME_CHARS,
ARGUMENT_HELP_DEVICE,
BUNDLE_EXTENSION,
CONF_API,
@@ -35,6 +36,7 @@ from esphome.const import (
CONF_LOGGER,
CONF_MDNS,
CONF_MQTT,
CONF_NAME,
CONF_NAME_ADD_MAC_SUFFIX,
CONF_OTA,
CONF_PASSWORD,
@@ -60,7 +62,6 @@ from esphome.stacktrace import LogLineProcessor
from esphome.types import ConfigType
from esphome.upload_targets import PortType, get_port_type
from esphome.util import (
ESPHOME_COMMAND,
PICOTOOL_PACKAGE,
FlashImage,
detect_rp2040_bootsel,
@@ -84,6 +85,7 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
ESPHOME_COMMAND = [sys.executable, "-m", "esphome"]
# Maximum buffer size for serial log reading to prevent unbounded memory growth
SERIAL_BUFFER_MAX_SIZE = 65536
@@ -148,7 +150,6 @@ class ArgsProtocol(Protocol):
file: str | None
no_logs: bool
only_generate: bool
skip_bootloader: bool
show_secrets: bool
dashboard: bool
configuration: str
@@ -828,14 +829,6 @@ def write_cpp_file() -> int:
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
if CORE.skip_bootloader and not (CORE.is_esp32 and CORE.using_toolchain_esp_idf):
# Info, not a warning: an orchestrator cannot see YAML toolchain
# overrides, this is its expected no-op, and a full build is safe.
_LOGGER.info(
"--skip-bootloader ignored: only supported on ESP32 with the "
"esp-idf toolchain"
)
CORE.skip_bootloader = False
# Keep this gate here, NOT in config validation: device-builder needs
# `esphome config` to keep succeeding with placeholders so onboarding can run.
if CONF_WIFI in config:
@@ -866,10 +859,7 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
return rc
# Create factory.bin, ota.bin, and firmware.elf copy
if not toolchain.create_factory_bin():
# A build whose factory image could not be produced must not
# exit 0; downloads would serve an image from an older build.
return 1
toolchain.create_factory_bin()
toolchain.create_ota_bin()
toolchain.create_elf_copy()
from esphome.build_helpers.idedata import warn_if_idedata_missing
@@ -934,14 +924,11 @@ def _check_and_emit_build_info() -> None:
def _get_configured_xtal_freq() -> int | None:
"""Read the configured crystal frequency from the sdkconfig file."""
from esphome.espidf import parse_sdkconfig
sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}")
if not sdkconfig_path.is_file():
return None
with suppress(OSError, ValueError):
content = sdkconfig_path.read_text()
for line in content.splitlines():
if line.startswith("CONFIG_XTAL_FREQ="):
return int(line.split("=", 1)[1])
with suppress(OSError, ValueError, KeyError):
return int(parse_sdkconfig(sdkconfig_path)["CONFIG_XTAL_FREQ"])
return None
@@ -987,9 +974,8 @@ def upload_using_esptool(
# and partitions included where the target needs them)
image = native.get_factory_firmware_path()
if not image.is_file():
hint = getattr(native, "missing_image_hint", lambda: None)()
raise EsphomeError(
hint or f"{image} does not exist; compile the configuration first"
f"{image} does not exist; compile the configuration first"
)
flash_images = [FlashImage(path=image, offset="0x0")]
else:
@@ -1300,9 +1286,10 @@ def _choose_ota_platform(config: ConfigType, requested: str | None) -> str:
The native API uses challenge-response auth with MD5/SHA256 hashing of a
server-issued nonce, so the password is never sent over the wire; the
``web_server`` path uses HTTP Basic auth which transmits credentials in
cleartext over the LAN. (The native path also compresses the upload:
gzip on ESP8266 and RP2040, which inflate it at reboot, and a deflate
stream on ESP32/LibreTiny, which inflate it as it arrives.) Falls back to
cleartext over the LAN. (The native path also supports gzip compression
on ESP8266, where flash space is tight; on ESP32/RP2040/LibreTiny the
backend reports ``supports_compression() == false`` and the firmware is
sent uncompressed regardless of which platform is used.) Falls back to
``web_server`` only when that is the only available platform.
"""
# Use a dict (insertion-ordered) instead of a list so error messages and
@@ -1391,12 +1378,6 @@ def _upload_via_native_api(
ota_type = espota2.OTA_TYPE_UPDATE_PARTITION_TABLE
elif getattr(args, "bootloader", False):
check_partition_access("--bootloader")
if (
getattr(args, "file", None) is None
and (native := native_backend())
and (hint := getattr(native, "missing_image_hint", lambda: None)())
):
raise EsphomeError(hint)
binary = CORE.bootloader_bin
ota_type = espota2.OTA_TYPE_UPDATE_BOOTLOADER
if getattr(args, "file", None) is not None:
@@ -1772,18 +1753,6 @@ def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
if (
CORE.skip_bootloader
and CORE.is_esp32
and CORE.using_toolchain_esp_idf
and any(
get_port_type(device) == PortType.SERIAL for device in (args.device or [])
)
):
# Fail before the compile: the result could never flash over serial.
# Elsewhere the flag is ignored, so serial stays fine there.
_LOGGER.error("--skip-bootloader builds cannot be flashed over serial")
return 1
exit_code = write_cpp(config)
if exit_code != 0:
return exit_code
@@ -2132,9 +2101,155 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
from esphome.cli.rename import command_rename as run
from esphome import yaml_util
return run(args, config)
new_name = args.name
for c in new_name:
if c not in ALLOWED_NAME_CHARS:
safe_print(
color(
AnsiFore.BOLD_RED,
f"'{c}' is an invalid character for names. Valid characters are: "
f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)",
)
)
return 1
# Load existing yaml file
raw_contents = CORE.config_path.read_text(encoding="utf-8")
yaml = yaml_util.load_yaml(CORE.config_path)
if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]:
safe_print(
color(
AnsiFore.BOLD_RED, "Complex YAML files cannot be automatically renamed."
)
)
return 1
old_name = yaml[CONF_ESPHOME][CONF_NAME]
match = re.match(r"^\$\{?([a-zA-Z0-9_]+)\}?$", old_name)
if match is None:
# Only swap the ``name:`` line that sits directly under the
# top-level ``esphome:`` block. A naked ``re.sub`` would
# also clobber any other ``name:`` line whose value happens
# to match (e.g. a sensor / output / wifi entry sharing the
# device's hostname), silently rewriting unrelated user
# configuration. The pattern anchors:
# - at the start of the line so ``friendly_name:``,
# ``device_name:`` etc. don't match the trailing ``name:``
# substring; and
# - at the end of the value (lookahead for whitespace +
# comment + EOL) so ``old_name`` doesn't match as a
# prefix of a longer value (``kitchen`` vs ``kitchen2``).
name_pattern = re.compile(
rf"^(\s*)name:\s+[\"']?{re.escape(old_name)}[\"']?(?=\s*(?:#|$))"
)
out_lines: list[str] = []
in_esphome_block = False
for line in raw_contents.splitlines(keepends=True):
if line and not line[0].isspace() and line.strip():
in_esphome_block = line.lstrip().startswith("esphome:")
out_lines.append(line)
continue
if in_esphome_block:
line = name_pattern.sub(rf'\1name: "{new_name}"', line, count=1)
out_lines.append(line)
new_raw = "".join(out_lines)
else:
old_name = yaml[CONF_SUBSTITUTIONS][match.group(1)]
if (
len(
re.findall(
rf"^\s+{match.group(1)}:\s+[\"']?{old_name}[\"']?",
raw_contents,
flags=re.MULTILINE,
)
)
> 1
):
safe_print(
color(AnsiFore.BOLD_RED, "Too many matches in YAML to safely rename")
)
return 1
new_raw = re.sub(
rf"^(\s+{match.group(1)}):\s+[\"']?{old_name}[\"']?",
f'\\1: "{new_name}"',
raw_contents,
flags=re.MULTILINE,
)
# ``new_name == old_name`` (after substitution resolution) is
# a no-op rewrite that would still queue a pointless re-flash.
# Catch it before the path-equality check below — covers the
# case where the config filename doesn't match the device name
# (e.g. ``weird-file.yaml`` whose ``esphome.name`` is
# ``kitchen``; running ``esphome rename weird-file.yaml kitchen``
# would otherwise just re-flash the same hostname).
if new_name == old_name:
safe_print(
color(
AnsiFore.BOLD_RED,
f"'{new_name}' is already the device's name.",
)
)
return 1
new_path: Path = CORE.config_dir / (new_name + ".yaml")
if new_path.resolve() == CORE.config_path.resolve():
safe_print(
color(
AnsiFore.BOLD_RED,
f"'{new_name}' is already the device's name.",
)
)
return 1
if new_path.exists():
safe_print(
color(
AnsiFore.BOLD_RED,
f"Cannot rename: {new_path} already exists. "
"Refusing to overwrite an existing configuration.",
)
)
return 1
safe_print(
f"Updating {color(AnsiFore.CYAN, str(CORE.config_path))} to {color(AnsiFore.CYAN, str(new_path))}"
)
print()
new_path.write_text(new_raw, encoding="utf-8")
rc = run_external_process(*ESPHOME_COMMAND, "config", str(new_path))
if rc != 0:
safe_print(color(AnsiFore.BOLD_RED, "Rename failed. Reverting changes."))
new_path.unlink()
return 1
cli_args = [
"run",
str(new_path),
"--no-logs",
"--device",
CORE.address,
]
if args.dashboard:
cli_args.insert(0, "--dashboard")
try:
rc = run_external_process(*ESPHOME_COMMAND, *cli_args)
except KeyboardInterrupt:
rc = 1
if rc != 0:
new_path.unlink()
return 1
if CORE.config_path != new_path:
CORE.config_path.unlink()
safe_print(color(AnsiFore.BOLD_GREEN, "SUCCESS"))
print()
return 0
PRE_CONFIG_ACTIONS = {
@@ -2169,15 +2284,6 @@ SIMPLE_CONFIG_ACTIONS = [
]
def _add_skip_bootloader_arg(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--skip-bootloader",
help="Do not build the bootloader or the factory image; "
"the result can only be flashed over OTA.",
action="store_true",
)
def _add_states_args(parser: argparse.ArgumentParser) -> None:
"""Add mutually exclusive ``--states``/``--no-states`` flags to a parser.
@@ -2319,7 +2425,6 @@ def parse_args(argv):
help="Only generate source code, do not compile.",
action="store_true",
)
_add_skip_bootloader_arg(parser_compile)
parser_upload = subparsers.add_parser(
"upload",
@@ -2383,7 +2488,7 @@ def parse_args(argv):
"-r",
action="store_true",
help="Reset the device before starting serial logs.",
default=get_bool_env("ESPHOME_SERIAL_LOGGING_RESET"),
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
)
_add_states_args(parser_logs)
@@ -2416,7 +2521,6 @@ def parse_args(argv):
parser_run.add_argument(
"--no-logs", help="Disable starting logs.", action="store_true"
)
_add_skip_bootloader_arg(parser_run)
_add_states_args(parser_run)
@@ -2425,7 +2529,7 @@ def parse_args(argv):
"-r",
action="store_true",
help="Reset the device before starting serial logs.",
default=get_bool_env("ESPHOME_SERIAL_LOGGING_RESET"),
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
)
parser_run.add_argument(
"--ota-platform",
@@ -2676,7 +2780,6 @@ def run_esphome(argv):
CORE.config_path = conf_path
CORE.dashboard = args.dashboard
CORE.skip_bootloader = getattr(args, "skip_bootloader", False)
if args.toolchain is not None:
# CLI toolchain wins over esp32.toolchain in YAML.
CORE.toolchain = args.toolchain
+2 -9
View File
@@ -148,13 +148,6 @@ class AddressCache:
continue
hostname, ips = arg.split("=", 1)
# Normalize hostname for consistent lookups
normalized = normalize_hostname(hostname.strip())
addresses = [ip for value in ips.split(",") if (ip := value.strip())]
if not normalized or not addresses:
_LOGGER.warning(
"Invalid cache entry: %s (hostname and at least one address are required)",
arg,
)
continue
cache[normalized] = addresses
normalized = normalize_hostname(hostname)
cache[normalized] = [ip.strip() for ip in ips.split(",")]
return cache
+15 -8
View File
@@ -24,10 +24,9 @@ from esphome.core import EsphomeError, Version
from esphome.framework_helpers import str_to_lst_of_str
from esphome.platformio.registry import (
Download,
PackageSpec,
Resolver,
get_systype,
install_packages,
install_package,
prefetch_packages,
)
@@ -152,14 +151,14 @@ def check_and_install(framework_version: Version) -> InstalledPaths:
toolchain_path = get_toolchain_path()
# One spec per package: the prefetch and the installs must agree
specs = (
PackageSpec(
(
FRAMEWORK_PACKAGE,
release.tag,
framework_path,
ESPHOME_ARDUINO8266_FRAMEWORK_MIRRORS,
("cores/esp8266", "tools/sdk", "libraries"),
),
PackageSpec(
(
TOOLCHAIN_PACKAGE,
TOOLCHAIN_VERSION,
toolchain_path,
@@ -175,10 +174,18 @@ def check_and_install(framework_version: Version) -> InstalledPaths:
resolvers[FRAMEWORK_PACKAGE] = release.download
if not ESPHOME_ARDUINO8266_TOOLCHAIN_MIRRORS:
resolvers[TOOLCHAIN_PACKAGE] = toolchain_download
# Fetch both archives at once; the installs verify and extract them.
# One spec list for both, so the two phases cannot drift.
prefetch_packages(specs, downloads_dir, resolvers)
install_packages(specs, downloads_dir, resolvers)
# Fetch both archives at once; the installs below verify and extract
prefetch_packages([spec[:4] for spec in specs], downloads_dir, resolvers)
for name, version, dest, mirrors, expect in specs:
install_package(
name,
version,
dest,
mirrors,
downloads_dir,
expect=expect,
resolve=resolvers.get(name),
)
return InstalledPaths(
framework=framework_path, toolchain=toolchain_path, ninja=ninja_path
)
+3 -28
View File
@@ -1065,10 +1065,10 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
" rspfile_content = $in_newline",
" description = LINK $out",
"rule elf2bin",
# --flash_size follows PlatformIO: ldscript filename first, then
# upload.maximum_size, so the header always matches the layout.
# --flash_size deliberately stays board-derived, as under
# PlatformIO (which reads upload.maximum_size, not the ldscript).
# -W: the framework's own elf2bin.py trips SyntaxWarning on 3.12+.
f" command = $python -W ignore::SyntaxWarning {_q(framework / 'tools' / 'elf2bin.py')} --eboot {_q(framework / 'bootloaders' / 'eboot' / 'eboot.elf')} --app $in --flash_mode {flash_mode} --flash_freq {_FLASH_FREQ_MHZ} --flash_size {_elf2bin_flash_size(board, flash_ld_name)} --path {_q(toolchain_bin)} --out $out",
f" command = $python -W ignore::SyntaxWarning {_q(framework / 'tools' / 'elf2bin.py')} --eboot {_q(framework / 'bootloaders' / 'eboot' / 'eboot.elf')} --app $in --flash_mode {flash_mode} --flash_freq {_FLASH_FREQ_MHZ} --flash_size {_flash_size_str(BOARDS[board][KEY_FLASH_SIZE])} --path {_q(toolchain_bin)} --out $out",
" description = BIN $out",
"rule copy",
" command = $python $buildtool copy $in $out",
@@ -1189,28 +1189,3 @@ def _flash_size_str(flash_size: int) -> str:
"""Flash size argument for elf2bin (e.g. ``4M``, ``512K``)."""
mb = 1024 * 1024
return f"{flash_size // mb}M" if flash_size >= mb else f"{flash_size // 1024}K"
# Same pattern PlatformIO's _get_flash_size applies to the ldscript path
_LD_FLASH_SIZE_RE = re.compile(r"\.flash\.(\d+[mk]).*\.ld")
# The framework elf2bin.py's --flash_size choices
_ELF2BIN_FLASH_SIZES = frozenset({"256K", "512K", "1M", "2M", "4M", "8M", "16M"})
def _elf2bin_flash_size(board: str, flash_ld_name: str) -> str:
"""Image-header flash size as PlatformIO derives it: ldscript filename,
else board_upload.maximum_size, else the board table. The SDK clamps the
chip to the header size at boot, so a header smaller than the linked
layout breaks OTA writes on the running device."""
if match := _LD_FLASH_SIZE_RE.search(flash_ld_name):
token = match.group(1)
base = 1024 if token[-1] == "k" else 1024 * 1024
return _flash_size_str(int(token[:-1]) * base)
if max_size := _pio_option("board_upload.maximum_size", ""):
if (
not max_size.isdigit()
or (size := _flash_size_str(int(max_size))) not in _ELF2BIN_FLASH_SIZES
):
raise EsphomeError(f"Invalid board_upload.maximum_size value {max_size!r}")
return size
return _flash_size_str(BOARDS[board][KEY_FLASH_SIZE])
+17 -110
View File
@@ -21,7 +21,7 @@ from esphome.framework_helpers import (
get_project_cxx_compile_flags,
get_project_link_flags,
)
from esphome.helpers import get_bool_env, mkdir_p, write_file_if_changed
from esphome.helpers import mkdir_p, write_file_if_changed
_LOGGER = logging.getLogger(__name__)
@@ -36,83 +36,6 @@ list(FILTER esphome_cxx_compile_options EXCLUDE REGEX "^-std=")
list(APPEND esphome_cxx_compile_options "-std={standard}")
idf_build_set_property(CXX_COMPILE_OPTIONS "${{esphome_cxx_compile_options}}")"""
# Drops the app archive from ldgen's inputs so app-only edits skip the
# sections.ld regeneration. Safe: no mapping fragment references it
# (run_compile re-checks each build). Filters only the top-level call;
# the prior definition stays reachable with an underscore prefix.
_LDGEN_OVERRIDE = """\
if(COMMAND __ldgen_get_lib_deps_of_target)
set_property(GLOBAL PROPERTY ESPHOME_LDGEN_ARMED 1)
function(__ldgen_get_lib_deps_of_target target out_list_var)
if(NOT COMMAND ___ldgen_get_lib_deps_of_target)
message(FATAL_ERROR "ESPHome ldgen override lost the original "
"implementation; set ESPHOME_LDGEN_FULL_DEPS=1 and rebuild.")
endif()
___ldgen_get_lib_deps_of_target(${target} ${out_list_var})
if(out_list_var STREQUAL "ldgen_libraries")
set_property(GLOBAL PROPERTY ESPHOME_LDGEN_FILTERED 1)
list(LENGTH ${out_list_var} esphome_ldgen_before)
list(REMOVE_ITEM ${out_list_var} idf::src __idf_src)
list(LENGTH ${out_list_var} esphome_ldgen_after)
if(esphome_ldgen_before EQUAL esphome_ldgen_after)
message(@SEVERITY@ "ESPHome ldgen app archive exclusion matched "
"nothing; app edits will regenerate sections.ld.")
endif()
endif()
set(${out_list_var} "${${out_list_var}}" PARENT_SCOPE)
endfunction()
else()
message(@MISSING@ "ESPHome ldgen override target not found; "
"app edits will regenerate sections.ld.")
endif()"""
# lwip sources that compile to empty objects with the option off (their own
# #if guard). (option, regex valid for both Python and CMake); a source is
# only dropped when its option is defined and off, so a renamed option
# keeps it.
LWIP_EMPTY_SOURCES: tuple[tuple[str, str], ...] = (
("CONFIG_LWIP_PPP_SUPPORT", "/netif/ppp/"),
("CONFIG_LWIP_IPV6", "/core/ipv6/"),
("CONFIG_LWIP_AUTOIP", "/core/ipv4/autoip[.]c$"),
("CONFIG_LWIP_STATS", "/core/stats[.]c$"),
)
# Drift guard only: keep every lwip source.
LWIP_FULL_SOURCES_ENV = "ESPHOME_LWIP_FULL_SOURCES"
# Drops the empty objects after project(), once the lwip target exists.
_LWIP_EMPTY_SOURCES_FILTER = f"""\
idf_build_get_property(esphome_build_components BUILD_COMPONENTS)
if(lwip IN_LIST esphome_build_components AND NOT DEFINED ENV{{{LWIP_FULL_SOURCES_ENV}}})
idf_component_get_property(esphome_lwip_lib lwip COMPONENT_LIB)
get_target_property(esphome_lwip_srcs ${{esphome_lwip_lib}} SOURCES)
@FILTERS@
set_property(TARGET ${{esphome_lwip_lib}} PROPERTY SOURCES ${{esphome_lwip_srcs}})
endif()"""
def lwip_empty_source_gate(option: str, regex: str) -> str:
return (
f" if(DEFINED {option} AND NOT {option})\n"
f' list(FILTER esphome_lwip_srcs EXCLUDE REGEX "{regex}")\n'
" endif()"
)
def _lwip_empty_sources_filter() -> str:
gates = "\n".join(lwip_empty_source_gate(*entry) for entry in LWIP_EMPTY_SOURCES)
return _LWIP_EMPTY_SOURCES_FILTER.replace("@FILTERS@", gates)
# Runs after project() so the walk has happened; catches the remaining
# silent path where the top-level out-var was renamed.
_LDGEN_OVERRIDE_CHECK = """\
get_property(esphome_ldgen_armed GLOBAL PROPERTY ESPHOME_LDGEN_ARMED)
get_property(esphome_ldgen_filtered GLOBAL PROPERTY ESPHOME_LDGEN_FILTERED)
if(esphome_ldgen_armed AND NOT esphome_ldgen_filtered)
message(@SEVERITY@ "ESPHome ldgen override never filtered the app "
"archive; app edits will regenerate sections.ld.")
endif()"""
def get_available_components() -> list[str] | None:
"""List the built-in ESP-IDF components from ``project_description.json``.
@@ -161,10 +84,10 @@ def _cmake_quote(value: str) -> str:
# CONFIG_APP_BUILD_BOOTLOADER is hidden and force-selected, so it can only be
# cleared at the CMake level (the same state IDF's RAM-app build type uses).
# The macro is IDF's __build_process_project_includes plus two added lines;
# the flag is ignored and the bootloader builds as usual if IDF changes it.
# esphome.espidf.bootloader falls back to in-tree if an IDF update changes it.
IDF_BOOTLOADER_OVERRIDE = """\
# ESPHome bootloader skip switch; see esphome/espidf/toolchain.py.
if(ESPHOME_SKIP_BOOTLOADER)
# ESPHome cached bootloader switch; see esphome/espidf/bootloader.py.
if(ESPHOME_USE_CACHED_BOOTLOADER)
macro(__build_process_project_includes)
idf_build_get_property(sdkconfig_cmake SDKCONFIG_CMAKE)
include(${sdkconfig_cmake})
@@ -193,8 +116,8 @@ if(ESPHOME_SKIP_BOOTLOADER)
endif()
"""
# The lines the override adds to IDF's macro; idf_macro_matches() below
# strips them before comparing with the live macro.
# The lines the override adds to IDF's macro; the staleness tripwire in
# esphome.espidf.bootloader strips them before comparing with the live macro.
BOOTLOADER_OVERRIDE_ADDED_LINES = (
'set(CONFIG_APP_BUILD_BOOTLOADER "")',
"idf_build_get_property(idf_target IDF_TARGET)",
@@ -223,9 +146,12 @@ _EXPECTED_MACRO = [
]
def idf_macro_matches(idf_path: Path) -> bool:
def idf_macro_matches() -> bool:
"""Whether IDF's macro still matches the copy the override replays."""
build_cmake = idf_path / "tools" / "cmake" / "build.cmake"
from esphome.espidf import toolchain
# pylint: disable-next=protected-access
build_cmake = toolchain._get_idf_path() / "tools" / "cmake" / "build.cmake"
live = _normalized_macro(build_cmake.read_text(encoding="utf-8"))
return live == _EXPECTED_MACRO
@@ -275,22 +201,6 @@ def get_project_cmakelists(
else ""
)
# Stops the ~3s sections.ld regeneration on app-only edits; see
# _LDGEN_OVERRIDE. ESPHOME_LDGEN_FULL_DEPS=1 restores stock behavior;
# ESPHOME_LDGEN_STRICT=1 (CI) fails the configure when an IDF bump
# breaks the override instead of degrading to stock deps.
if get_bool_env("ESPHOME_LDGEN_FULL_DEPS"):
ldgen_override = ""
ldgen_override_check = ""
else:
strict = get_bool_env("ESPHOME_LDGEN_STRICT")
severity = "FATAL_ERROR" if strict else "WARNING"
missing = "FATAL_ERROR" if strict else "STATUS"
ldgen_override = _LDGEN_OVERRIDE.replace("@SEVERITY@", severity).replace(
"@MISSING@", missing
)
ldgen_override_check = _LDGEN_OVERRIDE_CHECK.replace("@SEVERITY@", severity)
# CMake variables registered via cg.add_cmake_arg(). Emitted before
# include(project.cmake) so values like EXCLUDE_COMPONENTS are already
# set when project.cmake seeds the component list, and on minimal
@@ -369,8 +279,6 @@ set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
{IDF_BOOTLOADER_OVERRIDE}
{ldgen_override}
{cpp_standard_options}
{cxx_compile_options}
@@ -383,22 +291,21 @@ include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
project({CORE.name})
{ldgen_override_check}
{_lwip_empty_sources_filter()}
# Record the bootloader customization channels the cache cannot replay;
# esphome.espidf.bootloader bypasses the cache when any is non-empty.
idf_build_get_property(esphome_bl_dirs BOOTLOADER_EXTRA_COMPONENT_DIRS)
idf_build_get_property(esphome_bl_args EXTRA_CMAKE_ARGS)
file(WRITE ${{CMAKE_BINARY_DIR}}/esphome_bootloader_hooks.txt
"${{esphome_bl_dirs}}${{BOOTLOADER_IGNORE_EXTRA_COMPONENT}}${{esphome_bl_args}}")
# Emit per-memory-type JSON size data for ESPHome to read post-build.
# json2 stays small; raw dumps every symbol (~2s on a large map) and
# this command runs inside the link edge, blocking everything downstream.
# The map is a BYPRODUCT so ninja knows the link writes it; IDF's size
# target depends on the map and can then be built in the same run as all.
# IDF's cmakev2 declares the map itself, so drop this line on that switch.
add_custom_command(
TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=json2
-o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json
${{CMAKE_PROJECT_NAME}}.map
BYPRODUCTS ${{CMAKE_BINARY_DIR}}/${{CMAKE_PROJECT_NAME}}.map
WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}}
VERBATIM
)
-2
View File
@@ -1,2 +0,0 @@
"""Commands of the esphome command line, one module each, imported by
__main__ only when they run so that startup stays light."""
-164
View File
@@ -1,164 +0,0 @@
"""``esphome rename``."""
from __future__ import annotations
import argparse
from pathlib import Path
import re
from esphome import yaml_edit, yaml_util
from esphome.const import (
ALLOWED_NAME_CHARS,
CONF_ESPHOME,
CONF_NAME,
CONF_SUBSTITUTIONS,
)
from esphome.core import CORE, EsphomeError
from esphome.log import AnsiFore, color
from esphome.types import ConfigType
from esphome.util import ESPHOME_COMMAND, run_external_process, safe_print
def _revert(new_path: Path, why: str) -> int:
"""Say why the rename stopped and take the new file back; an orphan the
next attempt would trip over is reported."""
safe_print(color(AnsiFore.BOLD_RED, f"Rename failed: {why}"))
try:
new_path.unlink(missing_ok=True)
except OSError as err:
safe_print(color(AnsiFore.BOLD_RED, f"Could not remove {new_path}: {err}"))
return 1
def command_rename(args: argparse.Namespace, config: ConfigType) -> int | None:
"""Rename the device: a new file with the name line rewritten, validated
and installed, then the old file removed."""
new_name = args.name
for c in new_name:
if c not in ALLOWED_NAME_CHARS:
safe_print(
color(
AnsiFore.BOLD_RED,
f"'{c}' is an invalid character for names. Valid characters are: "
f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)",
)
)
return 1
yaml = yaml_util.load_yaml(CORE.config_path)
def name_edit() -> tuple[str, yaml_edit.LineEdit]:
"""The name and the line to rewrite: the name's own line, or the
substitution's line it comes from, as a plain value in this file."""
esphome_conf = yaml.get(CONF_ESPHOME)
if not isinstance(esphome_conf, dict) or CONF_NAME not in esphome_conf:
raise EsphomeError(f"no '{CONF_ESPHOME}: {CONF_NAME}:' in the file")
old_name = str(esphome_conf[CONF_NAME])
mapping, field = esphome_conf, CONF_NAME
if match := re.match(r"^\$\{?([a-zA-Z0-9_]+)\}?$", old_name):
mapping, field = yaml.get(CONF_SUBSTITUTIONS), match.group(1)
if not isinstance(mapping, dict) or field not in mapping:
raise EsphomeError(f"the substitution '{field}' is not in the file")
old_name = str(mapping[field])
# Only read here; the rewritten text goes to a new file, so the
# source may live anywhere the config path points to
source = yaml_edit.source_of(mapping, field)
if source is None or source[0].resolve() != CORE.config_path.resolve():
raise EsphomeError(f"'{field}' was not read from {CORE.config_path}")
doc, line_no = source
text = yaml_edit.line_at(doc, line_no)
if (line_match := yaml_edit.field_line_re(field, old_name).match(text)) is None:
raise EsphomeError(f"'{field}' is not a plain value on {doc}:{line_no + 1}")
# The new value is always quoted, whatever the old line had
return old_name, yaml_edit.LineEdit(
doc, line_no, text, yaml_edit.rewrite(line_match, new_name, '"')
)
try:
old_name, edit = name_edit()
except EsphomeError as err:
safe_print(
color(
AnsiFore.BOLD_RED,
f"Complex YAML files cannot be automatically renamed: {err}",
)
)
return 1
# ``new_name == old_name`` (after substitution resolution) is
# a no-op rewrite that would still queue a pointless re-flash.
# Catch it before the path-equality check below — covers the
# case where the config filename doesn't match the device name
# (e.g. ``weird-file.yaml`` whose ``esphome.name`` is
# ``kitchen``; running ``esphome rename weird-file.yaml kitchen``
# would otherwise just re-flash the same hostname).
if new_name == old_name:
safe_print(
color(
AnsiFore.BOLD_RED,
f"'{new_name}' is already the device's name.",
)
)
return 1
new_path: Path = CORE.config_dir / (new_name + ".yaml")
if new_path.resolve() == CORE.config_path.resolve():
safe_print(
color(
AnsiFore.BOLD_RED,
f"'{new_name}' is already the device's name.",
)
)
return 1
if new_path.exists():
safe_print(
color(
AnsiFore.BOLD_RED,
f"Cannot rename: {new_path} already exists. "
"Refusing to overwrite an existing configuration.",
)
)
return 1
safe_print(
f"Updating {color(AnsiFore.CYAN, str(CORE.config_path))} to {color(AnsiFore.CYAN, str(new_path))}"
)
print()
try:
yaml_edit.write_keeping_mode(
new_path,
yaml_edit.rewritten_text(yaml_edit.read_text(CORE.config_path), [edit]),
like=CORE.config_path,
)
except EsphomeError as err:
return _revert(new_path, str(err))
if run_external_process(*ESPHOME_COMMAND, "config", str(new_path)) != 0:
return _revert(new_path, "the new configuration does not validate")
cli_args = [
"run",
str(new_path),
"--no-logs",
"--device",
CORE.address,
]
if args.dashboard:
cli_args.insert(0, "--dashboard")
try:
rc = run_external_process(*ESPHOME_COMMAND, *cli_args)
except KeyboardInterrupt:
rc = 1
if rc != 0:
return _revert(
new_path,
"the install did not finish; the device may already run the new name",
)
CORE.config_path.unlink()
safe_print(color(AnsiFore.BOLD_GREEN, "SUCCESS"))
print()
return 0
-1
View File
@@ -40,7 +40,6 @@ from esphome.cpp_generator import ( # noqa: F401
progmem_array,
safe_exp,
set_cpp_standard,
shared_progmem_array,
statement,
static_const_array,
static_function,
+10 -4
View File
@@ -62,10 +62,16 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_TVOC, var.set_tvoc)
await sensors(CONF_VERSION, var.set_version)
await sensors(CONF_RESISTANCE, var.set_resistance)
sens = await sensor.new_sensor(config[CONF_TVOC])
cg.add(var.set_tvoc(sens))
if version_config := config.get(CONF_VERSION):
sens = await sensor.new_sensor(version_config)
cg.add(var.set_version(sens))
if resistance_config := config.get(CONF_RESISTANCE):
sens = await sensor.new_sensor(resistance_config)
cg.add(var.set_resistance(sens))
AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value(
+7 -3
View File
@@ -57,6 +57,10 @@ async def to_code(config: ConfigType) -> None:
await i2c.register_i2c_device(var, config)
cg.add(var.set_variant(config[CONF_VARIANT]))
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
await sensors(CONF_HUMIDITY, var.set_humidity_sensor)
if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature_sensor(sens))
if humidity := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity)
cg.add(var.set_humidity_sensor(sens))
+23 -7
View File
@@ -74,10 +74,26 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await ble_client.register_ble_node(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_FLOW, var.set_flow_sensor)
await sensors(CONF_HEAD, var.set_head_sensor)
await sensors(CONF_POWER, var.set_power_sensor)
await sensors(CONF_CURRENT, var.set_current_sensor)
await sensors(CONF_SPEED, var.set_speed_sensor)
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
if flow_config := config.get(CONF_FLOW):
sens = await sensor.new_sensor(flow_config)
cg.add(var.set_flow_sensor(sens))
if head_config := config.get(CONF_HEAD):
sens = await sensor.new_sensor(head_config)
cg.add(var.set_head_sensor(sens))
if power_config := config.get(CONF_POWER):
sens = await sensor.new_sensor(power_config)
cg.add(var.set_power_sensor(sens))
if current_config := config.get(CONF_CURRENT):
sens = await sensor.new_sensor(current_config)
cg.add(var.set_current_sensor(sens))
if speed_config := config.get(CONF_SPEED):
sens = await sensor.new_sensor(speed_config)
cg.add(var.set_speed_sensor(sens))
if voltage_config := config.get(CONF_VOLTAGE):
sens = await sensor.new_sensor(voltage_config)
cg.add(var.set_voltage_sensor(sens))
+7 -3
View File
@@ -46,6 +46,10 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
await sensors(CONF_HUMIDITY, var.set_humidity_sensor)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity_sensor(sens))
+7 -3
View File
@@ -48,6 +48,10 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
await sensors(CONF_HUMIDITY, var.set_humidity_sensor)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity_sensor(sens))
+7 -3
View File
@@ -48,6 +48,10 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await ble_client.register_ble_node(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_BATTERY_LEVEL, var.set_battery)
await sensors(CONF_ILLUMINANCE, var.set_illuminance)
if battery_level_config := config.get(CONF_BATTERY_LEVEL):
sens = await sensor.new_sensor(battery_level_config)
cg.add(var.set_battery(sens))
if illuminance_config := config.get(CONF_ILLUMINANCE):
sens = await sensor.new_sensor(illuminance_config)
cg.add(var.set_illuminance(sens))
+1 -3
View File
@@ -13,7 +13,6 @@ from esphome.components.logger import request_log_listener
from esphome.components.noise import ( # noqa: F401
ENCRYPTION_SCHEMA,
decode_encryption_key,
enable_spare_ephemeral,
encryption_schema,
new_psk_progmem,
validate_encryption_key,
@@ -601,7 +600,7 @@ async def to_code(config: ConfigType) -> None:
if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None:
if key := encryption_config.get(CONF_KEY):
cg.add(var.set_noise_psk(new_psk_progmem(key)))
cg.add(var.set_noise_psk(new_psk_progmem(config[CONF_ID], key)))
cg.add_define("USE_API_NOISE_PSK_FROM_YAML")
else:
# No key provided, but encryption desired
@@ -614,7 +613,6 @@ async def to_code(config: ConfigType) -> None:
# and plaintext disabled. Only a factory reset can remove it.
cg.add_define("USE_API_PLAINTEXT")
cg.add_define("USE_API_NOISE")
enable_spare_ephemeral()
else:
cg.add_define("USE_API_PLAINTEXT")
-6
View File
@@ -317,15 +317,9 @@ class APIConnection final : public APIServerConnectionBase {
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg);
#endif
// How long a new connection holds off the spare ephemeral refill
static constexpr uint32_t CONNECT_GRACE_MS = 1000;
bool is_authenticated() {
return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::AUTHENTICATED;
}
// An older unauthenticated connection is a stale half open client and does not count
bool is_still_connecting(uint32_t now) {
return !this->is_authenticated() && now - this->last_traffic_ < CONNECT_GRACE_MS;
}
bool is_connection_setup() {
return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::CONNECTED ||
this->is_authenticated();
+2 -21
View File
@@ -162,13 +162,6 @@ void APIServer::loop() {
this->accept_new_connections_();
}
const bool connected = network::is_connected();
#ifdef USE_NOISE_SPARE_EPHEMERAL
if (connected && !noise::has_spare_ephemeral()) {
this->refill_spare_ephemeral_();
}
#endif
if (this->api_connection_count_ == 0) {
// Check reboot timeout - done in loop to avoid scheduler heap churn
// (cancelled scheduler items sit in heap memory until their scheduled time).
@@ -185,7 +178,8 @@ void APIServer::loop() {
}
// Process clients and remove disconnected ones in a single pass
if (!connected) {
// Check network connectivity once for all clients
if (!network::is_connected()) {
// Network is down - disconnect all clients
for (auto &client : this->active_clients()) {
client->on_fatal_error();
@@ -213,19 +207,6 @@ void APIServer::loop() {
}
}
#ifdef USE_NOISE_SPARE_EPHEMERAL
// An OTA handshake is not visible here and just pays the refill it triggered
void APIServer::refill_spare_ephemeral_() {
const uint32_t now = App.get_loop_component_start_time();
for (auto &client : this->active_clients()) {
if (client->is_still_connecting(now)) {
return;
}
}
noise::prepare_spare_ephemeral();
}
#endif
void APIServer::remove_client_(uint8_t client_index) {
auto &client = this->clients_[client_index];
+1 -4
View File
@@ -5,7 +5,7 @@
#include "api_buffer.h"
// Must precede clients_ so APIConnection is complete for default_delete (libc++).
#include "api_connection.h"
#if defined(USE_API_NOISE) || defined(USE_NOISE_SPARE_EPHEMERAL)
#ifdef USE_API_NOISE
// Only present in the build when the noise component is loaded
#include "esphome/components/noise/noise.h"
#endif
@@ -367,9 +367,6 @@ class APIServer final : public Component
uint8_t provisioning_source_{0};
#endif
#ifdef USE_NOISE_SPARE_EPHEMERAL
void refill_spare_ephemeral_();
#endif
#ifdef USE_API_NOISE
noise::NoiseContext noise_ctx_;
#ifndef USE_API_NOISE_PSK_FROM_YAML
+7 -3
View File
@@ -35,6 +35,10 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_AS3935_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_DISTANCE, hub.set_distance_sensor)
await sensors(CONF_LIGHTNING_ENERGY, hub.set_energy_sensor)
if distance_config := config.get(CONF_DISTANCE):
sens = await sensor.new_sensor(distance_config)
cg.add(hub.set_distance_sensor(sens))
if lightning_energy_config := config.get(CONF_LIGHTNING_ENERGY):
sens = await sensor.new_sensor(lightning_energy_config)
cg.add(hub.set_energy_sensor(sens))
+6 -3
View File
@@ -241,9 +241,12 @@ async def to_code(config: ConfigType) -> None:
if peak_current_config := conf.get(CONF_PEAK_CURRENT):
sens = await sensor.new_sensor(peak_current_config)
cg.add(var.set_peak_current_sensor(i, sens))
sensors = sensor.sub_sensors(config)
await sensors(CONF_FREQUENCY, var.set_freq_sensor)
await sensors(CONF_CHIP_TEMPERATURE, var.set_chip_temperature_sensor)
if frequency_config := config.get(CONF_FREQUENCY):
sens = await sensor.new_sensor(frequency_config)
cg.add(var.set_freq_sensor(sens))
if chip_temperature_config := config.get(CONF_CHIP_TEMPERATURE):
sens = await sensor.new_sensor(chip_temperature_config)
cg.add(var.set_chip_temperature_sensor(sens))
cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY]))
cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES]))
cg.add(var.set_pga_gain(config[CONF_GAIN_PGA]))
@@ -36,13 +36,14 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_ID])
parent = await cg.get_variable(config[CONF_ID])
if phase_cfg := config.get(CONF_PHASE_STATUS):
for i, key in enumerate(PHASE_KEYS):
if sub_phase_cfg := phase_cfg.get(key):
sens = await text_sensor.new_text_sensor(sub_phase_cfg)
cg.add(hub.set_phase_status_text_sensor(i, sens))
cg.add(parent.set_phase_status_text_sensor(i, sens))
text_sensors = text_sensor.sub_text_sensors(config)
await text_sensors(CONF_FREQUENCY_STATUS, hub.set_freq_status_text_sensor)
if freq_status_config := config.get(CONF_FREQUENCY_STATUS):
sens = await text_sensor.new_text_sensor(freq_status_config)
cg.add(parent.set_freq_status_text_sensor(sens))
-4
View File
@@ -14,10 +14,6 @@ class AudioDac {
virtual bool is_muted() = 0;
virtual float volume() = 0;
/// Called from the main loop by the speaker driving this DAC once its audio clocks are running.
/// DACs that can only be configured while clocked (e.g. DSP state lost across clock stops) override this.
virtual void on_audio_started() {}
protected:
bool is_muted_{false};
};
+7 -3
View File
@@ -44,6 +44,10 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await register_bedjet_child(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_OUTLET_TEMPERATURE, var.set_outlet_temperature_sensor)
await sensors(CONF_AMBIENT_TEMPERATURE, var.set_ambient_temperature_sensor)
if outlet_temperature_sensor := config.get(CONF_OUTLET_TEMPERATURE):
sensor_var = await sensor.new_sensor(outlet_temperature_sensor)
cg.add(var.set_outlet_temperature_sensor(sensor_var))
if ambient_temperature_sensor := config.get(CONF_AMBIENT_TEMPERATURE):
sensor_var = await sensor.new_sensor(ambient_temperature_sensor)
cg.add(var.set_ambient_temperature_sensor(sensor_var))
@@ -17,13 +17,6 @@
#include "esphome/core/log.h"
#include <cstring>
#include <esp_idf_version.h>
// cancel_open exists from ESP-IDF 5.5.5 and 6.0.1.
#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 5) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0)) || \
ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 1)
#define BLUEDROID_HAS_CANCEL_OPEN
#endif
namespace esphome::bluetooth_connection {
@@ -70,12 +63,6 @@ void BluedroidGattClient::loop() {
// The one teardown safety net: a lost CLOSE_EVT, or a scheduled
// teardown whose OPEN_EVT never arrives.
if (millis() - this->disconnecting_started_ > ble_device_base::GATT_DISCONNECT_TIMEOUT_MS) {
if (this->conn_id_ == UNSET_CONN_ID) {
this->cancel_pending_open_();
} else if (st == ClientState::CONNECTING) {
// CONNECT_EVT came but OPEN_EVT was lost, so our open still holds the ACL link.
this->check_and_log_error_("esp_ble_gap_disconnect", esp_ble_gap_disconnect(this->remote_bda_));
}
ESP_LOGE(TAG, "[%d] Timeout waiting for teardown, forcing IDLE", this->connection_index_);
// Release before idling: a lost completion must not leak the cache.
this->release_services();
@@ -153,8 +140,6 @@ void BluedroidGattClient::tracker_connect_() {
this->services_released_ = false;
this->seen_mtu_ = false;
this->mtu_failed_ = false;
this->cancel_open_sent_ = false;
this->conn_id_ = UNSET_CONN_ID;
this->enable_loop();
this->set_state(ClientState::CONNECTING);
if (this->connection_type_ == ConnectionType::V3_WITHOUT_CACHE) {
@@ -198,28 +183,12 @@ int BluedroidGattClient::gatt_disconnect() {
// Arm the safety window: a lost OPEN_EVT must not leak the teardown.
this->disconnecting_started_ = millis();
this->enable_loop();
if (this->conn_id_ == UNSET_CONN_ID) {
// CANCEL_OPEN_EVT or a racing OPEN_EVT settles the slot.
this->cancel_pending_open_();
}
return 0;
}
this->unconditional_disconnect_();
return 0;
}
void BluedroidGattClient::cancel_pending_open_() {
#ifdef BLUEDROID_HAS_CANCEL_OPEN
esp_ble_gattc_cancel_open_params_t params{};
params.gattc_if = this->gattc_if_;
memcpy(params.remote_bda, this->remote_bda_, sizeof(esp_bd_addr_t));
// A refused cancel schedules no CANCEL_OPEN_EVT, so the teardown stays cancellable.
if (this->check_and_log_error_("esp_ble_gattc_cancel_open", esp_ble_gattc_cancel_open(&params)) == ESP_OK) {
this->cancel_open_sent_ = true;
}
#endif
}
void BluedroidGattClient::unconditional_disconnect_() {
ESP_LOGI(TAG, "[%d] Disconnecting (conn_id: %d)", this->connection_index_, this->conn_id_);
if (this->conn_id_ == UNSET_CONN_ID) {
@@ -245,10 +214,6 @@ bool BluedroidGattClient::cancel_gatt_disconnect() {
if (this->state() != ClientState::CONNECTING || !this->disconnect_pending()) {
return false;
}
if (this->cancel_open_sent_) {
// The cancel already went out, so CANCEL_OPEN_EVT ends it.
return false;
}
this->want_disconnect_ = false;
return true;
}
@@ -609,8 +574,6 @@ void BluedroidGattClient::handle_open_evt_(esp_ble_gattc_cb_param_t *param) {
this->listener_->on_connection_state(false, 0, param->open.status);
return;
}
// ALREADY_OPEN on an existing link sends no CONNECT_EVT, so take the id here.
this->conn_id_ = param->open.conn_id;
if (this->disconnect_pending()) {
// Open resolved with a teardown scheduled: close now (conn_id_ stays set
// so CLOSE_EVT still matches).
@@ -654,11 +617,6 @@ void BluedroidGattClient::handle_disconnect_evt_(esp_ble_gattc_cb_param_t *param
// Active close delivers CLOSE_EVT first; never walk back to DISCONNECTING.
return;
}
if (this->conn_id_ == UNSET_CONN_ID) {
// No CONNECT_EVT for this attempt yet, so the event belongs to an earlier
// link, such as one the teardown timeout forced down.
return;
}
// Passive disconnect: wait for CLOSE_EVT before going IDLE (reconnecting
// earlier makes the controller reject with 133 or assert) and before
// reporting - the wrapper frees the slot on the report, and a freed slot
@@ -687,10 +645,6 @@ bool BluedroidGattClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
case ESP_GATTC_CONNECT_EVT: {
if (!this->check_addr_(param->connect.remote_bda))
return false;
// Every client interface gets CONNECT_EVT for every new link; only an
// attempt in flight owns it. An idle slot must not adopt another slot's link.
if (this->state() != ClientState::CONNECTING)
break;
this->conn_id_ = param->connect.conn_id;
// MTU request here rather than OPEN_EVT, matching the IDF examples.
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->connect.conn_id);
@@ -702,15 +656,8 @@ bool BluedroidGattClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
break;
}
case ESP_GATTC_OPEN_EVT: {
if (!this->check_addr_(param->open.remote_bda)) {
// Our interface (filtered above), stale address, so nobody tracks this link.
if (param->open.status == ESP_GATT_OK || param->open.status == ESP_GATT_ALREADY_OPEN) {
ESP_LOGW(TAG, "[%d] Closing link left by an abandoned open", this->connection_index_);
this->check_and_log_error_("esp_ble_gattc_close", esp_ble_gattc_close(this->gattc_if_, param->open.conn_id));
return true;
}
if (!this->check_addr_(param->open.remote_bda))
return false;
}
this->handle_open_evt_(param);
break;
}
@@ -801,24 +748,6 @@ bool BluedroidGattClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
this->listener_->on_notify_data(param->notify.handle, param->notify.value, param->notify.value_len);
break;
}
#ifdef BLUEDROID_HAS_CANCEL_OPEN
case ESP_GATTC_CANCEL_OPEN_EVT: {
if (!this->check_addr_(param->cancel_open.remote_bda))
return false;
if (param->cancel_open.status != ESP_GATT_OK) {
// Too late to cancel: OPEN_EVT follows and settles the slot. The latch
// stays set because the event cannot be tied to this attempt's cancel.
break;
}
if (this->state() == ClientState::CONNECTING && this->disconnect_pending()) {
ESP_LOGD(TAG, "[%d] Pending open cancelled", this->connection_index_);
this->release_services();
this->set_idle_();
this->listener_->on_connection_state(false, 0, ESP_GATT_CONN_CONN_CANCEL);
}
break;
}
#endif
default:
break;
}
@@ -100,7 +100,6 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public
int handle_search_cmpl_(esp_gatt_status_t status);
void deliver_pending_search_();
void unconditional_disconnect_();
void cancel_pending_open_();
void set_idle_();
void set_disconnecting_();
esp_err_t update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,
@@ -134,9 +133,6 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public
bool seen_mtu_ : 1 {false};
// The MTU request was refused at CONNECT_EVT; OPEN_EVT reports instead.
bool mtu_failed_ : 1 {false};
// esp_ble_gattc_cancel_open() was accepted; CANCEL_OPEN_EVT or OPEN_EVT ends
// the attempt, so the scheduled teardown can no longer be cancelled.
bool cancel_open_sent_ : 1 {false};
// Search issued at OPEN_EVT overlaps the MTU exchange; discover_services()
// completes from it. Reset by set_idle_().
static_assert(static_cast<uint8_t>(SearchState::REPORT_PENDING) < (1 << 4), "search_state_ bitfield too narrow");
+18 -9
View File
@@ -131,15 +131,24 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
if await sensors(CONF_TEMPERATURE, var.set_temperature_sensor):
oversampling = config[CONF_TEMPERATURE][CONF_OVERSAMPLING]
cg.add(var.set_temperature_oversampling(oversampling))
if await sensors(CONF_PRESSURE, var.set_pressure_sensor):
cg.add(var.set_pressure_oversampling(config[CONF_PRESSURE][CONF_OVERSAMPLING]))
if await sensors(CONF_HUMIDITY, var.set_humidity_sensor):
cg.add(var.set_humidity_oversampling(config[CONF_HUMIDITY][CONF_OVERSAMPLING]))
await sensors(CONF_GAS_RESISTANCE, var.set_gas_resistance_sensor)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(var.set_temperature_oversampling(temperature_config[CONF_OVERSAMPLING]))
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING]))
if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity_sensor(sens))
cg.add(var.set_humidity_oversampling(humidity_config[CONF_OVERSAMPLING]))
if gas_resistance_config := config.get(CONF_GAS_RESISTANCE):
sens = await sensor.new_sensor(gas_resistance_config)
cg.add(var.set_gas_resistance_sensor(sens))
cg.add(var.set_iir_filter(IIR_FILTER_OPTIONS[config[CONF_IIR_FILTER]]))
+13 -8
View File
@@ -88,11 +88,16 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_ACCELERATION_X, var.set_accel_x_sensor)
await sensors(CONF_GYROSCOPE_X, var.set_gyro_x_sensor)
await sensors(CONF_ACCELERATION_Y, var.set_accel_y_sensor)
await sensors(CONF_GYROSCOPE_Y, var.set_gyro_y_sensor)
await sensors(CONF_ACCELERATION_Z, var.set_accel_z_sensor)
await sensors(CONF_GYROSCOPE_Z, var.set_gyro_z_sensor)
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
for d in ["x", "y", "z"]:
accel_key = f"acceleration_{d}"
if accel_key in config:
sens = await sensor.new_sensor(config[accel_key])
cg.add(getattr(var, f"set_accel_{d}_sensor")(sens))
accel_key = f"gyroscope_{d}"
if accel_key in config:
sens = await sensor.new_sensor(config[accel_key])
cg.add(getattr(var, f"set_gyro_{d}_sensor")(sens))
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
cg.add(var.set_temperature_sensor(sens))
+7 -3
View File
@@ -48,6 +48,10 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature)
await sensors(CONF_PRESSURE, var.set_pressure)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature(sens))
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure(sens))
+6 -5
View File
@@ -75,13 +75,14 @@ async def to_code_base(config: ConfigType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
sensors = sensor.sub_sensors(config)
if await sensors(CONF_TEMPERATURE, var.set_temperature_sensor):
temperature_config = config[CONF_TEMPERATURE]
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(var.set_temperature_oversampling(temperature_config[CONF_OVERSAMPLING]))
if await sensors(CONF_PRESSURE, var.set_pressure_sensor):
pressure_config = config[CONF_PRESSURE]
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING]))
cg.add(var.set_iir_filter(config[CONF_IIR_FILTER]))
+6 -5
View File
@@ -80,17 +80,18 @@ async def to_code_base(config: ConfigType) -> MockObj:
await cg.register_component(var, config)
cg.add(var.set_iir_filter_config(config[CONF_IIR_FILTER]))
sensors = sensor.sub_sensors(config)
if await sensors(CONF_TEMPERATURE, var.set_temperature_sensor):
temperature_config = config[CONF_TEMPERATURE]
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(
var.set_temperature_oversampling_config(
temperature_config[CONF_OVERSAMPLING]
)
)
if await sensors(CONF_PRESSURE, var.set_pressure_sensor):
pressure_config = config[CONF_PRESSURE]
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING]))
return var
+6 -5
View File
@@ -137,9 +137,9 @@ CONFIG_SCHEMA_BASE = cv.Schema(
async def to_code_base(config: ConfigType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
sensors = sensor.sub_sensors(config)
if await sensors(CONF_TEMPERATURE, var.set_temperature_sensor):
temperature_config = config[CONF_TEMPERATURE]
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(
var.set_temperature_oversampling_config(
temperature_config[CONF_OVERSAMPLING]
@@ -149,8 +149,9 @@ async def to_code_base(config: ConfigType) -> MockObj:
var.set_temperature_iir_filter_config(temperature_config[CONF_IIR_FILTER])
)
if await sensors(CONF_PRESSURE, var.set_pressure_sensor):
pressure_config = config[CONF_PRESSURE]
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING]))
cg.add(var.set_pressure_iir_filter_config(pressure_config[CONF_IIR_FILTER]))
@@ -422,21 +422,6 @@ bool BTHomeMiThermometer::handle_service_data_(const ble_device_base::ServiceDat
}
break;
}
case 0x45: { // temperature (0.1C), e.g. Shelly BLU H&T
if (this->temperature_ != nullptr) {
const int16_t raw = encode_uint16(value[1], value[0]);
this->temperature_->publish_state(raw * 0.1f);
reported = true;
}
break;
}
case 0x2E: { // humidity (uint8, 1%), e.g. Shelly BLU H&T
if (this->humidity_ != nullptr) {
this->humidity_->publish_state(value[0]);
reported = true;
}
break;
}
default:
break;
}
+11 -3
View File
@@ -368,11 +368,19 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() {
}
void Climate::save_state_(const ClimateTraits &traits) {
#if (defined(USE_ESP32) || defined(USE_ESP8266)) && !defined(CLANG_TIDY)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wclass-memaccess"
#define TEMP_IGNORE_MEMACCESS
#endif
ClimateDeviceRestoreState state{};
// initialize as zero (including padding) to prevent random data on stack triggering erase;
// the void * cast tells GCC the bytewise clear of a non-trivial type is intentional
// initialize as zero (including padding) to prevent random data on stack triggering erase
// NOLINTNEXTLINE(bugprone-raw-memory-call-on-non-trivial-type) -- intentional bytewise zero for RTC save
memset(static_cast<void *>(&state), 0, sizeof(ClimateDeviceRestoreState));
memset(&state, 0, sizeof(ClimateDeviceRestoreState));
#ifdef TEMP_IGNORE_MEMACCESS
#pragma GCC diagnostic pop
#undef TEMP_IGNORE_MEMACCESS
#endif
state.mode = this->mode;
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
+3 -2
View File
@@ -54,8 +54,9 @@ async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_CO2, var.set_co2_sensor)
if co2_config := config.get(CONF_CO2):
sens = await sensor.new_sensor(co2_config)
cg.add(var.set_co2_sensor(sens))
CALIBRATION_ACTION_SCHEMA = maybe_simple_id(
+3 -2
View File
@@ -203,5 +203,6 @@ async def to_code(config: ConfigType) -> None:
else:
cg.add(var.add_source(source))
sensors = sensor.sub_sensors(config)
await sensors(CONF_STD_DEV, var.set_std_dev_sensor)
if CONF_STD_DEV in config:
sens = await sensor.new_sensor(config[CONF_STD_DEV])
cg.add(var.set_std_dev_sensor(sens))
-4
View File
@@ -7,7 +7,6 @@ BYTE_ORDER_BIG = "big_endian"
CONF_ACCELEROMETER_ODR = "accelerometer_odr"
CONF_ACCELEROMETER_RANGE = "accelerometer_range"
CONF_ALLOWED_IPS = "allowed_ips"
CONF_B_CONSTANT = "b_constant"
CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
CONF_BYTE_ORDER = "byte_order"
@@ -16,7 +15,6 @@ CONF_CLIMATE_ID = "climate_id"
CONF_CO2_EQUIVALENT = "co2_equivalent"
CONF_COLOR_DEPTH = "color_depth"
CONF_COLUMNS = "columns"
CONF_CONNECTED = "connected"
CONF_CONVERSION_RATE = "conversion_rate"
CONF_CRC_ENABLE = "crc_enable"
CONF_DATA_BITS = "data_bits"
@@ -43,9 +41,7 @@ CONF_ON_SCAN_END = "on_scan_end"
CONF_ON_STATE_CHANGE = "on_state_change"
CONF_PARITY = "parity"
CONF_RECEIVER_FREQUENCY = "receiver_frequency"
CONF_RECONNECT_INTERVAL = "reconnect_interval"
CONF_REQUEST_HEADERS = "request_headers"
CONF_ROLE = "role"
CONF_ROWS = "rows"
CONF_SCAN_PARAMETERS = "scan_parameters"
CONF_SHA256 = "sha256"
+9 -4
View File
@@ -117,10 +117,15 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_hpf_enable(config[CONF_CURRENT_HPF], config[CONF_VOLTAGE_HPF]))
cg.add(var.set_pulse_energy_wh(config[CONF_PULSE_ENERGY]))
sensors = sensor.sub_sensors(config)
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
await sensors(CONF_CURRENT, var.set_current_sensor)
await sensors(CONF_POWER, var.set_power_sensor)
if voltage_config := config.get(CONF_VOLTAGE):
sens = await sensor.new_sensor(voltage_config)
cg.add(var.set_voltage_sensor(sens))
if current_config := config.get(CONF_CURRENT):
sens = await sensor.new_sensor(current_config)
cg.add(var.set_current_sensor(sens))
if power_config := config.get(CONF_POWER):
sens = await sensor.new_sensor(power_config)
cg.add(var.set_power_sensor(sens))
automation.register_apply_action(
+6 -3
View File
@@ -61,8 +61,11 @@ async def to_code(config: ConfigType) -> None:
pin = await gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin))
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
await sensors(CONF_HUMIDITY, var.set_humidity_sensor)
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
cg.add(var.set_temperature_sensor(sens))
if CONF_HUMIDITY in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
cg.add(var.set_humidity_sensor(sens))
cg.add(var.set_dht_model(config[CONF_MODEL]))
+7 -3
View File
@@ -46,6 +46,10 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
await sensors(CONF_HUMIDITY, var.set_humidity_sensor)
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
cg.add(var.set_temperature_sensor(sens))
if CONF_HUMIDITY in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
cg.add(var.set_humidity_sensor(sens))
+2 -4
View File
@@ -287,15 +287,13 @@ template<typename T> class DisplayWriter {
// Type alias for Display writer - uses optimized DisplayWriter instead of std::function
using display_writer_t = DisplayWriter<Display>;
// Callers usually pass `this`; checking a copy keeps GCC's -Wnonnull-compare quiet.
#define LOG_DISPLAY(prefix, type, obj) \
if (auto *log_display_obj = (obj); log_display_obj != nullptr) { \
if ((obj) != nullptr) { \
ESP_LOGCONFIG(TAG, \
prefix type "\n" \
"%s Rotations: %d °\n" \
"%s Dimensions: %dpx x %dpx", \
prefix, log_display_obj->rotation_, prefix, log_display_obj->get_width(), \
log_display_obj->get_height()); \
prefix, (obj)->rotation_, prefix, (obj)->get_width(), (obj)->get_height()); \
}
/// Turn the pixel OFF.
+1 -1
View File
@@ -86,7 +86,7 @@ void DlmsMeterComponent::setup() { this->flush_rx_buffer_(); }
void DlmsMeterComponent::dump_config() {
ESP_LOGCONFIG(TAG, "DLMS Meter:");
ESP_LOGCONFIG(TAG, " Receive Timeout: %" PRIu32 " ms", this->receive_timeout_ms_);
ESP_LOGCONFIG(TAG, " Receive Timeout: %u ms", this->receive_timeout_ms_);
ESP_LOGCONFIG(TAG, " Skip CRC Check: %s", YESNO(this->skip_crc_check_));
for (const auto &pattern : this->custom_patterns_) {
+7 -3
View File
@@ -54,6 +54,10 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
await sensors(CONF_PRESSURE, var.set_pressure_sensor)
if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature_sensor(sens))
if pressure := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure)
cg.add(var.set_pressure_sensor(sens))
+3 -2
View File
@@ -63,8 +63,9 @@ async def to_code(config: ConfigType) -> None:
if CONF_LAMBDA in config:
lambda_ = await cg.process_lambda(config[CONF_LAMBDA], [], return_type=cg.bool_)
cg.add(var.set_lambda(lambda_))
sensors = sensor.sub_sensors(config)
await sensors(CONF_LAST_TIME, var.set_last_duty_time_sensor)
if CONF_LAST_TIME in config:
sens = await sensor.new_sensor(config[CONF_LAST_TIME])
cg.add(var.set_last_duty_time_sensor(sens))
# AUTOMATIONS
+11 -4
View File
@@ -57,7 +57,14 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
await sensors(CONF_CO2, var.set_co2_sensor)
await sensors(CONF_PRESSURE, var.set_pressure_sensor)
if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature_sensor(sens))
if co2 := config.get(CONF_CO2):
sens = await sensor.new_sensor(co2)
cg.add(var.set_co2_sensor(sens))
if pressure := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure)
cg.add(var.set_pressure_sensor(sens))
+17 -7
View File
@@ -55,12 +55,22 @@ CONFIG_SCHEMA = EMC2101_COMPONENT_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_EMC2101_ID])
var = cg.new_Pvariable(config[CONF_ID], hub)
paren = await cg.get_variable(config[CONF_EMC2101_ID])
var = cg.new_Pvariable(config[CONF_ID], paren)
await cg.register_component(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_INTERNAL_TEMPERATURE, var.set_internal_temperature_sensor)
await sensors(CONF_EXTERNAL_TEMPERATURE, var.set_external_temperature_sensor)
await sensors(CONF_SPEED, var.set_speed_sensor)
await sensors(CONF_DUTY_CYCLE, var.set_duty_cycle_sensor)
if CONF_INTERNAL_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_INTERNAL_TEMPERATURE])
cg.add(var.set_internal_temperature_sensor(sens))
if CONF_EXTERNAL_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_EXTERNAL_TEMPERATURE])
cg.add(var.set_external_temperature_sensor(sens))
if CONF_SPEED in config:
sens = await sensor.new_sensor(config[CONF_SPEED])
cg.add(var.set_speed_sensor(sens))
if CONF_DUTY_CYCLE in config:
sens = await sensor.new_sensor(config[CONF_DUTY_CYCLE])
cg.add(var.set_duty_cycle_sensor(sens))
+9 -4
View File
@@ -63,10 +63,15 @@ async def to_code_base(config: ConfigType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_ECO2, var.set_co2)
await sensors(CONF_TVOC, var.set_tvoc)
await sensors(CONF_AQI, var.set_aqi)
if eco2_config := config.get(CONF_ECO2):
sens = await sensor.new_sensor(eco2_config)
cg.add(var.set_co2(sens))
if tvoc_config := config.get(CONF_TVOC):
sens = await sensor.new_sensor(tvoc_config)
cg.add(var.set_tvoc(sens))
if aqi_config := config.get(CONF_AQI):
sens = await sensor.new_sensor(aqi_config)
cg.add(var.set_aqi(sens))
if compensation_config := config.get(CONF_COMPENSATION):
sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE])
+7 -3
View File
@@ -50,6 +50,10 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
await sensors(CONF_HUMIDITY, var.set_humidity_sensor)
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
cg.add(var.set_temperature_sensor(sens))
if CONF_HUMIDITY in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
cg.add(var.set_humidity_sensor(sens))
+1 -16
View File
@@ -92,21 +92,6 @@ uint8_t ES8311::calculate_resolution_value(ES8311Resolution resolution) {
}
}
/// Encodes the MCLK pre-multiplier for REG02 bits 3-4. The coefficient table stores the multiplier as the factor
/// itself (1, 2, 4 or 8), while the register takes 0, 1, 2 or 3 (ES8311 datasheet, register 0x02 MULT_PRE).
static uint8_t encode_pre_mult(uint8_t pre_mult) {
switch (pre_mult) {
case 2:
return 1;
case 4:
return 2;
case 8:
return 3;
default:
return 0;
}
}
const ES8311Coefficient *ES8311::get_coefficient(uint32_t mclk, uint32_t rate) {
for (const auto &coefficient : ES8311_COEFFICIENTS) {
if (coefficient.mclk == mclk && coefficient.rate == rate)
@@ -142,7 +127,7 @@ bool ES8311::configure_clock_() {
ES8311_ERROR_CHECK(this->read_byte(ES8311_REG02_CLK_MANAGER, &reg02));
reg02 &= 0x07;
reg02 |= (coefficient->pre_div - 1) << 5;
reg02 |= encode_pre_mult(coefficient->pre_mult) << 3;
reg02 |= coefficient->pre_mult << 3;
ES8311_ERROR_CHECK(this->write_byte(ES8311_REG02_CLK_MANAGER, reg02));
// Register 0x03
+16 -10
View File
@@ -30,13 +30,19 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_ES8388_ID])
selects = select.sub_selects(config, parent=hub)
await selects(
CONF_DAC_OUTPUT, hub.set_dac_output_select, options=["LINE1", "LINE2", "BOTH"]
)
await selects(
CONF_ADC_INPUT_MIC,
hub.set_adc_input_mic_select,
options=["LINE1", "LINE2", "DIFFERENCE"],
)
parent = await cg.get_variable(config[CONF_ES8388_ID])
if dac_output_config := config.get(CONF_DAC_OUTPUT):
s = await select.new_select(
dac_output_config,
options=["LINE1", "LINE2", "BOTH"],
)
await cg.register_parented(s, parent)
cg.add(parent.set_dac_output_select(s))
if adc_input_mic_config := config.get(CONF_ADC_INPUT_MIC):
s = await select.new_select(
adc_input_mic_config,
options=["LINE1", "LINE2", "DIFFERENCE"],
)
await cg.register_parented(s, parent)
cg.add(parent.set_adc_input_mic_select(s))
+13 -52
View File
@@ -322,34 +322,6 @@ ARDUINO_EXCLUDED_IDF_COMPONENTS = (
"joltwallet__littlefs", # LittleFS - ESPHome doesn't use filesystem
)
# Entries arduino-esp32 only declares below the given IDF version; stubbing one past
# it clashes with ESPHome's own managed component of the same short name.
ARDUINO_EXCLUDED_IDF_COMPONENTS_MAX_IDF: dict[str, cv.Version] = {
"espressif__libsodium": cv.Version(6, 0, 0),
}
def arduino_bundles_libsodium() -> bool:
"""arduino-esp32 ships its own libsodium below IDF 6.0."""
return (
CORE.using_arduino
and idf_version()
< ARDUINO_EXCLUDED_IDF_COMPONENTS_MAX_IDF["espressif__libsodium"]
)
def arduino_excluded_idf_components() -> set[str]:
"""The arduino-bundled components to stub for this build's IDF version."""
version = idf_version()
return {
component
for component in ARDUINO_EXCLUDED_IDF_COMPONENTS
if (max_version := ARDUINO_EXCLUDED_IDF_COMPONENTS_MAX_IDF.get(component))
is None
or version < max_version
}
# Mapping of Arduino libraries to IDF managed components they require
# When an Arduino library is enabled via cg.add_library(), these components
# are automatically un-stubbed from ARDUINO_EXCLUDED_IDF_COMPONENTS.
@@ -670,29 +642,20 @@ def get_download_types(storage_json):
# No recorded firmware path means nothing was built; no downloads.
if storage_json.firmware_bin_path is None:
return []
from esphome.espidf.toolchain import tree_skips_bootloader
types = []
# A --skip-bootloader tree deliberately has no factory image; an
# unreadable tree (PlatformIO, capability probes) reads as full.
if not tree_skips_bootloader(Path(storage_json.firmware_bin_path).parent):
types.append(
{
"title": "Factory format (Previously Modern)",
"description": "For use with ESPHome Web and other tools.",
"file": "firmware.factory.bin",
"download": f"{storage_json.name}.factory.bin",
}
)
types.append(
return [
{
"title": "Factory format (Previously Modern)",
"description": "For use with ESPHome Web and other tools.",
"file": "firmware.factory.bin",
"download": f"{storage_json.name}.factory.bin",
},
{
"title": "OTA format (Previously Legacy)",
"description": "For OTA updating a device.",
"file": "firmware.ota.bin",
"download": f"{storage_json.name}.ota.bin",
}
)
return types
},
]
def only_on_variant(*, supported=None, unsupported=None, msg_prefix="This feature"):
@@ -3550,7 +3513,9 @@ def _write_idf_component_yml():
}
# Only stub components that are not required by any enabled Arduino library
components_to_stub = arduino_excluded_idf_components() - required_idf_components
components_to_stub = (
set(ARDUINO_EXCLUDED_IDF_COMPONENTS) - required_idf_components
)
stubs_dir = CORE.relative_build_path("component_stubs")
stubs_dir.mkdir(exist_ok=True)
@@ -3615,11 +3580,7 @@ def _write_idf_component_yml():
# Don't process arduino libraries
if name not in ARDUINO_DISABLED_LIBRARIES
]
# A library also declared as a managed component is not converted too, or
# IDF sees the same requirement twice; converted components reach it through
# ${ESPHOME_PROJECT_MANAGED_COMPONENTS}.
managed = set(CORE.data[KEY_ESP32].get(KEY_COMPONENTS, {}))
for component in generate_idf_components(libraries, managed=managed):
for component in generate_idf_components(libraries):
dependencies[component.get_sanitized_name()] = {
"override_path": str(component.path)
}
@@ -436,25 +436,6 @@ async def to_code(config: ConfigType) -> None:
cg.add_define("USE_ESP32_BLE_SOFTWARE_COEXISTENCE")
# First tagged release per series with espressif/esp-idf@82e71c1767 (see bluedroid_stubs.cpp).
# A series without an entry keeps the guard until a fixed release is tagged; the guard is
# harmless on fixed sources. The 5.4, 5.5 and 6.1 branches carry the fix but have no tag yet.
DIRECT_CONN_FIX_VERSIONS = {
(5, 2): cv.Version(5, 2, 8),
(5, 3): cv.Version(5, 3, 6),
(6, 0): cv.Version(6, 0, 3),
}
DIRECT_CONN_FIX_ALL_FROM = cv.Version(6, 2, 0)
def _needs_direct_conn_guard() -> bool:
ver = idf_version()
if ver >= DIRECT_CONN_FIX_ALL_FROM:
return False
fixed = DIRECT_CONN_FIX_VERSIONS.get((ver.major, ver.minor))
return fixed is None or ver < fixed
# This needs to be run as a job with very low priority so that all components have
# chance to call register_ble_tracker and register_client before the list is checked
# and added to the global defines list.
@@ -471,11 +452,6 @@ async def _add_ble_features() -> None:
if BLEFeatures.ESP_BT_DEVICE in required_features:
cg.add_define("USE_ESP32_BLE_DEVICE")
cg.add_define("USE_ESP32_BLE_UUID")
if cg.get_slot_count(CLIENT_COUNT_DEFINE) and _needs_direct_conn_guard():
# --undefined keeps the wrapper, libsrc.a is scanned before the IDF libraries
cg.add_define("USE_ESP32_BLE_TRACKER_DIRECT_CONN_GUARD")
cg.add_build_flag("-Wl,--wrap=l2cble_init_direct_conn")
cg.add_build_flag("-Wl,--undefined=__wrap_l2cble_init_direct_conn")
ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema(
@@ -1,48 +0,0 @@
/*
* Bluedroid queues outgoing BLE connections as raw link block pointers and does
* not drop them when the block is released, so btm_send_pending_direct_conn()
* can start a connect on a released block and l2c_link_timeout() later crashes
* on its null timer parameter. Mirrors espressif/esp-idf@82e71c1767; codegen
* only enables it for releases without that commit.
*/
#include "esphome/core/defines.h"
#ifdef USE_ESP32_BLE_TRACKER_DIRECT_CONN_GUARD
#include <esp_idf_version.h>
#include <cstdint>
#include "esphome/core/log.h"
namespace esphome::esp32_ble_tracker {
static const char *const TAG = "esp32_ble_tracker";
} // namespace esphome::esp32_ble_tracker
static_assert(ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 2, 0),
"ESP-IDF 6.2 and later have the fix, this guard should not be enabled (esphome/esphome#19373)");
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" {
bool __real_l2cble_init_direct_conn(void *p_lcb);
void l2cu_release_lcb(void *p_lcb);
bool __wrap_l2cble_init_direct_conn(void *p_lcb) {
// in_use is the first member of the private tL2C_LCB (checked ESP-IDF 5.0 to 6.1)
const auto *in_use = static_cast<const uint8_t *>(p_lcb);
if (p_lcb == nullptr || *in_use == 0) {
ESP_LOGW(esphome::esp32_ble_tracker::TAG, "Dropped queued connect on a released link block");
return false;
}
const bool started = __real_l2cble_init_direct_conn(p_lcb);
// Every failure path releases the block except unknown device, also fixed upstream
if (!started && *in_use != 0) {
l2cu_release_lcb(p_lcb);
}
return started;
}
} // extern "C"
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif // USE_ESP32_BLE_TRACKER_DIRECT_CONN_GUARD
+2 -15
View File
@@ -315,19 +315,10 @@ FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
FILTER_SOURCE_FILES = filter_source_files_from_defines(
{
"ota_esphome_noise.cpp": "USE_OTA_ENCRYPTION",
"ota_esphome_inflate_session.cpp": "USE_OTA_DEFLATE",
"ota_esphome_inflate.c": "USE_OTA_DEFLATE",
}
{"ota_esphome_noise.cpp": "USE_OTA_ENCRYPTION"}
)
def enable_deflate() -> None:
"""Compile the on-the-fly inflater for compressed uploads."""
cg.add_define("USE_OTA_DEFLATE")
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
@@ -349,10 +340,6 @@ async def to_code(config: ConfigType) -> None:
if config.get(CONF_ALLOW_PARTITION_ACCESS):
cg.add_define("USE_OTA_PARTITIONS")
# ESP8266 and RP2040 inflate gzip at reboot; the rest inflate on the fly
if not (CORE.is_esp8266 or CORE.is_rp2):
enable_deflate()
# One key per device: an api encryption block supplies it (static or
# runtime) and offers; the ota block only adds the requirement
api_conf = CORE.config.get(CONF_API) or {}
@@ -360,7 +347,7 @@ async def to_code(config: ConfigType) -> None:
# Build time key: the ota keeps its own pointer so safe mode, which
# has no api server, still has it
cg.add_define("USE_OTA_ENCRYPTION")
cg.add(var.set_noise_psk(new_psk_progmem(key)))
cg.add(var.set_noise_psk(new_psk_progmem(config[CONF_ID], key)))
elif CONF_ENCRYPTION in api_conf:
# Runtime key: found in the api server, or in preferences in safe mode
cg.add_define("USE_OTA_ENCRYPTION")
+81 -151
View File
@@ -22,10 +22,8 @@
#include "esphome/core/lwip_fast_select.h"
#endif
#include <algorithm>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <sys/time.h>
namespace esphome {
@@ -49,8 +47,6 @@ static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds
// practice for a lost chunk ack (1.5 + 3 + 6 + 12 + 24 + 48 s); the CLI waits
// longer (espota2.DATA_PHASE_TIMEOUT) so the device is free before it retries
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 105000;
static constexpr uint32_t OTA_PROGRESS_INTERVAL_MS = 1000;
static constexpr size_t OTA_SIZE_FIELD_BYTES = 4; // sizes on the wire are 4 bytes MSB first
// Single-instance pointer — multi-port configs are rejected in final_validate.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
@@ -198,23 +194,12 @@ static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_COMPRESSION = 0x01;
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02;
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04;
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_NOISE = 0x08;
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_DEFLATE = 0x10;
// Noise needs the extended protocol: the prologue binds the 2-byte feature ack
static constexpr uint8_t CLIENT_NOISE_FEATURES =
CLIENT_FEATURE_SUPPORTS_NOISE | CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL;
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_COMPRESSION = 0x01;
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS = 0x02;
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_NOISE = 0x04;
// Raw deflate, window <= OTA_INFLATE_WINDOW_SIZE. Binding once offered: the
// client must then send the image size frame and a deflate stream.
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_DEFLATE = 0x08;
#ifdef USE_OTA_ENCRYPTION
inline bool ESPHomeOTAComponent::noise_offered_() const {
return (this->handshake_buf_[1] & SERVER_FEATURE_SUPPORTS_NOISE) != 0 &&
(this->ota_features_ & CLIENT_NOISE_FEATURES) == CLIENT_NOISE_FEATURES;
}
#endif
inline bool ESPHomeOTAComponent::extended_proto_() const {
#ifdef USE_OTA_ENCRYPTION_REQUIRED
@@ -320,7 +305,7 @@ void ESPHomeOTAComponent::handle_handshake_() {
this->transition_ota_state_(OTAState::FEATURE_ACK);
const bool supports_compression =
(this->ota_features_ & CLIENT_FEATURE_SUPPORTS_COMPRESSION) != 0 && ota::OTABackend::supports_compression();
(this->ota_features_ & CLIENT_FEATURE_SUPPORTS_COMPRESSION) != 0 && this->backend_->supports_compression();
// Compose the feature-ack response. When the client negotiates the extended protocol we emit
// a 2-byte response (marker + server feature flags); otherwise we emit the single-byte
@@ -340,28 +325,6 @@ void ESPHomeOTAComponent::handle_handshake_() {
#elif defined(USE_OTA_ENCRYPTION)
// A yaml key always exists: validation rejects the all-zeros key
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_NOISE;
#endif
#ifdef USE_OTA_ENCRYPTION
// Reserve the noise session before the optional inflate buffer, so the
// required allocation is not starved by the compression window
if (this->noise_offered_()) {
this->noise_reserve_session_();
}
#endif
#ifdef USE_OTA_DEFLATE
// Offered only once the session memory is in hand; else uncompressed
if ((this->ota_features_ & CLIENT_FEATURE_SUPPORTS_DEFLATE) != 0) {
// Value initialized: a corrupt stream that back references the
// window before it is filled then copies zeros, never stale memory.
// Default placement, PSRAM first where present: the session lives for one
// upload and keeps 4.9 KB of internal heap free while it runs
this->inflate_ = RAMAllocator<InflateSession>().make_unique();
if (this->inflate_ != nullptr) {
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_DEFLATE;
} else {
ESP_LOGW(TAG, "No memory to inflate");
}
}
#endif
} else {
this->handshake_buf_[0] =
@@ -380,7 +343,8 @@ void ESPHomeOTAComponent::handle_handshake_() {
#ifdef USE_OTA_ENCRYPTION
// Latch the offer actually sent: a key activating between the two
// states must not start a session the client never expects
if (this->noise_offered_()) {
if ((this->handshake_buf_[1] & SERVER_FEATURE_SUPPORTS_NOISE) != 0 &&
(this->ota_features_ & CLIENT_NOISE_FEATURES) == CLIENT_NOISE_FEATURES) {
// handshake_buf_ still holds the feature ack composed above; a
// would-block re-entry lands here without rebuilding it
if (!this->noise_start_session_(this->handshake_buf_[1])) {
@@ -478,11 +442,16 @@ void ESPHomeOTAComponent::handle_data_() {
// Backend calls overwrite this with OK; reset to UNKNOWN before any
// goto error that follows a successful begin()/write()
ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
DataTransfer xfer;
size_t total = 0;
uint32_t last_progress = 0;
uint32_t last_data_ms = 0;
uint8_t buf[OTA_BUFFER_SIZE];
char *sbuf = reinterpret_cast<char *>(buf);
size_t image_size;
size_t ota_size;
ota::OTAType ota_type = ota::OTA_TYPE_UPDATE_APP;
#if USE_OTA_VERSION == 2
size_t size_acknowledged = 0;
#endif
// Set socket timeouts and blocking mode (see strategy table above)
struct timeval tv;
@@ -511,20 +480,16 @@ void ESPHomeOTAComponent::handle_data_() {
ESP_LOGV(TAG, "OTA type is 0x%02x", ota_type);
// Read size, 4 bytes MSB first
if (!this->read_size_(buf, xfer.ota_size, LOG_STR("size"))) {
if (!this->data_readall_(buf, 4)) {
// The first request byte is the type on the extended protocol; a close after it was a cut-off request
if (!this->extended_proto_() && this->client_left_before_start_())
return;
this->log_read_error_(LOG_STR("size"));
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
image_size = xfer.ota_size;
#ifdef USE_OTA_DEFLATE
if (this->inflate_ != nullptr && !this->read_size_(buf, image_size, LOG_STR("image size"))) {
this->log_read_error_(LOG_STR("image size"));
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
#endif
ota_size = (static_cast<size_t>(buf[0]) << 24) | (static_cast<size_t>(buf[1]) << 16) |
(static_cast<size_t>(buf[2]) << 8) | buf[3];
ESP_LOGV(TAG, "Size is %zu bytes", ota_size);
#ifndef USE_OTA_PARTITIONS
if (ota_type != ota::OTA_TYPE_UPDATE_APP) {
@@ -544,7 +509,7 @@ void ESPHomeOTAComponent::handle_data_() {
#endif
// begin() returns quickly; flash sectors are erased incrementally during write().
error_code = this->backend_->begin(image_size, ota_type);
error_code = this->backend_->begin(ota_size, ota_type);
if (error_code != ota::OTA_RESPONSE_OK)
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
@@ -564,25 +529,77 @@ void ESPHomeOTAComponent::handle_data_() {
// Acknowledge MD5 OK - 1 byte
this->data_write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
xfer.last_data_ms = millis();
#ifdef USE_OTA_DEFLATE
if (this->inflate_ != nullptr) {
error_code = this->inflate_data_(buf, image_size, xfer);
if (error_code != ota::OTA_RESPONSE_OK)
// Track when we last received data so a silently-vanished peer (no FIN/RST
// delivered, e.g. uploader killed mid-transfer or NAT/router dropped state)
// can't wedge the device indefinitely. Without this, the loop only exits
// on actual data, EOF, or a non-EWOULDBLOCK error from read(), and lwIP
// TCP keepalive isn't enabled here.
last_data_ms = millis();
while (total < ota_size) {
if (millis() - last_data_ms > OTA_SOCKET_TIMEOUT_DATA) {
ESP_LOGW(TAG, "No data received for %u ms", (unsigned) OTA_SOCKET_TIMEOUT_DATA);
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
} else
#endif
{
while (xfer.total < xfer.ota_size) {
ssize_t read = this->receive_data_(buf, xfer);
if (read < 0) {
}
size_t remaining = ota_size - total;
size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
ssize_t read;
#ifdef USE_OTA_ENCRYPTION
if (this->noise_ != nullptr) {
// One frame per call; noise_read_data_ waits internally (readall_), so
// there is no would-block retry here and failures are already logged.
read = this->noise_read_data_(buf, requested);
if (read <= 0) {
if (this->remote_closed_)
this->log_remote_closed_(LOG_STR("data"));
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
error_code = this->write_flash_(buf, read);
if (error_code != ota::OTA_RESPONSE_OK)
} else
#endif
{
read = this->client_->read(buf, requested);
if (read == -1) {
const int err = errno;
if (this->would_block_(err)) {
// read() already waited up to SO_RCVTIMEO for data, just feed WDT
App.feed_wdt();
continue;
}
ESP_LOGW(TAG, "Read err %d", err);
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
this->ack_written_(xfer);
} else if (read == 0) {
ESP_LOGW(TAG, "Remote closed");
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
}
last_data_ms = millis();
error_code = this->backend_->write(buf, read);
if (error_code != ota::OTA_RESPONSE_OK) {
ESP_LOGW(TAG, "Flash write err %d", error_code);
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
total += read;
#if USE_OTA_VERSION == 2
while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) {
this->data_write_byte_(ota::OTA_RESPONSE_CHUNK_OK);
size_acknowledged += OTA_BLOCK_SIZE;
}
#endif
uint32_t now = millis();
if (now - last_progress > 1000) {
last_progress = now;
float percentage = (total * 100.0f) / ota_size;
ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0);
#endif
// feed watchdog and give other tasks a chance to run
this->yield_and_feed_watchdog_();
}
}
@@ -798,90 +815,6 @@ bool ESPHomeOTAComponent::try_write_(size_t to_write, const LogString *desc) {
return this->handshake_buf_pos_ >= to_write;
}
bool ESPHomeOTAComponent::read_size_(uint8_t *buf, size_t &size, const LogString *desc) {
if (!this->data_readall_(buf, OTA_SIZE_FIELD_BYTES))
return false;
size = encode_uint32(buf[0], buf[1], buf[2], buf[3]);
ESP_LOGV(TAG, "%s is %zu bytes", LOG_STR_ARG(desc), size);
return true;
}
ota::OTAResponseTypes ESPHomeOTAComponent::write_flash_(uint8_t *data, size_t len) {
ota::OTAResponseTypes result = this->backend_->write(data, len);
if (result != ota::OTA_RESPONSE_OK) {
ESP_LOGW(TAG, "Flash write err %d", result);
}
return result;
}
ssize_t ESPHomeOTAComponent::receive_data_(uint8_t *buf, DataTransfer &xfer) {
const size_t remaining = xfer.ota_size - xfer.total;
const size_t requested = std::min(remaining, OTA_BUFFER_SIZE);
ssize_t read;
for (;;) {
// A silently-vanished peer (no FIN/RST delivered, e.g. uploader killed
// mid-transfer or NAT/router dropped state) must not wedge the device:
// read() only fails on EOF or a real error, and lwIP TCP keepalive isn't
// enabled here.
if (millis() - xfer.last_data_ms > OTA_SOCKET_TIMEOUT_DATA) {
ESP_LOGW(TAG, "No data received for %u ms", (unsigned) OTA_SOCKET_TIMEOUT_DATA);
return -1;
}
#ifdef USE_OTA_ENCRYPTION
if (this->noise_ != nullptr) {
// One frame per call; noise_read_data_ waits internally (readall_), so
// there is no would-block retry here and failures are already logged.
read = this->noise_read_data_(buf, requested);
if (read <= 0) {
if (this->remote_closed_)
this->log_remote_closed_(LOG_STR("data"));
return -1;
}
break;
}
#endif
read = this->client_->read(buf, requested);
if (read > 0)
break;
if (read == 0) {
this->log_remote_closed_(LOG_STR("data"));
return -1;
}
if (!this->would_block_(errno)) {
this->log_socket_error_(LOG_STR("data"));
return -1;
}
// read() already waited up to SO_RCVTIMEO for data, just feed WDT
App.feed_wdt();
}
const uint32_t now = millis();
xfer.last_data_ms = now;
xfer.total += read;
this->ack_received_(xfer);
if (now - xfer.last_progress > OTA_PROGRESS_INTERVAL_MS) {
xfer.last_progress = now;
float percentage = (xfer.total * 100.0f) / xfer.ota_size;
ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0);
#endif
// feed watchdog and give other tasks a chance to run
this->yield_and_feed_watchdog_();
}
return read;
}
void ESPHomeOTAComponent::send_chunk_acks_(DataTransfer &xfer) {
#if USE_OTA_VERSION == 2
while (xfer.acknowledged + OTA_BLOCK_SIZE <= xfer.total ||
(xfer.total == xfer.ota_size && xfer.acknowledged < xfer.ota_size)) {
this->data_write_byte_(ota::OTA_RESPONSE_CHUNK_OK);
xfer.acknowledged += OTA_BLOCK_SIZE;
}
#endif
}
void ESPHomeOTAComponent::cleanup_connection_() {
this->client_->close();
this->client_ = nullptr;
@@ -896,9 +829,6 @@ void ESPHomeOTAComponent::cleanup_connection_() {
#endif
#ifdef USE_OTA_ENCRYPTION
this->noise_ = nullptr;
#endif
#ifdef USE_OTA_DEFLATE
this->inflate_ = nullptr;
#endif
// Intentionally no disable_loop() — letting loop() run one more iteration catches
// any connection that queued on the listener mid-session (otherwise the wake flag,
@@ -7,9 +7,6 @@
#ifdef USE_OTA_ENCRYPTION
#include "esphome/components/noise/noise_handshake.h"
#endif
#ifdef USE_OTA_DEFLATE
#include "ota_esphome_inflate.h"
#endif
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
@@ -92,9 +89,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
// The api server's live context when it exists, otherwise our own (a build
// time key, or the saved key loaded in safe mode)
const noise::NoiseContext &noise_context_() const;
// True once the feature ack offers noise and the client asked for it
bool noise_offered_() const;
void noise_reserve_session_();
bool noise_start_session_(uint8_t server_feature_flags);
bool handle_noise_handshake_();
bool noise_try_read_frame_();
@@ -126,38 +120,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
return this->readall_(buf, len);
}
// Upload accounting shared by the data loop and the inflate read callback
struct DataTransfer {
size_t ota_size{0}; // bytes the client sends
size_t total{0}; // bytes received so far
#if USE_OTA_VERSION == 2
size_t acknowledged{0};
#endif
uint32_t last_data_ms{0};
uint32_t last_progress{0};
};
// Up to OTA_BUFFER_SIZE bytes into buf; returns bytes read, -1 on failure (logged)
ssize_t receive_data_(uint8_t *buf, DataTransfer &xfer);
// Raw lwIP cannot service the radio during a sector write, so the ack waits
// for the write there; a socket task lets the next block arrive meanwhile
#ifdef USE_SOCKET_IMPL_LWIP_TCP
static constexpr bool ACK_AFTER_WRITE = true;
#else
static constexpr bool ACK_AFTER_WRITE = false;
#endif
void send_chunk_acks_(DataTransfer &xfer);
inline void ack_received_(DataTransfer &xfer) {
if (!ACK_AFTER_WRITE)
this->send_chunk_acks_(xfer);
}
inline void ack_written_(DataTransfer &xfer) {
if (ACK_AFTER_WRITE)
this->send_chunk_acks_(xfer);
}
inline bool read_size_(uint8_t *buf, size_t &size, const LogString *desc);
// Writes to the backend and logs a failure
ota::OTAResponseTypes write_flash_(uint8_t *data, size_t len);
bool try_read_(size_t to_read, const LogString *desc);
bool try_write_(size_t to_write, const LogString *desc);
@@ -213,34 +175,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
static_assert(OTA_BUFFER_SIZE >= NOISE_CLIENT_MAX_PLAINTEXT + noise::MAC_SIZE,
"OTA_BUFFER_SIZE must fit a full encrypted data frame");
#endif
#ifdef USE_OTA_DEFLATE
// At least 1 << espota2.DEFLATE_WINDOW_BITS; also the inflate output buffer
static constexpr size_t OTA_INFLATE_WINDOW_SIZE = 4096;
// Heap-allocated only while a deflate upload is negotiated; the decoder
// state is the base so the read callback can recover the session
struct InflateSession : OtaInflateState {
// The session outlives the upload it serves, but these three are borrowed
// from inflate_data_'s caller and dangle once that call returns; only that
// call, and the flush and read callback it drives, may read them
ESPHomeOTAComponent *self;
DataTransfer *xfer;
uint8_t *in; // caller's buffer for the compressed input
size_t image_size;
size_t written; // inflated bytes in flash
size_t flushed; // bytes of the current window already in flash
ota::OTAResponseTypes error; // first failure inside the read callback
uint8_t window[OTA_INFLATE_WINDOW_SIZE];
};
#ifndef CLANG_TIDY // static analysis sets every define at once
static_assert(!ota::OTABackend::supports_compression(),
"USE_OTA_DEFLATE is for backends that cannot store a gzip image");
#endif
// Writes the decoded bytes not yet in flash without moving dest
ota::OTAResponseTypes inflate_flush_(InflateSession &session);
ota::OTAResponseTypes inflate_data_(uint8_t *in, size_t image_size, DataTransfer &xfer);
RAMUniquePtr<InflateSession> inflate_;
#endif
static constexpr uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45};
// Derived from the feature byte; storing it would pad the trailing bytes
bool extended_proto_() const;
@@ -1,498 +0,0 @@
/*
* uzlib - tiny deflate/inflate library (deflate, gzip, zlib)
*
* Copyright (c) 2003 by Joergen Ibsen / Jibz
* All Rights Reserved
* http://www.ibsensoftware.com/
*
* Copyright (c) 2014-2018 by Paul Sokolovsky
*
* This software is provided 'as-is', without any express
* or implied warranty. In no event will the authors be
* held liable for any damages arising from the use of
* this software.
*
* Permission is granted to anyone to use this software
* for any purpose, including commercial applications,
* and to alter it and redistribute it freely, subject to
* the following restrictions:
*
* 1. The origin of this software must not be
* misrepresented; you must not claim that you
* wrote the original software. If you use this
* software in a product, an acknowledgment in
* the product documentation would be appreciated
* but is not required.
*
* 2. Altered source versions must be plainly marked
* as such, and must not be misrepresented as
* being the original software.
*
* 3. This notice may not be removed or altered from
* any source distribution.
*/
/*
* Altered for ESPHome: this is the raw deflate decoder from uzlib's
* tinflate.c (v2.9.5) with the gzip/zlib header parsers, checksums,
* runtime table builder and in-memory (non ring window) output path
* removed, and the public names prefixed with ota_inflate.
*/
#include "ota_esphome_inflate.h"
#include <stddef.h>
#define TINF_OK OTA_INFLATE_OK
#define TINF_DONE OTA_INFLATE_DONE
#define TINF_DATA_ERROR OTA_INFLATE_DATA_ERROR
#define TINF_DICT_ERROR OTA_INFLATE_DICT_ERROR
#define TINF_DATA struct OtaInflateState
#define TINF_TREE struct OtaInflateTree
#define TINF_ARRAY_SIZE(arr) (sizeof(arr) / sizeof(*(arr)))
/* every output byte also goes into the ring window */
#define TINF_PUT(d, c) \
{ \
*d->dest++ = c; \
d->dict_ring[d->dict_idx++] = c; \
if (d->dict_idx == d->dict_size) \
d->dict_idx = 0; \
}
/* --------------------------------------------------- *
* -- constant tables (upstream builds them at runtime) -- *
* --------------------------------------------------- */
static const unsigned char LENGTH_BITS[30] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2,
2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5};
static const unsigned short LENGTH_BASE[30] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27,
31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258};
static const unsigned char DIST_BITS[30] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6,
6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13};
static const unsigned short DIST_BASE[30] = {1, 2, 3, 4, 5, 7, 9, 13, 17, 25,
33, 49, 65, 97, 129, 193, 257, 385, 513, 769,
1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577};
/* special ordering of code length codes */
static const unsigned char CLCIDX[] = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15};
/* ----------------------- *
* -- utility functions -- *
* ----------------------- */
/* given an array of code lengths, build a tree */
static void tinf_build_tree(TINF_TREE *t, const unsigned char *lengths, unsigned int num) {
unsigned short offs[16];
unsigned int i, sum;
/* clear code length count table */
for (i = 0; i < 16; ++i)
t->table[i] = 0;
/* scan symbol lengths, and sum code length counts */
for (i = 0; i < num; ++i)
t->table[lengths[i]]++;
/* In the lengths array, 0 means unused code. So, t->table[0] now contains
number of unused codes. But table's purpose is to contain # of codes of
particular length, and there're 0 codes of length 0. */
t->table[0] = 0;
/* compute offset table for distribution sort */
for (sum = 0, i = 0; i < 16; ++i) {
offs[i] = sum;
sum += t->table[i];
}
/* create code->symbol translation table (symbols sorted by code) */
for (i = 0; i < num; ++i) {
if (lengths[i])
t->trans[offs[lengths[i]]++] = i;
}
}
/* ---------------------- *
* -- decode functions -- *
* ---------------------- */
static unsigned char uzlib_get_byte(TINF_DATA *d) {
/* If end of source buffer is not reached, return next byte from source
buffer. */
if (d->source < d->source_limit) {
return *d->source++;
}
/* Otherwise if there's callback and we haven't seen EOF yet, try to
read next byte using it. (Note: the callback can also update ->source
and ->source_limit). */
if (!d->eof) {
int val = d->source_read_cb(d);
if (val >= 0) {
return (unsigned char) val;
}
}
/* Otherwise, we hit EOF (either from ->source_read_cb() or from exhaustion
of the buffer), and it will be "sticky", i.e. further calls to this
function will end up here too. */
d->eof = true;
return 0;
}
/* get one bit from source stream */
static int tinf_getbit(TINF_DATA *d) {
unsigned int bit;
/* check if tag is empty */
if (!d->bitcount--) {
/* load next tag */
d->tag = uzlib_get_byte(d);
d->bitcount = 7;
}
/* shift bit out of tag */
bit = d->tag & 0x01;
d->tag >>= 1;
return bit;
}
/* read a num bit value from a stream and add base */
static unsigned int tinf_read_bits(TINF_DATA *d, int num, int base) {
unsigned int val = 0;
/* read num bits */
if (num) {
unsigned int limit = 1 << (num);
unsigned int mask;
for (mask = 1; mask < limit; mask *= 2)
if (tinf_getbit(d))
val += mask;
}
return val + base;
}
/* given a data stream and a tree, decode a symbol */
static int tinf_decode_symbol(TINF_DATA *d, TINF_TREE *t) {
int sum = 0, cur = 0, len = 0;
/* get more bits while code value is above sum */
do {
cur = 2 * cur + tinf_getbit(d);
if (++len == TINF_ARRAY_SIZE(t->table)) {
return TINF_DATA_ERROR;
}
sum += t->table[len];
cur -= t->table[len];
} while (cur >= 0);
sum += cur;
if (sum < 0 || sum >= t->size) {
return TINF_DATA_ERROR;
}
return t->trans[sum];
}
/* given a data stream, decode dynamic trees from it */
static int tinf_decode_trees(TINF_DATA *d, TINF_TREE *lt, TINF_TREE *dt) {
/* code lengths for 288 literal/len symbols and 32 dist symbols */
unsigned char lengths[288 + 32];
unsigned int hlit, hdist, hclen, hlimit;
unsigned int i, num, length;
/* get 5 bits HLIT (257-286) */
hlit = tinf_read_bits(d, 5, 257);
/* get 5 bits HDIST (1-32) */
hdist = tinf_read_bits(d, 5, 1);
/* get 4 bits HCLEN (4-19) */
hclen = tinf_read_bits(d, 4, 4);
for (i = 0; i < 19; ++i)
lengths[i] = 0;
/* read code lengths for code length alphabet */
for (i = 0; i < hclen; ++i) {
/* get 3 bits code length (0-7) */
unsigned int clen = tinf_read_bits(d, 3, 0);
lengths[CLCIDX[i]] = clen;
}
/* build code length tree, temporarily use length tree */
tinf_build_tree(lt, lengths, 19);
/* decode code lengths for the dynamic trees */
hlimit = hlit + hdist;
for (num = 0; num < hlimit;) {
int sym = tinf_decode_symbol(d, lt);
unsigned char fill_value = 0;
int lbits, lbase = 3;
/* error decoding */
if (sym < 0)
return sym;
switch (sym) {
case 16:
/* copy previous code length 3-6 times (read 2 bits) */
if (num == 0)
return TINF_DATA_ERROR;
fill_value = lengths[num - 1];
lbits = 2;
break;
case 17:
/* repeat code length 0 for 3-10 times (read 3 bits) */
lbits = 3;
break;
case 18:
/* repeat code length 0 for 11-138 times (read 7 bits) */
lbits = 7;
lbase = 11;
break;
default:
/* values 0-15 represent the actual code lengths */
lengths[num++] = sym;
/* continue the for loop */
continue;
}
/* special code length 16-18 are handled here */
length = tinf_read_bits(d, lbits, lbase);
if (num + length > hlimit)
return TINF_DATA_ERROR;
for (; length; --length) {
lengths[num++] = fill_value;
}
}
/* Check that there's "end of block" symbol */
if (lengths[256] == 0) {
return TINF_DATA_ERROR;
}
/* build dynamic trees */
tinf_build_tree(lt, lengths, hlit);
tinf_build_tree(dt, lengths + hlit, hdist);
return TINF_OK;
}
/* build the fixed huffman trees (RFC 1951 3.2.6) through the generic tree
builder; altered from upstream, which unrolls them by hand */
static void tinf_build_fixed_trees(TINF_TREE *lt, TINF_TREE *dt) {
unsigned char lengths[288];
unsigned int i;
for (i = 0; i < 144; ++i)
lengths[i] = 8;
for (; i < 256; ++i)
lengths[i] = 9;
for (; i < 280; ++i)
lengths[i] = 7;
for (; i < 288; ++i)
lengths[i] = 8;
tinf_build_tree(lt, lengths, 288);
for (i = 0; i < 32; ++i)
lengths[i] = 5;
tinf_build_tree(dt, lengths, 32);
}
/* ----------------------------- *
* -- block inflate functions -- *
* ----------------------------- */
/* given a stream and two trees, inflate next chunk of output (a byte or more) */
static int tinf_inflate_block_data(TINF_DATA *d, TINF_TREE *lt, TINF_TREE *dt) {
if (d->curlen == 0) {
unsigned int offs;
int dist;
int sym = tinf_decode_symbol(d, lt);
if (d->eof) {
return TINF_DATA_ERROR;
}
if (sym < 0) {
return sym;
}
/* literal byte */
if (sym < 256) {
TINF_PUT(d, sym);
return TINF_OK;
}
/* end of block */
if (sym == 256) {
return TINF_DONE;
}
/* substring from sliding dictionary */
sym -= 257;
if (sym >= 29) {
return TINF_DATA_ERROR;
}
/* possibly get more bits from length code */
d->curlen = tinf_read_bits(d, LENGTH_BITS[sym], LENGTH_BASE[sym]);
dist = tinf_decode_symbol(d, dt);
if (dist < 0 || dist >= 30) {
return TINF_DATA_ERROR;
}
/* possibly get more bits from distance code */
offs = tinf_read_bits(d, DIST_BITS[dist], DIST_BASE[dist]);
/* calculate and validate actual LZ offset to use */
if (offs > d->dict_size) {
return TINF_DICT_ERROR;
}
/* Note: we don't try to catch offset which points to not yet filled
part of the dictionary here. Doing so would require keeping another
variable to track "filled in" size of the dictionary. Appearance of
such an offset cannot lead to accessing memory outside of the
dictionary buffer, and clients which don't want to leak unrelated
information, should explicitly initialize dictionary buffer passed
to uzlib. */
d->lz_off = d->dict_idx - offs;
if (d->lz_off < 0) {
d->lz_off += d->dict_size;
}
}
/* copy next byte from dict substring */
TINF_PUT(d, d->dict_ring[d->lz_off]);
if ((unsigned) ++d->lz_off == d->dict_size) {
d->lz_off = 0;
}
d->curlen--;
return TINF_OK;
}
/* inflate next byte from uncompressed block of data */
static int tinf_inflate_uncompressed_block(TINF_DATA *d) {
if (d->curlen == 0) {
unsigned int length, invlength;
/* get length */
length = uzlib_get_byte(d);
length += 256 * uzlib_get_byte(d);
/* get one's complement of length */
invlength = uzlib_get_byte(d);
invlength += 256 * uzlib_get_byte(d);
/* check length */
if (length != (~invlength & 0x0000ffff))
return TINF_DATA_ERROR;
/* increment length to properly return TINF_DONE below, without
producing data at the same time */
d->curlen = length + 1;
/* make sure we start next block on a byte boundary */
d->bitcount = 0;
}
if (--d->curlen == 0) {
return TINF_DONE;
}
unsigned char c = uzlib_get_byte(d);
TINF_PUT(d, c);
return TINF_OK;
}
/* ---------------------- *
* -- public functions -- *
* ---------------------- */
/* initialize decompression structure */
void ota_inflate_init(TINF_DATA *d, unsigned char *dict, unsigned int dict_len) {
d->source = NULL;
d->source_limit = NULL;
d->tag = 0;
d->eof = 0;
d->bitcount = 0;
d->lz_off = 0;
d->bfinal = 0;
d->btype = -1;
d->dict_size = dict_len;
d->dict_ring = dict;
d->dict_idx = 0;
d->curlen = 0;
d->ltree.trans = d->ltrans;
d->ltree.size = TINF_ARRAY_SIZE(d->ltrans);
d->dtree.trans = d->dtrans;
d->dtree.size = TINF_ARRAY_SIZE(d->dtrans);
}
/* inflate next output bytes from compressed stream */
int ota_inflate(TINF_DATA *d) {
do {
int res;
/* start a new block */
if (d->btype == -1) {
int old_btype;
next_blk:
old_btype = d->btype;
/* read final block flag */
d->bfinal = tinf_getbit(d);
/* read block type (2 bits) */
d->btype = tinf_read_bits(d, 2, 0);
if (d->btype == 1 && old_btype != 1) {
/* build fixed huffman trees */
tinf_build_fixed_trees(&d->ltree, &d->dtree);
} else if (d->btype == 2) {
/* decode trees from stream */
res = tinf_decode_trees(d, &d->ltree, &d->dtree);
if (res != TINF_OK) {
return res;
}
}
}
/* process current block */
switch (d->btype) {
case 0:
/* decompress uncompressed block */
res = tinf_inflate_uncompressed_block(d);
break;
case 1:
case 2:
/* decompress block with fixed/dynamic huffman trees */
/* trees were decoded previously, so it's the same routine for both */
res = tinf_inflate_block_data(d, &d->ltree, &d->dtree);
break;
default:
return TINF_DATA_ERROR;
}
if (res == TINF_DONE && !d->bfinal) {
/* the block has ended (without producing more data), but we
can't return without data, so start procesing next block */
goto next_blk;
}
if (res != TINF_OK) {
return res;
}
} while (d->dest < d->dest_limit);
return TINF_OK;
}
@@ -1,65 +0,0 @@
#pragma once
// Raw deflate decoder cut down from uzlib (https://github.com/pfalcon/uzlib,
// zlib licence, see the .c file); output goes through a ring window.
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
enum OtaInflateResult {
OTA_INFLATE_OK = 0, /* more data produced, call again */
OTA_INFLATE_DONE = 1, /* end of compressed stream reached */
OTA_INFLATE_DATA_ERROR = -3,
OTA_INFLATE_DICT_ERROR = -5,
};
struct OtaInflateTree {
uint16_t table[16]; /* table of code length counts */
uint16_t *trans; /* code -> symbol translation table, size entries */
uint16_t size;
};
struct OtaInflateState {
/* Next byte in the input buffer and one past its end */
const unsigned char *source;
const unsigned char *source_limit;
/* Called when source is exhausted; returns the next byte or -1 at EOF.
It may refill source/source_limit for buffered operation. */
int (*source_read_cb)(struct OtaInflateState *d);
/* Output cursor and one past the end of the output buffer */
unsigned char *dest;
unsigned char *dest_limit;
/* Ring window holding the last dict_size output bytes for back references */
unsigned char *dict_ring;
unsigned int tag;
unsigned int curlen;
int lz_off;
unsigned int dict_size;
unsigned int dict_idx;
/* One word: btype is -1 between blocks, bitcount never exceeds 7 */
int8_t btype;
uint8_t bfinal;
uint8_t bitcount;
bool eof;
struct OtaInflateTree ltree; /* dynamic length/symbol tree */
struct OtaInflateTree dtree; /* dynamic distance tree */
uint16_t ltrans[288];
uint16_t dtrans[32]; /* the distance alphabet has 30 symbols, so the tree is kept small */
};
/* dict must cover the encoder's window (its largest back reference) */
void ota_inflate_init(struct OtaInflateState *d, unsigned char *dict, unsigned int dict_len);
/* Fills dest up to dest_limit (OK) or to the end of the stream (DONE). dest may
alias dict only if dest_limit - dest == dict_len and dest is reset to dict
exactly when a call returns OK, so the ring index and dest stay in lockstep */
int ota_inflate(struct OtaInflateState *d);
#ifdef __cplusplus
}
#endif
@@ -1,94 +0,0 @@
#include "ota_esphome.h"
#ifdef USE_OTA
#ifdef USE_OTA_DEFLATE
#include "esphome/components/ota/ota_backend.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
namespace esphome {
static const char *const TAG = "esphome.ota";
// The window doubles as the output buffer; flushed bytes stay as back
// reference history for the next windowful.
ota::OTAResponseTypes ESPHomeOTAComponent::inflate_flush_(InflateSession &session) {
const size_t produced = session.dest - session.window;
const size_t pending = produced - session.flushed;
if (pending != 0) {
if (pending > session.image_size - session.written) {
ESP_LOGW(TAG, "Inflate overrun");
return ota::OTA_RESPONSE_ERROR_UNKNOWN;
}
ota::OTAResponseTypes result = this->write_flash_(session.window + session.flushed, pending);
if (result != ota::OTA_RESPONSE_OK)
return result;
session.flushed = produced;
session.written += pending;
// A compressible region yields many windows per socket read
App.feed_wdt();
}
// Even with nothing new written: a block boundary can fall inside a header
this->ack_written_(*session.xfer);
return ota::OTA_RESPONSE_OK;
}
ota::OTAResponseTypes ESPHomeOTAComponent::inflate_data_(uint8_t *in, size_t image_size, DataTransfer &xfer) {
InflateSession &session = *this->inflate_;
session.self = this;
session.xfer = &xfer;
session.in = in;
session.image_size = image_size;
session.written = 0;
session.error = ota::OTA_RESPONSE_OK;
ota_inflate_init(&session, session.window, OTA_INFLATE_WINDOW_SIZE);
// Where the ack must follow the write, flush and ack before waiting for
// input, or the client waits for an ack while the decoder waits for data
session.source_read_cb = [](OtaInflateState *d) -> int {
auto *s = static_cast<InflateSession *>(d);
if (ACK_AFTER_WRITE) {
s->error = s->self->inflate_flush_(*s);
if (s->error != ota::OTA_RESPONSE_OK)
return -1;
}
// More input than announced; reported by the size check below
if (s->xfer->total >= s->xfer->ota_size)
return -1;
ssize_t read = s->self->receive_data_(s->in, *s->xfer);
if (read <= 0) {
// Already logged by receive_data_
s->error = ota::OTA_RESPONSE_ERROR_UNKNOWN;
return -1;
}
d->source = s->in + 1;
d->source_limit = s->in + read;
return s->in[0];
};
int res;
do {
// The ring index wrapped to 0 exactly when the window filled
session.dest = session.window;
session.dest_limit = session.window + OTA_INFLATE_WINDOW_SIZE;
session.flushed = 0;
res = ota_inflate(&session);
// A stored block keeps emitting zeros after a failed read, hence eof
if (res < 0 || session.eof)
break;
session.error = this->inflate_flush_(session);
} while (res != OTA_INFLATE_DONE && session.error == ota::OTA_RESPONSE_OK);
// Transport and flash failures are logged where they happen
if (session.error != ota::OTA_RESPONSE_OK)
return session.error;
if (res != OTA_INFLATE_DONE || session.written != image_size || xfer.total != xfer.ota_size) {
ESP_LOGW(TAG, "Inflate err %d, %zu of %zu B from %zu of %zu", res, session.written, image_size, xfer.total,
xfer.ota_size);
return ota::OTA_RESPONSE_ERROR_UNKNOWN;
}
ESP_LOGD(TAG, "Inflated %zu bytes from %zu", session.written, xfer.total);
return ota::OTA_RESPONSE_OK;
}
} // namespace esphome
#endif // USE_OTA_DEFLATE
#endif // USE_OTA
@@ -32,12 +32,7 @@ ESPHomeOTAComponent::NoiseSession::~NoiseSession() {
}
}
void ESPHomeOTAComponent::noise_reserve_session_() {
// Default placement, PSRAM first where present: the session lives for one upload
this->noise_ = RAMAllocator<NoiseSession>().make_unique();
}
/** Start the responder handshake, on the session reserved at offer time.
/** Allocate the session and start the responder handshake.
*
* The prologue binds the whole plaintext preamble, so any tampering with the
* negotiation (a stripped feature flag, a changed version) breaks the first
@@ -46,7 +41,9 @@ void ESPHomeOTAComponent::noise_reserve_session_() {
*/
bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) {
// A provisioned key cleared between the offer and here is not guarded: the
// session runs on the zero key load_psk fills in and fails the client's MAC
// session runs on the zero key load_psk fills in and fails the client's MAC.
// Default placement, PSRAM first where present: the session only lives for one upload
this->noise_ = RAMAllocator<NoiseSession>().make_unique();
static constexpr size_t PROLOGUE_ACK_LEN = 2; // OTA_RESPONSE_OK + version
static constexpr size_t PROLOGUE_CLIENT_FEATURES_LEN = 1;
static constexpr size_t PROLOGUE_FEATURE_ACK_LEN = 2; // OTA_RESPONSE_FEATURE_FLAGS + server flags
+1 -11
View File
@@ -138,7 +138,6 @@ ETHERNET_TYPES = {
"GENERIC": EthernetType.ETHERNET_TYPE_GENERIC,
"YT8531": EthernetType.ETHERNET_TYPE_YT8531,
"CH390": EthernetType.ETHERNET_TYPE_CH390,
"KSZ8851SNL": EthernetType.ETHERNET_TYPE_KSZ8851SNL,
}
# PHY types that need compile-time defines for conditional compilation
@@ -161,7 +160,6 @@ _PHY_TYPE_TO_DEFINE = {
"GENERIC": "USE_ETHERNET_GENERIC",
"YT8531": "USE_ETHERNET_YT8531",
"CH390": "USE_ETHERNET_CH390",
"KSZ8851SNL": "USE_ETHERNET_KSZ8851SNL",
}
@@ -186,14 +184,13 @@ _IDF6_ETHERNET_COMPONENTS: dict[str, IDFRegistryComponent] = {
"ENC28J60": IDFRegistryComponent("espressif/enc28j60", "1.0.1"),
"LAN8670": IDFRegistryComponent("espressif/lan867x", "2.0.0"),
"CH390": IDFRegistryComponent("espressif/ch390", "0.3.0"),
"KSZ8851SNL": IDFRegistryComponent("espressif/ksz8851snl", "1.2.0"),
}
# These types are always external IDF components (never built-in to ESP-IDF)
_ALWAYS_EXTERNAL_IDF_COMPONENTS = {"LAN8670", "ENC28J60", "CH390"}
# ESP32-only SPI ethernet types (W5100 is RP2040-only, no ESP-IDF driver)
SPI_ETHERNET_TYPES = {"W5500", "DM9051", "ENC28J60", "CH390", "KSZ8851SNL"}
SPI_ETHERNET_TYPES = {"W5500", "DM9051", "ENC28J60", "CH390"}
# RP2-supported ethernet types (SPI and PIO QSPI). Applies to the whole
# RP2 family (RP2040 and RP2350); the chip-specific W5100 caveat in the
# comment above is about ESP-IDF driver coverage, not the RP2 platform.
@@ -537,12 +534,6 @@ SPI_SCHEMA_ENC28J60 = _spi_schema(default_clock="20MHz", max_clock=int(20e6))
# https://www.wch-ic.com/downloads/CH390DS1_PDF.html
SPI_SCHEMA_CH390 = _spi_schema(max_clock=int(72e6))
# The KSZ8851SNL rates fSCLK at 40 MHz maximum, so the shared 80 MHz ceiling is
# out of spec while the 26.67 MHz default is not. KSZ8851SNL/SNLI datasheet
# DS00002381C, table 7-1:
# https://ww1.microchip.com/downloads/aemDocuments/documents/UNG/ProductDocuments/DataSheets/KSZ8851SNL-Single-Port-Ethernet-Controller-with-SPI-DS00002381C.pdf
SPI_SCHEMA_KSZ8851SNL = _spi_schema(max_clock=int(40e6))
CONFIG_SCHEMA = cv.All(
cv.typed_schema(
{
@@ -558,7 +549,6 @@ CONFIG_SCHEMA = cv.All(
"OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])),
"DM9051": SPI_SCHEMA,
"CH390": SPI_SCHEMA_CH390,
"KSZ8851SNL": SPI_SCHEMA_KSZ8851SNL,
"ENC28J60": SPI_SCHEMA_ENC28J60,
"W6100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2])),
"W6300": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2])),
@@ -92,7 +92,6 @@ enum EthernetType : uint8_t {
ETHERNET_TYPE_GENERIC,
ETHERNET_TYPE_YT8531,
ETHERNET_TYPE_CH390,
ETHERNET_TYPE_KSZ8851SNL,
};
struct ManualIP {
@@ -220,9 +219,6 @@ class EthernetComponent final : public Component {
protected:
void start_connect_();
void finish_connect_();
#if LWIP_IPV6
esp_err_t ensure_ip6_linklocal_();
#endif
void dump_connect_params_();
#ifdef USE_ESP32
@@ -10,10 +10,6 @@
#include <lwip/dns.h>
#include <cinttypes>
#include "esp_event.h"
#if USE_NETWORK_IPV6
#include <esp_netif_net_stack.h>
#include <lwip/netif.h>
#endif
#ifdef USE_PSRAM
#include <esp_psram.h>
#endif
@@ -45,10 +41,6 @@
#include "esp_eth_mac_dm9051.h"
#include "esp_eth_phy_dm9051.h"
#endif
#ifdef USE_ETHERNET_KSZ8851SNL
#include "esp_eth_mac_ksz8851snl.h"
#include "esp_eth_phy_ksz8851snl.h"
#endif
#endif // ESP_IDF_VERSION >= 6.0.0
// LAN867x header exists on all IDF versions (external component since IDF 5.3)
@@ -272,8 +264,6 @@ void EthernetComponent::ethernet_lazy_init_() {
eth_enc28j60_config_t enc28j60_config = ETH_ENC28J60_DEFAULT_CONFIG(host, &devcfg);
#elif defined(USE_ETHERNET_CH390)
eth_ch390_config_t ch390_config = ETH_CH390_DEFAULT_CONFIG(host, &devcfg);
#elif defined(USE_ETHERNET_KSZ8851SNL)
eth_ksz8851snl_config_t ksz8851snl_config = ETH_KSZ8851SNL_DEFAULT_CONFIG(host, &devcfg);
#endif
#if defined(USE_ETHERNET_W5500)
@@ -300,11 +290,6 @@ void EthernetComponent::ethernet_lazy_init_() {
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
ch390_config.poll_period_ms = this->polling_interval_;
#endif
#elif defined(USE_ETHERNET_KSZ8851SNL)
ksz8851snl_config.int_gpio_num = this->interrupt_pin_;
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
ksz8851snl_config.poll_period_ms = this->polling_interval_;
#endif
#endif
phy_config.phy_addr = this->phy_addr_spi_;
@@ -435,12 +420,6 @@ void EthernetComponent::ethernet_lazy_init_() {
this->phy_ = esp_eth_phy_new_ch390(&phy_config);
break;
}
#elif defined(USE_ETHERNET_KSZ8851SNL)
case ETHERNET_TYPE_KSZ8851SNL: {
mac = esp_eth_mac_new_ksz8851snl(&ksz8851snl_config, &mac_config);
this->phy_ = esp_eth_phy_new_ksz8851snl(&phy_config);
break;
}
#endif
#endif
default: {
@@ -513,18 +492,6 @@ void EthernetComponent::ethernet_lazy_init_() {
}
}
#endif
#if USE_NETWORK_IPV6 && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
// Since IDF 5.5 the internal EMAC drops multicast groups that were never added (before,
// it passed all multicast), and lwIP never adds all-nodes, so router advertisements
// were lost and SLAAC never ran.
{
uint8_t all_nodes[6] = {0x33, 0x33, 0x00, 0x00, 0x00, 0x01};
if (esp_err_t filter_err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_ADD_MAC_FILTER, all_nodes);
filter_err != ESP_OK) {
ESP_LOGD(TAG, "IPv6 all-nodes multicast filter not added: %s", esp_err_to_name(filter_err));
}
}
#endif
// Register user defined event handers
err = esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &EthernetComponent::eth_event_handler, nullptr);
@@ -626,10 +593,6 @@ void EthernetComponent::dump_config() {
case ETHERNET_TYPE_CH390:
eth_type = "CH390";
break;
#elif defined(USE_ETHERNET_KSZ8851SNL)
case ETHERNET_TYPE_KSZ8851SNL:
eth_type = "KSZ8851SNL";
break;
#endif
#ifdef USE_ETHERNET_OPENETH
case ETHERNET_TYPE_OPENETH:
@@ -771,13 +734,6 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
global_eth_component->notify_ip_state_listeners_();
}
#endif
#if USE_NETWORK_IPV6
// Start SLAAC on link-up, not after the DHCPv4 lease. This also restores the
// link-local after a link flap, which clears the IPv6 addresses.
if (esp_err_t ll_err = esp_netif_create_ip6_linklocal(global_eth_component->eth_netif_); ll_err != ESP_OK) {
ESP_LOGW(TAG, "esp_netif_create_ip6_linklocal failed on link-up: %s", esp_err_to_name(ll_err));
}
#endif /* USE_NETWORK_IPV6 */
break;
case ETHERNET_EVENT_DISCONNECTED:
event_name = "ETH disconnected";
@@ -814,10 +770,7 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
void *event_data) {
ip_event_got_ip6_t *event = (ip_event_got_ip6_t *) event_data;
ESP_LOGV(TAG, "[Ethernet event] ETH Got IPv6: " IPV6STR, IPV62STR(event->ip6_info.ip));
// Count the addresses on the interface, not the events: recreating the link-local
// after a link flap fires another event for the same address.
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
global_eth_component->ipv6_count_ = esp_netif_get_all_ip6(global_eth_component->eth_netif_, if_ip6s);
global_eth_component->ipv6_count_ += 1;
#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ =
global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT);
@@ -832,29 +785,6 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
}
#endif /* USE_NETWORK_IPV6 */
#if USE_NETWORK_IPV6
// Create the link-local address unless the interface already has one, including one still in
// duplicate address detection: recreating it would restart DAD. esp_netif_get_ip6_linklocal()
// only reports a preferred address, so ask lwIP for the slot state instead.
esp_err_t EthernetComponent::ensure_ip6_linklocal_() {
if (auto *netif = static_cast<struct netif *>(esp_netif_get_netif_impl(this->eth_netif_)); netif != nullptr) {
u8_t state;
{
LwIPLock lock;
state = netif_ip6_addr_state(netif, 0);
}
if (ip6_addr_istentative(state) || ip6_addr_isvalid(state)) {
return ESP_OK;
}
}
esp_err_t err = esp_netif_create_ip6_linklocal(this->eth_netif_);
if (err == ESP_OK) {
ESP_LOGD(TAG, "IPv6 link-local address created");
}
return err;
}
#endif /* USE_NETWORK_IPV6 */
void EthernetComponent::finish_connect_() {
#if USE_NETWORK_IPV6
// Retry IPv6 link-local setup if it failed during initial connect
@@ -865,7 +795,10 @@ void EthernetComponent::finish_connect_() {
// - Cable unplugged/network interruption (#10705)
// We can now retry since we're in CONNECTED state and the interface is definitely up.
if (!this->ipv6_setup_done_) {
this->ensure_ip6_linklocal_();
esp_err_t err = esp_netif_create_ip6_linklocal(this->eth_netif_);
if (err == ESP_OK) {
ESP_LOGD(TAG, "IPv6 link-local address created (retry succeeded)");
}
// Always set the flag to prevent continuous retries
// If IPv6 setup fails here with the interface up and stable, it's
// likely a persistent issue (IPv6 disabled at router, hardware
@@ -879,9 +812,7 @@ void EthernetComponent::finish_connect_() {
void EthernetComponent::start_connect_() {
global_eth_component->got_ipv4_address_ = false;
#if USE_NETWORK_IPV6
// Recount rather than zero: addresses that survive a reconnect are not announced again.
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
global_eth_component->ipv6_count_ = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s);
global_eth_component->ipv6_count_ = 0;
this->ipv6_setup_done_ = false;
#endif /* USE_NETWORK_IPV6 */
this->connect_begin_ = millis();
@@ -960,7 +891,7 @@ void EthernetComponent::start_connect_() {
// - At bootup when link isn't ready (#10281)
// - After disconnection/cable unplugged (#10705)
// We'll retry in finish_connect_() if it fails here.
err = this->ensure_ip6_linklocal_();
err = esp_netif_create_ip6_linklocal(this->eth_netif_);
if (err != ESP_OK) {
if (err == ESP_ERR_ESP_NETIF_INVALID_PARAMS) {
// This is a programming error, not a transient failure
+1 -2
View File
@@ -726,8 +726,7 @@ async def to_code(config):
)
]
# constexpr Glyph table in flash; on ESP8266 static_const_array would place it in RAM.
glyphs = cg.progmem_array(config[CONF_RAW_GLYPH_ID], glyph_initializer)
glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer)
font_height = pt_to_px(base_font.size.height)
ascender = pt_to_px(base_font.size.ascender)
+1 -2
View File
@@ -309,8 +309,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
// Unknown char, skip
ESP_LOGW(TAG, "Codepoint 0x%08" PRIx32 " not found in font", code_point);
if (!this->glyphs_.empty()) {
// Full-width read: a narrowing byte load would fault on a PROGMEM table on ESP8266.
int glyph_width = this->glyphs_[0].advance;
uint8_t glyph_width = this->glyphs_[0].advance;
display->rectangle(x_at, y_start, glyph_width, this->height_, color);
x_at += glyph_width;
}
-14
View File
@@ -1,7 +1,5 @@
#pragma once
#include <type_traits>
#include "esphome/core/color.h"
#include "esphome/core/datatypes.h"
#include "esphome/core/defines.h"
@@ -38,18 +36,6 @@ class Glyph final {
int width;
int height;
};
// The glyph table lives in flash, read with plain loads, which ESP8266 only allows for whole words.
static_assert(std::is_same_v<decltype(Glyph::code_point), const uint32_t>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::data), const uint8_t *>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::advance), int>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::offset_x), int>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::offset_y), int>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::width), int>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::height), int>, "Glyph fields must stay word sized");
#ifdef USE_ESP8266
static_assert(alignof(Glyph) == sizeof(uint32_t), "Glyph fields must stay word sized");
static_assert(sizeof(Glyph) == 7 * sizeof(uint32_t), "Glyph is read from flash with word loads");
#endif
class Font final
#ifdef USE_DISPLAY
+15 -5
View File
@@ -63,8 +63,18 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_GDK101_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_RADIATION_DOSE_PER_1M, hub.set_rad_1m_sensor)
await sensors(CONF_RADIATION_DOSE_PER_10M, hub.set_rad_10m_sensor)
await sensors(CONF_STATUS, hub.set_status_sensor)
await sensors(CONF_MEASUREMENT_DURATION, hub.set_measurement_duration_sensor)
if radiation_dose_per_1m := config.get(CONF_RADIATION_DOSE_PER_1M):
sens = await sensor.new_sensor(radiation_dose_per_1m)
cg.add(hub.set_rad_1m_sensor(sens))
if radiation_dose_per_10m := config.get(CONF_RADIATION_DOSE_PER_10M):
sens = await sensor.new_sensor(radiation_dose_per_10m)
cg.add(hub.set_rad_10m_sensor(sens))
if status_config := config.get(CONF_STATUS):
sens = await sensor.new_sensor(status_config)
cg.add(hub.set_status_sensor(sens))
if measurement_duration_config := config.get(CONF_MEASUREMENT_DURATION):
sens = await sensor.new_sensor(measurement_duration_config)
cg.add(hub.set_measurement_duration_sensor(sens))
+3 -2
View File
@@ -55,8 +55,9 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_CO2, var.set_co2_sensor)
if co2 := config.get(CONF_CO2):
sens = await sensor.new_sensor(co2)
cg.add(var.set_co2_sensor(sens))
cg.add(var.set_warmup_seconds(config[CONF_WARMUP_TIME]))
+7 -3
View File
@@ -48,6 +48,10 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature)
await sensors(CONF_HUMIDITY, var.set_humidity)
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
cg.add(var.set_temperature(sens))
if CONF_HUMIDITY in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
cg.add(var.set_humidity(sens))
+7 -3
View File
@@ -48,6 +48,10 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
await sensors(CONF_HUMIDITY, var.set_humidity_sensor)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity_sensor(sens))
+7 -3
View File
@@ -49,6 +49,10 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature)
await sensors(CONF_HUMIDITY, var.set_humidity)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature(sens))
if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity(sens))
+7 -3
View File
@@ -66,9 +66,13 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temp_sensor)
await sensors(CONF_HUMIDITY, var.set_humidity_sensor)
if temp_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temp_config)
cg.add(var.set_temp_sensor(sens))
if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity_sensor(sens))
cg.add(var.set_power_mode(config[CONF_POWER_MODE]))
@@ -35,13 +35,16 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_HOERMANN_HCP_ID])
parent = await cg.get_variable(config[CONF_HOERMANN_HCP_ID])
if (conf := config.get(CONF_DOOR_STATE)) is not None:
var = await text_sensor.new_text_sensor(conf, hub)
var = await text_sensor.new_text_sensor(conf, parent)
await cg.register_component(var, conf)
# Only the identity sensors need the exchange with the motor compiled in.
if CONF_SERIAL_NUMBER in config or CONF_VERSION in config:
cg.add_define("USE_HOERMANN_HCP_IDENTITY")
text_sensors = text_sensor.sub_text_sensors(config)
await text_sensors(CONF_SERIAL_NUMBER, hub.set_serial_number_text_sensor)
await text_sensors(CONF_VERSION, hub.set_version_text_sensor)
if (conf := config.get(CONF_SERIAL_NUMBER)) is not None:
sens = await text_sensor.new_text_sensor(conf)
cg.add(parent.set_serial_number_text_sensor(sens))
if (conf := config.get(CONF_VERSION)) is not None:
sens = await text_sensor.new_text_sensor(conf)
cg.add(parent.set_version_text_sensor(sens))
@@ -7,23 +7,19 @@ from .. import (
HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA,
homeassistant_ns,
setup_home_assistant_entity,
validate_entity_domain,
)
CODEOWNERS = ["@landonr"]
DEPENDENCIES = ["api"]
SUPPORTED_DOMAINS = ["input_number", "number"]
HomeassistantNumber = homeassistant_ns.class_(
"HomeassistantNumber", number.Number, cg.Component
)
CONFIG_SCHEMA = cv.All(
CONFIG_SCHEMA = (
number.number_schema(HomeassistantNumber)
.extend(HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
validate_entity_domain("number", SUPPORTED_DOMAINS),
.extend(cv.COMPONENT_SCHEMA)
)
@@ -1,7 +1,5 @@
#include "homeassistant_number.h"
#include <cstring>
#include "esphome/components/api/api_pb2.h"
#include "esphome/components/api/api_server.h"
#include "esphome/core/log.h"
@@ -82,19 +80,12 @@ void HomeassistantNumber::control(float value) {
this->publish_state(value);
static constexpr auto NUMBER_SERVICE_NAME = StringRef::from_lit("number.set_value");
static constexpr auto INPUT_NUMBER_SERVICE_NAME = StringRef::from_lit("input_number.set_value");
static constexpr char INPUT_PREFIX[] = "input_";
static constexpr auto SERVICE_NAME = StringRef::from_lit("number.set_value");
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
static constexpr auto VALUE_KEY = StringRef::from_lit("value");
api::HomeassistantActionRequest resp;
// input_number entities have their own set_value action
if (strncmp(this->entity_id_, INPUT_PREFIX, sizeof(INPUT_PREFIX) - 1) == 0) {
resp.service = INPUT_NUMBER_SERVICE_NAME;
} else {
resp.service = NUMBER_SERVICE_NAME;
}
resp.service = SERVICE_NAME;
resp.data.init(2);
auto &entity_id = resp.data.emplace_back();
@@ -48,6 +48,10 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
await sensors(CONF_HUMIDITY, var.set_humidity_sensor)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity_sensor(sens))
+8 -3
View File
@@ -56,9 +56,14 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await spi.register_spi_device(var, config)
sensors = sensor.sub_sensors(config)
if await sensors(CONF_PRESSURE, var.set_pressure_sensor):
if CONF_PRESSURE in config:
conf = config[CONF_PRESSURE]
sens = await sensor.new_sensor(conf)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_honeywellabp_min_pressure(conf[CONF_MIN_PRESSURE]))
cg.add(var.set_honeywellabp_max_pressure(conf[CONF_MAX_PRESSURE]))
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
if CONF_TEMPERATURE in config:
conf = config[CONF_TEMPERATURE]
sens = await sensor.new_sensor(conf)
cg.add(var.set_temperature_sensor(sens))
@@ -63,11 +63,13 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
if await sensors(CONF_PRESSURE, var.set_pressure_sensor):
pressure_config = config[CONF_PRESSURE]
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_min_pressure(pressure_config[CONF_MIN_PRESSURE]))
cg.add(var.set_max_pressure(pressure_config[CONF_MAX_PRESSURE]))
cg.add(var.set_transfer_function(pressure_config[TRANSFER_FUNCTION]))
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
+7 -3
View File
@@ -50,6 +50,10 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
await sensors(CONF_HUMIDITY, var.set_humidity_sensor)
if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature_sensor(sens))
if humidity := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity)
cg.add(var.set_humidity_sensor(sens))
@@ -25,10 +25,11 @@ std::string HttpContainer::get_response_header(const std::string &header_name) {
auto lower = str_lower_case(header_name); // NOLINT
for (const auto &entry : this->response_headers_) {
if (entry.name == lower) {
ESP_LOGV(TAG, "Header with name %s found with value %s", lower.c_str(), entry.value.c_str());
ESP_LOGD(TAG, "Header with name %s found with value %s", lower.c_str(), entry.value.c_str());
return entry.value;
}
}
ESP_LOGW(TAG, "No header with name %s found", lower.c_str());
return "";
}
@@ -268,11 +268,6 @@ class HttpContainer : public Parented<HttpRequestComponent> {
return !this->is_chunked_ && this->bytes_read_ >= this->content_length;
}
/**
* @brief Get the value of a response header by name.
* @param header_name The name of the header to retrieve.
* @return The value of the header if found, otherwise an empty string.
*/
std::string get_response_header(const std::string &header_name);
protected:
+7 -3
View File
@@ -48,6 +48,10 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature)
await sensors(CONF_HUMIDITY, var.set_humidity)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature(sens))
if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity(sens))
+7 -3
View File
@@ -48,6 +48,10 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature)
await sensors(CONF_HUMIDITY, var.set_humidity)
if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature(sens))
if humidity := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity)
cg.add(var.set_humidity(sens))
@@ -60,11 +60,11 @@ class I2SAudioMicrophone final : public I2SAudioIn, public microphone::Microphon
gpio_num_t din_pin_{I2S_GPIO_UNUSED};
i2s_chan_handle_t rx_handle_;
bool pdm_{false};
#if SOC_I2S_SUPPORTS_PDM_RX
i2s_pdm_dsr_t pdm_dsr_{I2S_PDM_DSR_8S};
#endif
bool pdm_{false};
bool correct_dc_offset_;
bool locked_driver_{false};
int32_t dc_offset_prev_input_{0};
@@ -77,12 +77,6 @@ void I2SAudioSpeakerBase::loop() {
ESP_LOGV(TAG, "Started");
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING);
this->state_ = speaker::STATE_RUNNING;
#ifdef USE_AUDIO_DAC
// The task raises TASK_RUNNING only after enabling the channel, so the clocks are already running
if (this->audio_dac_ != nullptr) {
this->audio_dac_->on_audio_started();
}
#endif // USE_AUDIO_DAC
}
if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPING) {
ESP_LOGV(TAG, "Stopping");
+7 -3
View File
@@ -47,8 +47,12 @@ async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_CO2, var.set_co2)
await sensors(CONF_TVOC, var.set_tvoc)
if co2_config := config.get(CONF_CO2):
sens = await sensor.new_sensor(co2_config)
cg.add(var.set_co2(sens))
if tvoc_config := config.get(CONF_TVOC):
sens = await sensor.new_sensor(tvoc_config)
cg.add(var.set_tvoc(sens))
await i2c.register_i2c_device(var, config)
+11 -4
View File
@@ -59,7 +59,14 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_BUS_VOLTAGE, var.set_bus_voltage_sensor)
await sensors(CONF_CURRENT, var.set_current_sensor)
await sensors(CONF_POWER, var.set_power_sensor)
if CONF_BUS_VOLTAGE in config:
sens = await sensor.new_sensor(config[CONF_BUS_VOLTAGE])
cg.add(var.set_bus_voltage_sensor(sens))
if CONF_CURRENT in config:
sens = await sensor.new_sensor(config[CONF_CURRENT])
cg.add(var.set_current_sensor(sens))
if CONF_POWER in config:
sens = await sensor.new_sensor(config[CONF_POWER])
cg.add(var.set_power_sensor(sens))
-6
View File
@@ -21,9 +21,3 @@ async def to_code(config: ConfigType) -> None:
cg.add_library("bblanchon/ArduinoJson", "7.4.3")
cg.add_define("USE_JSON")
cg.add_global(json_ns.using)
def enable_arena() -> None:
"""Compile JsonArena and the allocator constructor of JsonBuilder; only the consumers that build
documents in a stack arena pay for them."""
cg.add_define("USE_JSON_ARENA")
+18 -33
View File
@@ -1,6 +1,4 @@
#include "json_util.h"
#include <cstring>
#include "esphome/core/log.h"
// ArduinoJson::Allocator is included via ArduinoJson.h in json_util.h
@@ -46,7 +44,11 @@ JsonDocument parse_json(const uint8_t *data, size_t len) {
ESP_LOGE(TAG, "No data to parse");
return JsonObject(); // return unbound object
}
JsonDocument json_document(heap_json_allocator());
#ifdef USE_PSRAM
JsonDocument json_document(&global_json_allocator);
#else
JsonDocument json_document;
#endif
if (json_document.overflowed()) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
return JsonObject(); // return unbound object
@@ -64,34 +66,7 @@ JsonDocument parse_json(const uint8_t *data, size_t len) {
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks,clang-analyzer-core.StackAddressEscape)
}
JsonBuilder::JsonBuilder() : doc_(heap_json_allocator()) {}
#ifdef USE_JSON_ARENA
JsonBuilder::JsonBuilder(ArduinoJson::Allocator *allocator) : doc_(allocator) {}
#endif
ArduinoJson::Allocator *heap_json_allocator() {
#ifdef USE_PSRAM
return &global_json_allocator;
#else
return ArduinoJson::detail::DefaultAllocator::instance();
#endif
}
size_t JsonBuilder::serialize_to(char *buf, size_t cap) {
if (doc_.overflowed()) {
ESP_LOGE(TAG, "JSON document overflow");
// Same contract as serializeJson; written by hand so no "{}" literal lives in RAM on ESP8266
size_t n = 0;
if (n < cap)
buf[n++] = '{';
if (n < cap)
buf[n++] = '}';
if (n < cap)
buf[n] = '\0';
return n;
}
return serializeJson(doc_, buf, cap);
}
JsonBuilder::JsonBuilder() = default;
SerializationBuffer<> JsonBuilder::serialize() {
// ===========================================================================================
@@ -134,7 +109,17 @@ SerializationBuffer<> JsonBuilder::serialize() {
constexpr size_t buf_size = SerializationBuffer<>::BUFFER_SIZE;
SerializationBuffer<> result(buf_size - 1); // Max content size (reserve 1 for null)
size_t size = this->serialize_to(result.data_writable_(), buf_size);
if (doc_.overflowed()) {
ESP_LOGE(TAG, "JSON document overflow");
auto *buf = result.data_writable_();
buf[0] = '{';
buf[1] = '}';
buf[2] = '\0';
result.set_size_(2);
return result;
}
size_t size = serializeJson(doc_, result.data_writable_(), buf_size);
if (size < buf_size) {
// Fits in stack buffer - update size to actual length
result.set_size_(size);
@@ -149,7 +134,7 @@ SerializationBuffer<> JsonBuilder::serialize() {
size_t heap_size = buf_size * 2;
while (heap_size <= max_heap_size) {
result.reallocate_heap_(heap_size - 1);
size = this->serialize_to(result.data_writable_(), heap_size);
size = serializeJson(doc_, result.data_writable_(), heap_size);
if (size < heap_size) {
result.set_size_(size);
return result;
+5 -83
View File
@@ -1,7 +1,5 @@
#pragma once
#include <algorithm>
#include <cstddef>
#include <cstring>
#include <string>
#include <vector>
@@ -167,88 +165,11 @@ inline JsonDocument parse_json(const std::string &data) {
return parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size());
}
/// The allocator a JsonBuilder uses by default (PSRAM first when available)
ArduinoJson::Allocator *heap_json_allocator();
#ifdef USE_JSON_ARENA
/// Size of one ArduinoJson slot pool, the first allocation every document makes (1 KB on 32 bit targets)
constexpr size_t JSON_POOL_BYTES = ARDUINOJSON_POOL_CAPACITY * sizeof(ArduinoJson::detail::VariantData);
/// One pool plus room for copied string nodes; a 40 option select with linked options fits
constexpr size_t JSON_ARENA_SIZE = JSON_POOL_BYTES + 1152;
static_assert(sizeof(void *) != 4 || JSON_ARENA_SIZE == 2176, "the arena was sized for a 1 KB pool");
/// Bump allocator over a fixed buffer for a document built and serialized in one scope. What the
/// buffer cannot hold goes to the heap allocator; nothing is freed until the arena goes away.
template<size_t N> class JsonArena final : public ArduinoJson::Allocator {
public:
// Takes what the buffer cannot hold
explicit JsonArena(ArduinoJson::Allocator *fallback = heap_json_allocator()) : fallback_(fallback) {}
// The document points into buf_
JsonArena(const JsonArena &) = delete;
JsonArena &operator=(const JsonArena &) = delete;
void *allocate(size_t size) override {
if (size > N) {
return this->fallback_->allocate(size); // also keeps the rounding below from wrapping
}
size = (size + ALIGN - 1) & ~(ALIGN - 1);
if (size > N - this->used_) {
return this->fallback_->allocate(size);
}
this->last_ = this->used_;
this->used_ += size;
return this->buf_ + this->last_;
}
void deallocate(void *ptr) override {
if (!this->owns_(ptr)) {
this->fallback_->deallocate(ptr);
}
}
void *reallocate(void *ptr, size_t new_size) override {
if (!this->owns_(ptr)) {
return this->fallback_->reallocate(ptr, new_size);
}
const size_t off = static_cast<uint8_t *>(ptr) - this->buf_;
const size_t size = new_size > N ? N + ALIGN : (new_size + ALIGN - 1) & ~(ALIGN - 1);
const bool newest = off == this->last_;
if (newest && size <= N - off) {
this->used_ = off + size; // the newest block grows or shrinks in place
return ptr;
}
// An older block's size is unknown; copying to the end of the buffer stays in bounds
const size_t old_size = newest ? this->used_ - off : N - off;
void *moved = this->fallback_->allocate(new_size);
if (moved == nullptr) {
return nullptr; // the caller keeps ptr, so its arena space stays reserved
}
std::memcpy(moved, ptr, std::min(new_size, old_size));
if (newest) {
this->used_ = off; // it moved to the heap, so its arena space is free again
}
return moved;
}
/// Bytes of the buffer handed out so far
size_t used() const { return this->used_; }
private:
static constexpr size_t ALIGN = alignof(std::max_align_t);
bool owns_(const void *ptr) const { return ptr >= this->buf_ && ptr < this->buf_ + N; }
ArduinoJson::Allocator *fallback_;
alignas(ALIGN) uint8_t buf_[N];
size_t used_{0};
size_t last_{0};
};
#endif // USE_JSON_ARENA
/// Builder class for creating JSON documents without lambdas
class JsonBuilder {
public:
// Out of line: inlining the JsonDocument constructor duplicates it at every call site
JsonBuilder();
#ifdef USE_JSON_ARENA
// The builder must not outlive the allocator
explicit JsonBuilder(ArduinoJson::Allocator *allocator);
#endif
JsonObject root() {
if (!root_created_) {
@@ -258,16 +179,17 @@ class JsonBuilder {
return root_;
}
/// Serialize into a caller owned buffer. Returns the length; cap or more means it did not fit
/// and buf holds a truncated, unterminated copy. An overflowed document serializes as "{}".
size_t serialize_to(char *buf, size_t cap);
/// Serialize the JSON document to a SerializationBuffer (stack-first allocation)
/// Uses 512-byte stack buffer by default, falls back to heap for larger JSON
SerializationBuffer<> serialize();
private:
#ifdef USE_PSRAM
SpiRamAllocator allocator_;
JsonDocument doc_{&allocator_};
#else
JsonDocument doc_;
#endif
JsonObject root_;
bool root_created_{false};
};
+6 -3
View File
@@ -48,6 +48,9 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
await sensors(CONF_INTERNAL_TEMPERATURE, var.set_internal_temperature_sensor)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
if internal_temperature_config := config.get(CONF_INTERNAL_TEMPERATURE):
sens = await sensor.new_sensor(internal_temperature_config)
cg.add(var.set_internal_temperature_sensor(sens))
+28 -8
View File
@@ -101,11 +101,31 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await modbus.register_modbus_client_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_PH, var.set_ph_sensor)
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
await sensors(CONF_DIS1, var.set_dis1_sensor)
await sensors(CONF_DIS2, var.set_dis2_sensor)
await sensors(CONF_REDOX, var.set_redox_sensor)
await sensors(CONF_EC, var.set_ec_sensor)
await sensors(CONF_OCI, var.set_oci_sensor)
if CONF_PH in config:
conf = config[CONF_PH]
sens = await sensor.new_sensor(conf)
cg.add(var.set_ph_sensor(sens))
if CONF_TEMPERATURE in config:
conf = config[CONF_TEMPERATURE]
sens = await sensor.new_sensor(conf)
cg.add(var.set_temperature_sensor(sens))
if CONF_DIS1 in config:
conf = config[CONF_DIS1]
sens = await sensor.new_sensor(conf)
cg.add(var.set_dis1_sensor(sens))
if CONF_DIS2 in config:
conf = config[CONF_DIS2]
sens = await sensor.new_sensor(conf)
cg.add(var.set_dis2_sensor(sens))
if CONF_REDOX in config:
conf = config[CONF_REDOX]
sens = await sensor.new_sensor(conf)
cg.add(var.set_redox_sensor(sens))
if CONF_EC in config:
conf = config[CONF_EC]
sens = await sensor.new_sensor(conf)
cg.add(var.set_ec_sensor(sens))
if CONF_OCI in config:
conf = config[CONF_OCI]
sens = await sensor.new_sensor(conf)
cg.add(var.set_oci_sensor(sens))
+11 -6
View File
@@ -79,10 +79,15 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_pack_size(config.get(CONF_SIZE)))
cg.add(var.set_pack_voltage(BATTERY_VOLTAGE_OPTIONS[config[CONF_VOLTAGE]]))
sensors = sensor.sub_sensors(config)
await sensors(CONF_BATTERY_VOLTAGE, var.set_voltage_sensor)
await sensors(CONF_BATTERY_LEVEL, var.set_battery_remaining_sensor)
if voltage_config := config.get(CONF_BATTERY_VOLTAGE):
sens = await sensor.new_sensor(voltage_config)
cg.add(var.set_voltage_sensor(sens))
if await sensors(CONF_TEMPERATURE, var.set_temperature_sensor):
b_constant = config[CONF_TEMPERATURE][CONF_B_CONSTANT]
cg.add(var.set_thermistor_b_constant(b_constant))
if level_config := config.get(CONF_BATTERY_LEVEL):
sens = await sensor.new_sensor(level_config)
cg.add(var.set_battery_remaining_sensor(sens))
if temp_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temp_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(var.set_thermistor_b_constant(temp_config[CONF_B_CONSTANT]))
+1 -4
View File
@@ -176,10 +176,6 @@ class LD2450Component : public Component, public uart::UARTDevice {
bool bluetooth_on_{false};
Target target_info_[MAX_TARGETS];
Zone zone_config_[MAX_ZONES];
#ifdef USE_TEXT_SENSOR
// Byte aligned, so it packs after zone_config_ instead of padding before data_callback_
std::array<Deduplicator<uint8_t>, MAX_TARGETS> direction_dedup_{};
#endif
#ifdef USE_NUMBER
ESPPreferenceObject pref_; // only used when numbers are in use
@@ -198,6 +194,7 @@ class LD2450Component : public Component, public uart::UARTDevice {
#endif
#ifdef USE_TEXT_SENSOR
std::array<text_sensor::TextSensor *, MAX_TARGETS> direction_text_sensors_{};
std::array<Deduplicator<uint8_t>, MAX_TARGETS> direction_dedup_{};
#endif
LazyCallbackManager<void()> data_callback_;
+3 -2
View File
@@ -40,8 +40,9 @@ CONFIG_SCHEMA = (
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD6002B_ID])
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(CONF_TARGET, hub.set_presence_binary_sensor)
if target_config := config.get(CONF_TARGET):
sens = await binary_sensor.new_binary_sensor(target_config)
cg.add(hub.set_presence_binary_sensor(sens))
for i in range(MAX_TARGETS):
if target_config := config.get(f"target_{i + 1}"):
+7 -3
View File
@@ -154,9 +154,13 @@ CONFIG_SCHEMA = (
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD6002B_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_TARGET_COUNT, hub.set_target_count_sensor)
await sensors(CONF_POINT_COUNT, hub.set_point_count_sensor)
if target_count_config := config.get(CONF_TARGET_COUNT):
sens = await sensor.new_sensor(target_count_config)
cg.add(hub.set_target_count_sensor(sens))
if point_count_config := config.get(CONF_POINT_COUNT):
sens = await sensor.new_sensor(point_count_config)
cg.add(hub.set_point_count_sensor(sens))
for i in range(MAX_TARGETS):
if target_config := config.get(f"target_{i + 1}"):
+6 -3
View File
@@ -24,6 +24,9 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD6002B_ID])
text_sensors = text_sensor.sub_text_sensors(config)
await text_sensors(CONF_WORK_MODE, hub.set_work_mode_text_sensor)
await text_sensors(CONF_OTA_VERSION, hub.set_ota_version_text_sensor)
if work_mode_config := config.get(CONF_WORK_MODE):
sens = await text_sensor.new_text_sensor(work_mode_config)
cg.add(hub.set_work_mode_text_sensor(sens))
if ota_config := config.get(CONF_OTA_VERSION):
sens = await text_sensor.new_text_sensor(ota_config)
cg.add(hub.set_ota_version_text_sensor(sens))
Loaded 100 of 535 files, more files were not shown because too many files have changed in this diff. Show more