Compare commits

..
286 changed files with 3617 additions and 12736 deletions

No files matched your search

@@ -1,39 +0,0 @@
name: Cache Arduino ESP8266
description: >
Resolve the pinned Arduino core and xtensa toolchain versions and cache the
native ESP8266 install (~110 MB framework + toolchain; no ccache store, the
seed job saves before any compile runs). Exports
ESPHOME_ARDUINO8266_PREFIX to the job so every later step installs into
the cached path; the Python venv must already be restored. Mirrors
cache-esp-idf: only dev-branch pushes write the shared cache, everything
else restores.
runs:
using: composite
steps:
- name: Resolve the native toolchain cache key
# Versions are pinned in code, not a hashable file; resolve them so a
# bump changes the cache key. Assignment form so errexit catches a
# resolver failure.
id: version
shell: bash
run: |
# One owner for the install prefix: exported here and referenced by
# the cache steps below via env, so the caller's install and the
# cached path cannot diverge.
echo "ESPHOME_ARDUINO8266_PREFIX=$HOME/.esphome-arduino8266" >> "$GITHUB_ENV"
. venv/bin/activate
key=$(python -c 'from esphome.components.esp8266 import RECOMMENDED_ARDUINO_FRAMEWORK_VERSION as f; from esphome.arduino8266.framework import TOOLCHAIN_VERSION as t; print(f"{f}-{t}")')
[ -n "$key" ] || exit 1
echo "key=$key" >> "$GITHUB_OUTPUT"
- name: Cache the native toolchain (write on dev)
if: github.ref == 'refs/heads/dev'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.ESPHOME_ARDUINO8266_PREFIX }}
key: ${{ runner.os }}-esp8266-native-${{ steps.version.outputs.key }}
- name: Restore the native toolchain (off dev)
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.ESPHOME_ARDUINO8266_PREFIX }}
key: ${{ runner.os }}-esp8266-native-${{ steps.version.outputs.key }}
-13
View File
@@ -195,7 +195,6 @@ jobs:
# the default.
id:
- esp8266-arduino
- esp8266-arduino-native
- esp32-arduino-platformio
- esp32-arduino-esp-idf
- esp32-idf-platformio
@@ -206,17 +205,6 @@ jobs:
- ln882x-arduino
- nrf52
- host
# Strict by default so a new matrix id cannot silently join in the
# degrade-quietly mode the knob exists to catch; the knob is inert
# where no pch code runs (esp32-*-platformio, nrf52).
# Opt-outs: libretiny GCC rejects its own pch until a toolchain bump.
include:
- id: bk72xx-arduino
pch_strict: "0"
- id: rtl87xx-arduino
pch_strict: "0"
- id: ln882x-arduino
pch_strict: "0"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download image artifact
@@ -228,7 +216,6 @@ jobs:
- name: Compile ${{ matrix.id }}
run: |
docker run --rm \
-e ESPHOME_PCH_STRICT="${{ matrix.pch_strict || '1' }}" \
-v "${{ github.workspace }}/docker/test_configs:/config" \
"ghcr.io/esphome/esphome-amd64:${{ needs.check-docker.outputs.tag }}" \
compile "${{ matrix.id }}.yaml"
+33 -96
View File
@@ -102,8 +102,6 @@ jobs:
device-builder: ${{ steps.determine.outputs.device-builder }}
esp32-platformio: ${{ steps.determine.outputs.esp32-platformio }}
esp32-platformio-components: ${{ steps.determine.outputs.esp32-platformio-components }}
esp8266-native: ${{ steps.determine.outputs.esp8266-native }}
esp8266-native-components: ${{ steps.determine.outputs.esp8266-native-components }}
changed-components: ${{ steps.determine.outputs.changed-components }}
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
@@ -167,8 +165,6 @@ jobs:
echo "device-builder=$(echo "$output" | jq -r '.device_builder')" >> $GITHUB_OUTPUT
echo "esp32-platformio=$(echo "$output" | jq -r '.esp32_platformio')" >> $GITHUB_OUTPUT
echo "esp32-platformio-components=$(echo "$output" | jq -r '.esp32_platformio_components')" >> $GITHUB_OUTPUT
echo "esp8266-native=$(echo "$output" | jq -r '.esp8266_native')" >> $GITHUB_OUTPUT
echo "esp8266-native-components=$(echo "$output" | jq -r '.esp8266_native_components')" >> $GITHUB_OUTPUT
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
@@ -187,33 +183,6 @@ jobs:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
seed-esp8266-native-cache:
name: Seed the esp8266 native toolchain cache
runs-on: ubuntu-24.04
needs:
- common
# PR-branch cache saves are invisible to other PRs, so dev pushes seed
# the shared entry the component matrix, the memory impact jobs and
# test-esp8266-native restore. Only dev: the composite action saves
# nowhere else, so a beta/release push would download the toolchain and
# discard it.
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
timeout-minutes: 15
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache the native toolchain
uses: ./.github/actions/cache-arduino8266
- name: Install the native toolchain
run: |
. venv/bin/activate
python -c "from esphome.arduino8266.framework import check_and_install; from esphome.components.esp8266 import RECOMMENDED_ARDUINO_FRAMEWORK_VERSION; check_and_install(RECOMMENDED_ARDUINO_FRAMEWORK_VERSION)"
ci-custom:
name: Run script/ci-custom
runs-on: ubuntu-24.04
@@ -400,9 +369,14 @@ jobs:
matrix:
bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }}
env:
# Registry libraries (noise-c, libsodium, ArduinoJson, lvgl) the host builds
# download, shared per xdist worker by tests/integration/conftest.py
INTEGRATION_LIBRARY_CACHE_PATH: ~/.esphome-integration-tests/pio_components
# What the cache steps persist; libdeps is excluded (keyed per xdist
# worker and env, it never crosses runs).
INTEGRATION_PIO_CACHE_PATH: |
~/.esphome-integration-tests/platformio/platforms
~/.esphome-integration-tests/platformio/packages
~/.esphome-integration-tests/platformio/appstate.json
~/.esphome-integration-tests/platformio/.cache
~/.esphome-integration-tests/platformio/.esphome.pio.stamp.json
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -422,13 +396,14 @@ jobs:
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
- name: Restore integration library cache
# A miss or a changed pin self-heals with a download, so no restore-keys
id: library-cache
- name: Restore integration PlatformIO cache
# Native platform + toolchain installed by shared_platformio_cache in
# tests/integration/conftest.py; a miss self-heals, so no restore-keys.
id: pio-cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.INTEGRATION_LIBRARY_CACHE_PATH }}
key: integration-libraries-v1-${{ runner.os }}-${{ hashFiles('esphome/components/json/__init__.py', 'esphome/components/noise/__init__.py', 'esphome/components/lvgl/__init__.py', 'esphome/components/improv_base/__init__.py') }}
path: ${{ env.INTEGRATION_PIO_CACHE_PATH }}
key: integration-pio-v1-${{ runner.os }}-py${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt', 'tests/integration/fixtures/cache_init.yaml', 'esphome/components/host/__init__.py') }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
@@ -496,16 +471,16 @@ jobs:
# A full cron period of margin for the weekly refresh
retention-days: 14
- name: Print ccache statistics
# esphome stores the host build's ccache under the machine-global
# cache dir (see get_build_env() in esphome/host/toolchain.py).
run: CCACHE_DIR="$HOME/.cache/esphome/host/ccache" ccache -s
- name: Save integration library cache
# esphome stores the PlatformIO ccache under the machine-global cache
# dir (see _ccache_env() in esphome/platformio/toolchain.py).
run: CCACHE_DIR="$HOME/.cache/esphome/platformio-ccache" ccache -s
- name: Save integration PlatformIO cache
# Bucket 0 only; the others would race the same immutable key.
if: success() && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && strategy.job-index == 0 && steps.library-cache.outputs.cache-hit != 'true'
if: success() && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && strategy.job-index == 0 && steps.pio-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.INTEGRATION_LIBRARY_CACHE_PATH }}
key: ${{ steps.library-cache.outputs.cache-primary-key }}
path: ${{ env.INTEGRATION_PIO_CACHE_PATH }}
key: ${{ steps.pio-cache.outputs.cache-primary-key }}
import-time:
name: Check import esphome.__main__ time
@@ -1122,11 +1097,6 @@ jobs:
uses: ./.github/actions/cache-sdk-nrf
with:
restore-only: true
- name: Cache the native ESP8266 toolchain
# Only batches whose test platforms include esp8266; never saves
# here, it reuses the install the dev seed job cached.
if: matrix.batch.needs_arduino8266
uses: ./.github/actions/cache-arduino8266
- name: Validate and compile components with intelligent grouping
run: |
. venv/bin/activate
@@ -1267,7 +1237,7 @@ jobs:
# compile validates config first, so a separate config pass is
# redundant for this smoke test. ESP-IDF framework via PlatformIO:
python3 script/test_build_components.py -e compile -t esp32-idf -c "$TEST_COMPONENTS" -f --toolchain platformio --fail-on-no-tests
python3 script/test_build_components.py -e compile -t esp32-idf -c "$TEST_COMPONENTS" -f --toolchain platformio
echo ""
echo "ESP-IDF-via-PlatformIO build passed! Starting Arduino smoke test..."
@@ -1276,42 +1246,6 @@ jobs:
# Arduino framework via PlatformIO (only components with an esp32-ard test are built):
python3 script/test_build_components.py -e compile -t esp32-ard -c "$TEST_COMPONENTS" -f --toolchain platformio
test-esp8266-native:
name: Test esp8266 components with the native toolchain
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.esp8266-native == 'true'
env:
# Computed by script/determine-jobs.py (ESP8266_NATIVE_TEST_COMPONENTS)
TEST_COMPONENTS: ${{ needs.determine-jobs.outputs.esp8266-native-components }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache the native toolchain
uses: ./.github/actions/cache-arduino8266
- name: Run native toolchain compile test
run: |
. venv/bin/activate
echo "Testing components: $TEST_COMPONENTS"
echo ""
# ESP8266 Arduino built directly (no PlatformIO); compile validates
# config first, so a separate config pass is redundant. Strict pch:
# exercises the native ninja pch (and its probe edge) against real
# component configs; the docker matrix smoke-tests both toolchains.
ESPHOME_PCH_STRICT=1 python3 script/test_build_components.py -e compile -t esp8266-ard -c "$TEST_COMPONENTS" -f --toolchain arduino --fail-on-no-tests
device-builder:
name: Test downstream esphome/device-builder
runs-on: ubuntu-24.04
@@ -1481,9 +1415,12 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache the native ESP8266 toolchain
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && fromJSON(needs.determine-jobs.outputs.memory_impact).needs_arduino8266
uses: ./.github/actions/cache-arduino8266
- name: Cache platformio
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
- name: Build, compile, and analyze memory
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
@@ -1568,9 +1505,11 @@ jobs:
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache the native ESP8266 toolchain
if: fromJSON(needs.determine-jobs.outputs.memory_impact).needs_arduino8266
uses: ./.github/actions/cache-arduino8266
- name: Cache platformio
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
- name: Build, compile, and analyze memory
id: extract
run: |
@@ -1669,7 +1608,6 @@ jobs:
needs:
- common
- seed-apt-cache
- seed-esp8266-native-cache
- determine-jobs
- ci-custom
- pylint
@@ -1685,7 +1623,6 @@ jobs:
- clang-tidy-esp32-variants
- test-build-components-split
- test-esp32-platformio
- test-esp8266-native
- device-builder
- memory-impact-target-branch
- memory-impact-pr-branch
-19
View File
@@ -322,25 +322,6 @@ file does, and it is the authority when they disagree. The most useful starting
var = await switch.new_switch(config)
```
- **Optional child entities of a hub:** bind the config once with `sensor.sub_sensors(config)` (or
`sub_binary_sensors`, `sub_text_sensors`, `sub_buttons`, `sub_switches`, `sub_numbers`,
`sub_selects` in their domains), adding `parent=hub` for entities that derive from `Parented<T>`,
then make one call per key, even when there is only one. A call creates the entity only when its key
is configured, passes it to the setter and returns it (or `None`); extra arguments such as
`min_value` or `options` go on the call. Always name the setter explicitly on the object that owns
it, never with `getattr` and an f-string, and keep that variable short (`var` for the component
itself, `hub` for one fetched with `cg.get_variable`) so the calls fit on one line. Loops whose
setter also takes an index, such as `set_gate_threshold(x, n)`, stay as they are.
```python
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
await sensors(CONF_HUMIDITY, var.set_humidity_sensor)
buttons = button.sub_buttons(config, parent=var)
await buttons(CONF_RESTART, var.set_restart_button)
```
* **Automations (Triggers, Actions, Conditions):**
Automations have three building blocks: **Triggers** (fire when something happens), **Actions** (do something), and **Conditions** (check if something is true).
-1
View File
@@ -125,7 +125,6 @@ esphome/components/combination/* @Cat-Ion @kahrendt
esphome/components/const/* @esphome/core
esphome/components/coolix/* @glmnet
esphome/components/copy/* @OttoWinter
esphome/components/counter/* @clydebarrow
esphome/components/cover/* @esphome/core
esphome/components/cs5460a/* @balrog-kun
esphome/components/cse7761/* @berfenger
+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.17.0
RUN uv pip install --no-cache-dir esphome-device-builder==1.16.0
RUN \
platformio settings set enable_telemetry No \
@@ -1,8 +0,0 @@
esphome:
name: docker-test-esp8266-native
esp8266:
board: d1_mini
toolchain: arduino
logger:
+16 -7
View File
@@ -1714,12 +1714,26 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
if exit_code != 0:
return exit_code
if CORE.is_host:
_LOGGER.info("Successfully compiled program to path '%s'", CORE.firmware_bin)
_LOGGER.info(
"Successfully compiled program to path '%s'", _host_program_path(config)
)
else:
_LOGGER.info("Successfully compiled program.")
return 0
def _host_program_path(config: ConfigType) -> str:
"""Return the compiled host ELF path."""
if CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain
return str(toolchain.get_elf_path())
from esphome.platformio.toolchain import get_idedata
# Memoized by compile_program's own call; this is a dict lookup
return str(get_idedata(config).firmware_elf_path)
def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
# Get devices, resolving special identifiers like OTA
devices = choose_upload_log_host(
@@ -1764,7 +1778,7 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
return exit_code
_LOGGER.info("Successfully compiled program.")
if CORE.is_host:
program_path = str(CORE.firmware_bin)
program_path = _host_program_path(config)
_LOGGER.info("Running program from path '%s'", program_path)
return run_external_process(program_path)
@@ -2010,11 +2024,6 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
CORE.target_platform,
)
return 1
if (
check_supported := getattr(analysis_toolchain, "check_analysis_supported", None)
) is not None:
# Raises with the reason; before the compile, not after it
check_supported()
# Always compile to ensure fresh data (fast if no changes - just relinks)
exit_code = write_cpp(config)
+15 -33
View File
@@ -2,9 +2,7 @@
Bundled names build straight from the framework tree; everything else goes
through ``esphome.platformio.library``. Mirrors ``lib_ldf_mode=off``: each
library builds its own archive; all include dirs join one global path. The
host build reuses it without a framework tree: nothing is bundled there and
every name resolves from the registry.
library builds its own archive; all include dirs join one global path.
Deviations from PlatformIO: flat-layout libraries get the recursive default
source filter; ``dot_a_linkage`` is honored; bundled libraries never run a
@@ -344,24 +342,13 @@ def _check_unfulfilled_provides(
def resolve_libraries(
framework_path: Path | None,
*,
pio_platform: str,
board_mcu: str,
cache_key: str,
framework: str | None = "arduino",
manifest_optional: bool = False,
framework_path: Path, *, pio_platform: str, board_mcu: str, cache_key: str
) -> list[ArduinoLibrary]:
"""Resolve every ``cg.add_library()`` entry into an :class:`ArduinoLibrary`.
``pio_platform``/``board_mcu`` filter manifests the way PlatformIO would
for that core (e.g. ``espressif8266``/``esp8266``); ``cache_key`` keys the
shared converter's download cache. ``framework`` is the manifest
framework token the compatibility check warns about; None skips it.
A None ``framework_path`` means no core-bundled libraries exist (the
host build): every name resolves from the registry.
``manifest_optional`` accepts libraries without a manifest, built with
PlatformIO's default layout.
shared converter's download cache.
The returned list is not topologically sorted, so the caller must link
the archives inside one ``--start-group``/``--end-group`` pair (the
@@ -372,22 +359,18 @@ def resolve_libraries(
# PlatformIO's lib_ignore covers framework-bundled libraries too; the
# shared converter only filters the registry/git ones.
lib_ignore = lib_ignore_set()
bundled_dir_names: frozenset[str] = frozenset()
if framework_path is not None:
# Exact directory names keep membership case-sensitive everywhere
# (an is_dir() probe would match "wire" on macOS/Windows and build
# the bundled Wire twice)
libraries_dir = framework_path / "libraries"
if not libraries_dir.is_dir():
# A registry fallback would fail later with a misleading
# package-not-found error per bundled name
raise EsphomeError(
f"{libraries_dir} is missing; the framework install may be "
"incomplete (run 'esphome clean-all')"
)
bundled_dir_names = frozenset(
p.name for p in libraries_dir.iterdir() if p.is_dir()
# Exact directory names keep membership case-sensitive everywhere
# (an is_dir() probe would match "wire" on macOS/Windows and build
# the bundled Wire twice)
libraries_dir = framework_path / "libraries"
if not libraries_dir.is_dir():
# A registry fallback would fail later with a misleading
# package-not-found error per bundled name
raise EsphomeError(
f"{libraries_dir} is missing; the framework install may be "
"incomplete (run 'esphome clean-all')"
)
bundled_dir_names = frozenset(p.name for p in libraries_dir.iterdir() if p.is_dir())
def _provided(name: object) -> bool:
return _is_safe_library_name(name) and name in bundled_dir_names
@@ -514,13 +497,12 @@ def resolve_libraries(
backend = LibraryBackend(
platform=pio_platform,
framework=framework,
framework="arduino",
emit=_emit,
cache_key=cache_key,
# The walk must not resolve bundled names from the registry;
# _add_bundled_dependencies adds them after emit
provides=_provided,
manifest_optional=manifest_optional,
)
if external:
convert_libraries(external, backend)
+14 -2
View File
@@ -17,7 +17,7 @@ import os
from pathlib import Path
from typing import NamedTuple
from esphome.build_helpers.ccache import ccache_env
from esphome.build_helpers.ccache import ccache_defaults_env
from esphome.build_helpers.ninja import find_ninja
from esphome.build_helpers.tools_cache import ARDUINO8266_TOOLS_CACHE, tools_cache_path
from esphome.core import EsphomeError, Version
@@ -143,5 +143,17 @@ def get_build_env(toolchain_path: Path, ccache: str | None) -> dict[str, str]:
*filter(None, env.get("PATH", "").split(os.pathsep)),
]
env["PATH"] = os.pathsep.join(parts)
env.update(ccache_env(ccache, ARDUINO8266_TOOLS_CACHE))
env.update(ccache_env(ccache))
return env
def ccache_env(ccache: str | None) -> dict[str, str]:
"""Return ccache settings for the build subprocess (not os.environ).
``ccache`` is the pre-resolved binary (resolve_ccache_path), or None
when disabled. Values the user already set in the environment are
respected.
"""
if ccache is None:
return {}
return ccache_defaults_env(get_arduino8266_tools_path() / "ccache")
+64 -10
View File
@@ -2,21 +2,21 @@
from __future__ import annotations
import json
import logging
from pathlib import Path
import subprocess
from typing import TYPE_CHECKING
from esphome.build_helpers.ccache import resolve_absolute_ccache_path
from esphome.build_helpers.native import warn_ignored_platformio_options
from esphome.build_helpers.ninja import refresh_compile_commands
from esphome.build_helpers.ccache import resolve_ccache_path
from esphome.const import (
CONF_COMPILE_PROCESS_LIMIT,
CONF_ESPHOME,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
)
from esphome.core import CORE
from esphome.core import CORE, EsphomeError
from esphome.helpers import write_file
from esphome.types import ConfigType
if TYPE_CHECKING:
@@ -28,6 +28,20 @@ _LOGGER = logging.getLogger(__name__)
_MAX_RAM_SIZE = 81920
def _warn_ignored_platformio_options() -> None:
"""Warn for component-added platformio options the native build drops."""
from esphome.core.config import NATIVE_ARDUINO_CONSUMED_PIO_OPTIONS
consumed = NATIVE_ARDUINO_CONSUMED_PIO_OPTIONS
for key in sorted(CORE.platformio_options or {}):
if key not in consumed:
_LOGGER.warning(
"platformio_options->%s is ignored when building with the "
"native 'arduino' toolchain",
key,
)
_RAM_SECTIONS = (".data", ".rodata", ".bss")
_FLASH_SECTIONS = (".irom0.text", ".text", ".text1", ".data", ".rodata")
@@ -70,17 +84,21 @@ def get_readelf_path() -> Path:
def run_compile(config: ConfigType, verbose: bool) -> int:
from esphome.arduino8266 import framework
from esphome.build_gen import arduino8266 as build_gen
from esphome.core.config import NATIVE_ARDUINO_CONSUMED_PIO_OPTIONS
warn_ignored_platformio_options(NATIVE_ARDUINO_CONSUMED_PIO_OPTIONS)
_warn_ignored_platformio_options()
paths = framework.check_and_install(CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION])
# Resolved once: the probe is not free and three consumers need it
ccache = resolve_absolute_ccache_path()
ninja_changed = build_gen.write_project(paths, ccache)
ccache = resolve_ccache_path()
build_gen.write_project(paths, ccache)
build_dir = get_build_dir()
env = framework.get_build_env(paths.toolchain, ccache)
refresh_compile_commands(paths.ninja, build_dir, env, ninja_changed)
# The DB is a pure function of build.ninja; regenerate when it is older
compdb = build_dir / "compile_commands.json"
ninja_file = build_dir / "build.ninja"
if not compdb.is_file() or compdb.stat().st_mtime < ninja_file.stat().st_mtime:
_write_compile_commands(paths.ninja, build_dir, env)
cmd = [str(paths.ninja)]
if verbose:
@@ -122,6 +140,42 @@ def run_compile(config: ConfigType, verbose: bool) -> int:
return 0
def _write_compile_commands(
ninja_path: Path, build_dir: Path, env: dict[str, str]
) -> None:
compdb = build_dir / "compile_commands.json"
result = subprocess.run(
[str(ninja_path), "-C", str(build_dir), "-t", "compdb", "c", "cxx", "asm"],
env=env,
capture_output=True,
text=True,
check=False,
close_fds=False,
)
if result.returncode != 0:
# Drop any stale database so consumers (IDE integration, clang-tidy,
# the memory analyzer) can't silently read outdated data.
compdb.unlink(missing_ok=True)
raise EsphomeError(f"Could not generate compile_commands.json: {result.stderr}")
try:
entries = json.loads(result.stdout)
except ValueError as err:
compdb.unlink(missing_ok=True)
raise EsphomeError(
f"ninja produced an unparsable compile database: {err} "
f"(output starts {result.stdout[:120]!r})"
) from err
if not entries:
# compdb exits 0 with [] for unknown rule names; a renamed compile
# rule must fail the build, not silently strand every consumer
compdb.unlink(missing_ok=True)
raise EsphomeError(
"ninja produced an empty compile database; the generator's rule "
"names no longer match"
)
write_file(compdb, result.stdout)
def _parse_app_size(build_dir: Path, paths: InstalledPaths) -> int | None:
"""Read the app flash budget (irom0_0_seg length) from the linker script."""
from esphome.build_gen.arduino8266 import get_flash_ld_path
@@ -199,7 +253,7 @@ def get_idedata(ccache: str | None = None) -> dict | None:
# A disabled ccache resolves to None without spawning anything, so
# re-resolving here costs nothing when the caller has no answer
launcher = ccache or resolve_absolute_ccache_path()
launcher = ccache or resolve_ccache_path()
return load_or_build_idedata(
get_build_dir() / "compile_commands.json",
get_elf_path(),
+13 -26
View File
@@ -345,28 +345,14 @@ async def _apply_parent(config: ConfigType, id_key: str = CONF_ID) -> str:
def _apply_lambda_args(args: TemplateArgsType) -> TemplateArgsType:
# The generated function's parameters; a std::string arg is never copied.
# Must match ApplyAction::ApplyFn and ApplyCondition::CheckFn exactly for the function
# pointer conversion.
return [
(cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), arg)
for t, arg in args
]
def _apply_function(
id_: ID,
return_type: SafeExpType,
template_arg: cg.TemplateArguments,
lambda_args: TemplateArgsType,
statements: list[str],
) -> MockObj:
"""Emit the generated function and declare ``id_`` as the ``ApplyAction`` or
``ApplyCondition`` templated on it, so ``play()`` calls it directly."""
fn = cg.static_function(
f"esphome__{id_.id}__fn", return_type, lambda_args, statements
)
return cg.new_Pvariable(id_, cg.TemplateArguments(fn, *template_arg))
async def _render_values(
name: str,
target: str,
@@ -417,8 +403,8 @@ def register_apply_action(
) -> None:
"""Register an action that only forwards config values to its parent, with no C++ class.
Generates one static function with the parent (read from ``id_key``) and constants baked
in, lambdas called inline with the trigger args, and an ``ApplyAction`` templated on it.
Generates one stateless function for ``ApplyAction<Ts...>``: the parent (read from
``id_key``) and constants are baked in, lambdas are called inline with the trigger args.
A constant that is an id (``cv.use_id`` under ``cv.templatable``) is the object it names.
With ``call`` every statement targets the call object ``auto apply_call = parent->call()``,
and ``apply_call.perform()`` is appended.
@@ -463,9 +449,10 @@ def register_apply_action(
*statements,
"apply_call.perform();",
]
return _apply_function(
action_id, cg.void, template_arg, lambda_args, statements
apply_lambda = LambdaExpression(
["\n".join(statements)], lambda_args, capture="", return_type=cg.void
)
return cg.new_Pvariable(action_id, template_arg, apply_lambda)
register_action(name, ApplyAction, schema, synchronous=True)(builder)
@@ -479,7 +466,7 @@ def register_apply_condition(
``ApplyCall`` such as ``ApplyCall("state == {}", ((CONF_STATE, cg.bool_),))`` compares
against config values, all of which must be present. Write ``== false`` to negate.
String constants are plain literals, so compare a ``std::string`` or ``StringRef`` member.
Generates one static predicate and an ``ApplyCondition`` templated on it.
Generates one stateless function for ``ApplyCondition<Ts...>``.
"""
call = check if isinstance(check, ApplyCall) else ApplyCall(check)
members = call.members
@@ -505,13 +492,13 @@ def register_apply_condition(
lambda_args,
compare=True,
)
return _apply_function(
condition_id,
cg.bool_,
template_arg,
lambda_args,
check_lambda = LambdaExpression(
[f"return {parent}->{call.target.format(*exprs)};"],
lambda_args,
capture="",
return_type=cg.bool_,
)
return cg.new_Pvariable(condition_id, template_arg, check_lambda)
register_condition(name, ApplyCondition, schema)(builder)
+122 -55
View File
@@ -14,6 +14,7 @@ from the build flags with the same precedence as the PlatformIO builder.
from __future__ import annotations
from collections.abc import Collection
from dataclasses import dataclass
import hashlib
import logging
@@ -22,26 +23,15 @@ from pathlib import Path
import re
import shlex
import subprocess
import sys
from typing import TYPE_CHECKING, NamedTuple
from esphome.arduino8266.framework import toolchain_tool
from esphome.build_helpers.idedata import is_joined_include
from esphome.build_helpers.ninja import (
escape as _e,
quote_path as _q,
shell_token as _shell_token,
)
from esphome.build_helpers.ninja_gen import (
ar_rule_lines,
collect_sources,
compile_edges,
compile_rule_lines,
library_edges,
pch_edges,
pch_rule_lines,
tool_lines,
)
from esphome.build_helpers.pch import PCH_CORE_HEADER
from esphome.components.esp8266 import build_surgery
from esphome.components.esp8266.boards import (
BOARDS,
@@ -56,9 +46,12 @@ from esphome.components.esp8266.const import (
KEY_SCANF_FLOAT,
)
from esphome.core import CORE, EsphomeError
from esphome.framework_helpers import get_project_cxx_compile_flags
from esphome.framework_helpers import (
get_project_cxx_compile_flags,
strip_win_long_path_prefix,
)
from esphome.helpers import mkdir_p, write_file_if_changed
from esphome.platformio.library import lex_build_flags
from esphome.platformio.library import SOURCE_KIND_FOR_SUFFIX, lex_build_flags
if TYPE_CHECKING:
from esphome.arduino8266.framework import InstalledPaths
@@ -650,6 +643,14 @@ _PLAIN_LINKER_PREFIXES = ("-T", "-Xlinker")
_PLAIN_DRIVER_LINK_PREFIXES = ("-fuse-ld=", "--specs=", "-specs=")
def _collect_sources(root: Path, exclude: Collection[str] = frozenset()) -> list[Path]:
return sorted(
p
for p in root.rglob("*")
if p.suffix in SOURCE_KIND_FOR_SUFFIX and p.name not in exclude
)
def _stat_sig(path: Path) -> str:
"""Size and mtime cache-stamp signature for one input file.
@@ -867,6 +868,34 @@ def _generate_testing_flash_ld(
)
def _ninja_compile_edges(
lines: list[str],
sources: list[Path],
root: Path,
group: str,
flags: str = "",
) -> list[str]:
"""Emit compile edges for ``sources``; return the object paths."""
objects = []
for src in sources:
rel = src.relative_to(root).as_posix()
obj = f"obj/{group}/{rel}.o"
escaped_obj = _e(obj)
lines.append(
f"build {escaped_obj}: {SOURCE_KIND_FOR_SUFFIX[src.suffix]} {_e(src)}"
)
if flags:
lines.append(f" flags = {flags}")
# Escaped once here: the returned paths only ever appear in build
# statements (archive/link inputs), which use ninja escaping.
objects.append(escaped_obj)
return objects
def _common_parent(paths: list[Path]) -> Path:
return Path(os.path.commonpath([str(p.parent) for p in paths]))
class _FlagSets(NamedTuple):
cflags: list[str]
cxxflags: list[str]
@@ -1053,15 +1082,48 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
+ ["stdc++-exc" if config.exceptions else "stdc++", "m", "c", "gcc"]
)
build_tool = Path(__file__).parent / "build_tool.py"
# $in/$out stay unquoted: ninja escapes its built-in path variables
# itself; only literal paths need _q().
lines = [
*tool_lines(
(toolchain_tool(paths.toolchain, "gcc"),),
(toolchain_tool(paths.toolchain, "g++"),),
ccache,
),
*compile_rule_lines(),
*ar_rule_lines(toolchain_tool(paths.toolchain, "ar")),
*pch_rule_lines(),
"# Auto-generated by ESPHome",
"ninja_required_version = 1.5",
f"cc = {_q(toolchain_tool(paths.toolchain, 'gcc'))}",
f"cxx = {_q(toolchain_tool(paths.toolchain, 'g++'))}",
# The NSIS launcher starts Python with a \\?\ extended-length path
# that cmd.exe cannot spawn; same strip every other emitted binary
# path gets
f"python = {_q(strip_win_long_path_prefix(sys.executable))}",
f"buildtool = {_q(build_tool)}",
f"ccache = {_q(ccache) if ccache else ''}",
"",
# Rule names match SOURCE_KIND_FOR_SUFFIX values (c, cxx, asm, aspp)
"rule c",
" command = $ccache $cc -MMD -MF $out.d $cflags $flags -c $in -o $out",
" depfile = $out.d",
" deps = gcc",
" description = CC $out",
"rule cxx",
" command = $ccache $cxx -MMD -MF $out.d $cxxflags $flags -c $in -o $out",
" depfile = $out.d",
" deps = gcc",
" description = CXX $out",
"rule aspp",
" command = $ccache $cc -MMD -MF $out.d -x assembler-with-cpp $asflags $flags -c $in -o $out",
" depfile = $out.d",
" deps = gcc",
" description = AS $out",
# Plain assembler, as SCons's ASCOM: no preprocessor, so no
# depfile and no $flags (defines/includes) either
"rule asm",
" command = $ccache $cc -x assembler $asflags -c $in -o $out",
" description = AS $out",
"rule ar",
f" command = $python $buildtool ar {_q(toolchain_tool(paths.toolchain, 'ar'))} $out $out.rsp",
" rspfile = $out.rsp",
" rspfile_content = $in_newline",
" description = AR $out",
"rule link",
" command = $cxx -o $out $linkflags @$out.rsp $libdirflags -Wl,--start-group $archives $libflags -Wl,--end-group",
" rspfile = $out.rsp",
@@ -1091,15 +1153,16 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
core_exclude |= _CORE_EXCLUDE_WAVEFORM
archives = []
direct_objs: list[str] = []
# variant_dir existence was already enforced with the include dirs
variant_sources = collect_sources(variant_dir)
variant_sources = _collect_sources(variant_dir)
if variant_sources:
objs = compile_edges(lines, variant_sources, variant_dir, "variant")
objs = _ninja_compile_edges(lines, variant_sources, variant_dir, "variant")
lines.append(f"build libFrameworkArduinoVariant.a: ar {' '.join(objs)}")
archives.append("libFrameworkArduinoVariant.a")
core_objs = compile_edges(
lines, collect_sources(core_dir, core_exclude), core_dir, "core"
core_objs = _ninja_compile_edges(
lines, _collect_sources(core_dir, core_exclude), core_dir, "core"
)
if not core_objs:
# An empty archive would link into a wall of undefined references
@@ -1110,14 +1173,37 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
lines.append(f"build libFrameworkArduino.a: ar {' '.join(core_objs)}")
archives.append("libFrameworkArduino.a")
lib_archives, direct_objs = library_edges(lines, libraries)
archives += lib_archives
for lib in libraries:
if not lib.sources:
# Header-only libraries are legitimate; the log makes an empty
# srcFilter or broken tree traceable before link errors do.
_LOGGER.debug(
"Library %s has no source files; contributing includes only",
lib.name,
)
continue
lib_root = _common_parent(lib.sources)
objs = _ninja_compile_edges(
lines,
lib.sources,
lib_root,
f"lib/{lib.name}",
flags=" ".join(_shell_token(f) for f in lib.flags),
)
if not lib.lib_archive:
# libArchive: false / dot_a_linkage=false: hand the objects to
# the linker directly so unreferenced-but-required symbols
# (exception handlers, weak overrides) survive
direct_objs.extend(objs)
continue
archive = f"lib{lib.name}.a"
lines.append(f"build {_e(archive)}: ar {' '.join(objs)}")
archives.append(archive)
# One source of truth with the PlatformIO path: esp8266/__init__ pins
# build_src_flags (the throw_stubs force-include); -include paths
# resolve against the source root
src_other: list[str] = []
src_includes: list[str] = []
src_parts: list[str] = []
src_it = iter(
lex_build_flags(_pio_option("build_src_flags", ""), "build_src_flags")
)
@@ -1128,34 +1214,15 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
raise EsphomeError(
"build_src_flags has a trailing '-include' with no header"
)
src_includes.append(header)
elif is_joined_include(tok):
# Left in src_other it would precede the pch include and
# silently defeat the .gch
src_includes.append(tok[len("-include") :])
src_parts.append(f"-include {_q(src_dir / header)}")
else:
src_other.append(_shell_token(tok))
include_flags = [f"-include {_q(src_dir / h)}" for h in src_includes]
src_parts.append(_shell_token(tok))
src_extra = " ".join(src_parts)
# One shared variable instead of repeating the flags line on every src
# edge (hundreds of edges in a real project)
lines.append(f"srcflags = {' '.join(src_other + include_flags)}")
src_cxx_override = pch_edges(
lines,
build_dir,
src_dir,
(*src_includes, PCH_CORE_HEADER),
flag_sets.cxxflags,
src_other,
ccache,
(str(paths.framework), str(paths.toolchain)),
)
src_objs = compile_edges(
lines,
collect_sources(src_dir),
src_dir,
"src",
flags="$srcflags",
cxx_override=src_cxx_override,
lines.append(f"srcflags = {src_extra}")
src_objs = _ninja_compile_edges(
lines, _collect_sources(src_dir), src_dir, "src", flags="$srcflags"
)
ld_deps = [f"ld/{_COMMON_LD_NAME}"]
+1 -11
View File
@@ -6,7 +6,6 @@ started esphome and must not depend on the package being importable.
Subcommands:
ar <ar-binary> <archive> <rspfile> remove stale archive, then ``ar rcs``
copy <src> <dst> copy a file
touch <path> create/update a stamp file
The ar rspfile carries one object path per line (the generating rule must
use ``$in_newline``, never ``$in``).
@@ -84,18 +83,9 @@ def _run_copy(src: str, dst: str) -> int:
return 0
def _run_touch(path: str) -> int:
try:
Path(path).touch()
except OSError as err:
print(f"touch: {path} failed: {err}", file=sys.stderr)
return 1
return 0
# mode -> (handler, expected operand count); surplus argv means a
# mis-specified ninja rule and must error, not silently drop operands
_MODES = {"ar": (_run_ar, 3), "copy": (_run_copy, 2), "touch": (_run_touch, 1)}
_MODES = {"ar": (_run_ar, 3), "copy": (_run_copy, 2)}
def main() -> int:
-81
View File
@@ -4,14 +4,6 @@ import json
import logging
from pathlib import Path
from esphome.build_helpers import pch
from esphome.build_helpers.pch import (
PCH_DEFAULT_HEADERS,
PCH_HEADER_NAME,
mark_pch_emitted,
pch_enabled,
pch_header_text,
)
from esphome.components.esp32 import (
get_esp32_variant,
get_excluded_builtin_components,
@@ -290,74 +282,9 @@ idf_component_register(
target_link_options(${{COMPONENT_LIB}} PUBLIC
{link_opts_str}
)
{_pch_cmake()}"""
def _pch_cmake() -> str:
"""The src component's precompiled-header block (C++ TUs only).
The -include stays relative (resolved from the compiler cwd, the build
dir); an absolute path would poison ccache keys.
"""
if not pch_enabled():
return ""
# Strict inverts: a per-process consumer rejection reds the build.
# Baked at generation: a knob flip takes effect when the CMakeLists is
# rewritten (every esphome compile); a hand-run idf.py keeps the old one
escalation = pch.pch_consumer_escalation()
return f"""
# ESPHome precompiled header (see esphome/build_helpers/pch.py).
# OBJECT_DEPENDS is on the header, not the .gch: pch-baked headers drop
# out of TU depfiles, and prepare_pch() touches the header on rebuild.
target_compile_options(${{COMPONENT_LIB}} PRIVATE
"$<$<COMPILE_LANGUAGE:CXX>:-Winvalid-pch>"
"$<$<COMPILE_LANGUAGE:CXX>:{escalation}>"
"$<$<COMPILE_LANGUAGE:CXX>:-include>"
"$<$<COMPILE_LANGUAGE:CXX>:{PCH_HEADER_NAME}>"
)
set_source_files_properties(${{app_sources}} PROPERTIES
OBJECT_DEPENDS "${{CMAKE_BINARY_DIR}}/{PCH_HEADER_NAME}")
"""
def discard_pch() -> None:
"""Drop the pch sidecars in the IDF build dir."""
pch.discard_pch(CORE.relative_build_path("build"))
def prepare_pch() -> None:
"""Build the .gch right before ninja, after every reconfigure, so the
compile_commands.json flags and the sdkconfig are the settled ones."""
if not pch_enabled():
# Self-cleaning escape hatch: drop any previously built .gch
pch.discard_pch(CORE.relative_build_path("build"))
pch.pch_disabled_degraded()
return
sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}")
try:
sdkconfig = sdkconfig_path.read_text(encoding="utf-8")
except OSError as err:
# Fail closed: the sdkconfig is the .sum's only config identity for
# sdkconfig.h-only options; a stand-in marker would collide
_LOGGER.warning(
"Could not read %s; compiling without the pch: %s", sdkconfig_path, err
)
pch.discard_pch(CORE.relative_build_path("build"))
pch.pch_degraded(f"sdkconfig unreadable: {err}")
return
pch.prepare_pch(
CORE.relative_build_path("build"),
PCH_DEFAULT_HEADERS,
(
str(idf_version()),
CORE.cpp_standard or "",
sdkconfig,
*get_project_compile_flags(),
*get_project_cxx_compile_flags(),
),
)
def write_project(
minimal: bool = False, builtin_components: list[str] | None = None
) -> None:
@@ -377,14 +304,6 @@ def write_project(
get_component_cmakelists(),
)
if pch_enabled():
write_file_if_changed(
CORE.relative_build_path("build", PCH_HEADER_NAME),
pch_header_text(PCH_DEFAULT_HEADERS),
)
# Consumers carry the -include; gate the ccache relaxation on it
mark_pch_emitted()
# Snapshot the exclusion set so has_outdated_files() can trigger a
# discovery reconfigure when it changes. Excluded components never
# register in project_description.json, so re-including one (e.g. a
-296
View File
@@ -1,296 +0,0 @@
"""Native ninja build generator for the host platform.
Emits ``build.ninja`` under ``.pioenvs/<name>/``: every source in the
generated ``src/`` tree plus the resolved registry libraries compiles with
the machine's compiler and links into ``program``, the name PlatformIO's
native platform produced. Build flags route the way SCons's ``ParseFlags``
did under PlatformIO: ``-D``/``-I``/``-std=``/``-W`` shapes reach the
compile lines only, ``-l``/``-L``/``-Wl,`` the link line only, everything
else both.
"""
from __future__ import annotations
from collections.abc import Iterable
import logging
import os
from pathlib import Path
import sys
from typing import TYPE_CHECKING
from esphome.build_helpers.ninja import escape as _e, quote_path as _q, shell_token
from esphome.build_helpers.ninja_gen import (
PATH_ARG_FLAGS,
Flag,
anchor_path_flag,
ar_rule_lines,
collect_sources,
compile_edges,
compile_rule_lines,
library_edges,
pch_edges,
pch_rule_lines,
tool_lines,
)
from esphome.build_helpers.pch import PCH_CORE_HEADER
from esphome.core import CORE, EsphomeError
from esphome.framework_helpers import get_project_cxx_compile_flags
from esphome.helpers import mkdir_p, write_file_if_changed
from esphome.host.toolchain import PROGRAM_NAME, HostCompilers, find_tool, get_build_dir
from esphome.platformio.library import lex_build_flags
if TYPE_CHECKING:
from esphome.arduino.library import ArduinoLibrary
_LOGGER = logging.getLogger(__name__)
# The PlatformIO platform the host built under; registry manifests declare
# compatibility against it, as lib_compat_mode=strict checked before
PIO_PLATFORM = "native"
# Namespaces the shared library download cache (pio_components/host/)
LIBRARY_CACHE_KEY = "host"
# Flag shapes that only the compiler understands; dropped from the link line
_COMPILE_ONLY_PREFIXES = ("-D", "-U", "-I", "-std=", "-W", *PATH_ARG_FLAGS)
# Flag shapes that only the linker consumes; inert on a -c compile line
_LINK_ONLY_PREFIXES = ("-l", "-L", "-Wl,")
# Link-only flags whose argument is the next token
_LINK_ONLY_ARG_FLAGS = ("-framework", "-Xlinker", "-z")
def parse_flags(entries: Iterable[str], owner: str) -> list[Flag]:
"""Lex build flag entries into flags, each with its argument.
Entries are a set, so their order is not the user's: a flag and the
argument it takes as the next token must share one entry.
"""
flags: list[Flag] = []
for entry in entries:
it = iter(lex_build_flags(entry, owner))
for tok in it:
if tok not in PATH_ARG_FLAGS and tok not in _LINK_ONLY_ARG_FLAGS:
flags.append((tok,))
continue
arg = next(it, None)
# A path never starts with "-"; that is the next flag
if arg is None or (tok in PATH_ARG_FLAGS and arg.startswith("-")):
raise EsphomeError(
f"{owner} build flags have '{tok}' with no argument; write "
f"the flag and its argument as one entry"
)
flags.append((tok, arg))
return flags
def split_flags(flags: list[Flag]) -> tuple[list[Flag], list[Flag]]:
"""Route build flags to the compile and link lines."""
compile_flags: list[Flag] = []
link_flags: list[Flag] = []
for flag in flags:
name = flag[0]
if len(flag) > 1:
(compile_flags if name in PATH_ARG_FLAGS else link_flags).append(flag)
elif name.startswith(_LINK_ONLY_PREFIXES):
# Checked before the compile prefixes: -Wl, would match -W
link_flags.append(flag)
elif name.startswith(_COMPILE_ONLY_PREFIXES):
compile_flags.append(flag)
else:
# -g, -O, -f*, -m*, -pthread, --coverage: both lines, as SCons
compile_flags.append(flag)
link_flags.append(flag)
return compile_flags, link_flags
def _is_std(flag: Flag) -> bool:
return flag[0].startswith("-std=")
def _is_cxx_std(flag: Flag) -> bool:
return _is_std(flag) and "++" in flag[0]
def _anchored_flags(entries: Iterable[str], owner: str) -> list[Flag]:
build_path = Path(CORE.build_path)
return [
anchor_path_flag(flag, build_path)
for flag in parse_flags(sorted(entries), owner)
]
def _flag_lists() -> tuple[list[str], list[str], list[str]]:
"""The C, C++, and link flag lists (raw tokens), build_unflags applied.
``cg.set_cpp_standard`` wins over any ``-std=`` in the build flags for
C++ compiles, as PlatformIO's unflag of every other standard did; C
compiles never see a C++ standard.
"""
# The funnel warns and drops empty glued arguments (-D "") itself
compile_flags, link_flags = split_flags(
_anchored_flags(CORE.build_flags, "esphome")
)
cflags = [f for f in compile_flags if not _is_cxx_std(f)]
cxx_std = CORE.cpp_standard
cxxflags = [f for f in compile_flags if not (cxx_std and _is_std(f))]
if cxx_std:
cxxflags.insert(0, (f"-std={cxx_std}",))
cxxflags += [(tok,) for tok in get_project_cxx_compile_flags()]
# A flag is removed whole, with its argument, as PlatformIO did
unflags = set(_anchored_flags(CORE.build_unflags, "esphome build_unflags"))
# An unflag that hits nothing (a typo, or -DUSE_FOO against
# -DUSE_FOO=1) must be visible, since the user believes the flag is
# gone while it still drives the build
if unmatched := sorted(unflags - set(cflags) - set(cxxflags) - set(link_flags)):
_LOGGER.warning(
"build_unflags entries matched no build flag: %s",
", ".join(" ".join(flag) for flag in unmatched),
)
def keep(flags: list[Flag]) -> list[str]:
return [tok for flag in flags if flag not in unflags for tok in flag]
return keep(cflags), keep(cxxflags), keep(link_flags)
def _resolve_host_libraries() -> list[ArduinoLibrary]:
"""Every ``cg.add_library()`` entry, fetched from the registry.
The host has no framework, so nothing is bundled and no framework
compatibility check applies; the platform check keeps the strict
manifest gate PlatformIO's native platform enforced. Manifest-less
libraries (a bare git checkout) build with PlatformIO's default
layout, as they did under its native platform.
"""
if not CORE.platformio_libraries:
return []
from esphome.arduino.library import resolve_libraries
return resolve_libraries(
None,
pio_platform=PIO_PLATFORM,
board_mcu="host",
cache_key=LIBRARY_CACHE_KEY,
framework=None,
manifest_optional=True,
)
def _file_macro_maps(build_dir: Path) -> list[str]:
"""Flags that keep ``__FILE__`` relative to the build path.
PlatformIO compiled ``src/x.cpp`` from the build path, and tools name
things after that spelling (CodSpeed's benchmark ids). Here a source
reaches the compiler by its absolute path, or relative to the build
directory when ccache rewrites it.
"""
build_path = Path(CORE.build_path)
prefixes = (build_path, Path(os.path.relpath(build_path, build_dir)))
return [
shell_token(f"-fmacro-prefix-map={prefix}{os.sep}=", force=True)
for prefix in prefixes
]
def write_project(compilers: HostCompilers, ccache: str | None) -> bool:
"""Write the ninja build for the current configuration.
``ccache`` is the caller's already-resolved binary (None when disabled).
Returns True when ``build.ninja`` changed, so the caller can skip work
derived purely from it (the compile database) on unchanged builds.
"""
build_dir = get_build_dir()
mkdir_p(build_dir)
src_dir = CORE.relative_src_path()
if not src_dir.is_dir():
# Generated project state, not install state: clean-all would not help
raise EsphomeError(f"Generated source directory {src_dir} is missing")
cflags, cxxflags, link_flags = _flag_lists()
libraries = _resolve_host_libraries()
include_dirs = [src_dir]
for lib in libraries:
include_dirs += lib.include_dirs
includes = [f"-I{_q(d)}" for d in include_dirs]
includes += _file_macro_maps(build_dir)
# SCons's link line: $LINKFLAGS $SOURCES $_LIBDIRFLAGS $_LIBFLAGS, so
# -L and -l trail the objects while every other link token leads
lib_dirs = [Path(t[2:]) for t in link_flags if t.startswith("-L")]
libs = [t for t in link_flags if t.startswith("-l")]
linkflags = [shell_token(t) for t in link_flags if not t.startswith(("-L", "-l"))]
for lib in libraries:
lib_dirs += lib.link_dirs
libs += [f"-l{name}" for name in lib.link_libs]
linkflags += [shell_token(f) for f in lib.link_flags]
# PlatformIO's ASPPCOM passes only -D/-I user flags to assembly
asflags = [t for t in cflags if t.startswith(("-D", "-I"))]
lines = [
*tool_lines(compilers.cc, compilers.cxx, ccache),
*compile_rule_lines(),
*pch_rule_lines(),
"rule link",
" command = $cxx -o $out $linkflags @$out.rsp $archives $libdirflags $libflags",
" rspfile = $out.rsp",
" rspfile_content = $in_newline",
" description = LINK $out",
]
if any(lib.sources and lib.lib_archive for lib in libraries):
# Resolved only when an archive is built, so a system without
# binutils still links a library-free configuration
lines += ar_rule_lines(find_tool("AR", ("ar",)))
lines += [
"",
f"cflags = {' '.join([*map(shell_token, cflags), *includes])}",
f"cxxflags = {' '.join([*map(shell_token, cxxflags), *includes])}",
f"asflags = {' '.join([*map(shell_token, asflags), *includes])}",
f"linkflags = {' '.join(linkflags)}",
f"libdirflags = {' '.join(f'-L{_q(d)}' for d in lib_dirs)}",
f"libflags = {' '.join(shell_token(lib) for lib in libs)}",
"",
]
archives, direct_objs = library_edges(lines, libraries)
from esphome.components.host import HOST_PCH_PREFIX
# Host has no framework force-includes; the per-TU cost is the STL
# closure behind the core headers, so those are what is precompiled
src_cxx_override = pch_edges(
lines,
build_dir,
src_dir,
(HOST_PCH_PREFIX, PCH_CORE_HEADER),
cxxflags,
(),
ccache,
compilers.cxx,
)
src_objs = compile_edges(
lines,
collect_sources(src_dir),
src_dir,
"src",
cxx_override=src_cxx_override,
)
if not src_objs:
raise EsphomeError(f"No source files found under {src_dir}")
# Archives are not topologically sorted; GNU ld needs the group to
# resolve references between them. ld64 loads archives iteratively and
# rejects the option, so macOS lists them bare.
archive_tokens = [shell_token(a) for a in archives]
if archive_tokens and sys.platform != "darwin":
archive_tokens = ["-Wl,--start-group", *archive_tokens, "-Wl,--end-group"]
lines.append(
f"build {PROGRAM_NAME}: link {' '.join(src_objs + direct_objs)} | "
f"{' '.join(_e(a) for a in archives)}"
)
lines.append(f" archives = {' '.join(archive_tokens)}")
lines.append(f"default {PROGRAM_NAME}")
lines.append("")
return write_file_if_changed(build_dir / "build.ninja", "\n".join(lines))
+4 -48
View File
@@ -21,13 +21,12 @@ def _ccache_runs(ccache: str) -> bool:
)
def parse_enable_env(name: str, strict: bool = False) -> bool | None:
def parse_enable_env(name: str) -> bool | None:
"""Strictly parse an on/off environment knob; None when unset or invalid.
``bool(str)`` truthiness would flip ``no``/``off`` to enabled, so only
1/true/yes/on and 0/false/no/off count; anything else warns and reads
as unset so the caller's default policy applies — or raises when
``strict`` (a typo must not silently disable a CI gate).
as unset so the caller's default policy applies.
"""
raw = os.environ.get(name)
if raw is None:
@@ -40,10 +39,6 @@ def parse_enable_env(name: str, strict: bool = False) -> bool | None:
return True
if lowered in FALSY_ENV_STRINGS:
return False
if strict:
from esphome.core import EsphomeError
raise EsphomeError(f"Unrecognized {name}={raw!r}; use 1 or 0")
_LOGGER.warning("Ignoring unrecognized %s=%r; use 1 or 0", name, raw)
return None
@@ -73,15 +68,6 @@ def resolve_ccache_path() -> str | None:
return ccache
def resolve_absolute_ccache_path() -> str | None:
"""``resolve_ccache_path`` for the ninja backends, which run their
commands from the build directory, where a relative path is lost."""
from esphome.build_helpers.ninja import absolute_tool
ccache = resolve_ccache_path()
return absolute_tool(ccache) if ccache else None
def ccache_defaults_env(cache_dir: Path) -> dict[str, str]:
"""Default ``CCACHE_*`` values for a build subprocess (not os.environ).
@@ -98,39 +84,9 @@ def ccache_defaults_env(cache_dir: Path) -> dict[str, str]:
"CORE.build_path must be set before constructing the build environment"
)
defaults = {
# ccache expands $VAR in its settings; $$ is a literal $
"CCACHE_DIR": str(cache_dir).replace("$", "$$"),
"CCACHE_DIR": str(cache_dir),
"CCACHE_NOHASHDIR": "true",
"CCACHE_DEPEND": "1",
"CCACHE_BASEDIR": str(Path(CORE.build_path).resolve()).replace("$", "$$"),
"CCACHE_BASEDIR": str(Path(CORE.build_path).resolve()),
}
return {k: v for k, v in defaults.items() if k not in os.environ}
def effective_ccache_basedir() -> str:
"""The prefix ccache rewrites out of hashed paths: a user CCACHE_BASEDIR
wins, else the resolved build path (matching ccache_defaults_env)."""
from esphome.core import CORE
raw = os.environ.get("CCACHE_BASEDIR")
if raw is not None and Path(raw).is_absolute() and len(Path(raw).parts) > 1:
return raw
# Unset or degenerate ("", "/", relative): fall back to the build path
return str(Path(CORE.build_path).resolve())
def ccache_env(ccache: str | None, tools_cache: tuple[str, str]) -> dict[str, str]:
"""The ccache settings for a build subprocess (not os.environ).
``ccache`` is the pre-resolved binary (resolve_ccache_path), or None when
disabled; ``tools_cache`` is the backend's tools cache spec, which holds
its ccache dir.
"""
if ccache is None:
return {}
from esphome.build_helpers.pch import ccache_pch_env
from esphome.build_helpers.tools_cache import tools_cache_path
env = ccache_defaults_env(tools_cache_path(*tools_cache) / "ccache")
env.update(ccache_pch_env())
return env
+14 -43
View File
@@ -50,11 +50,11 @@ def warn_if_idedata_missing(get_idedata: Callable[[], dict | None]) -> None:
_LOGGER.debug("Idedata failure detail", exc_info=True)
# C++ translation-unit suffixes.
CXX_SOURCE_SUFFIXES = (".cpp", ".cc", ".cxx")
# C++ translation-unit suffixes used to identify ESPHome source files.
_CXX_SUFFIXES = (".cpp", ".cc")
# Suffixes of input/output files that appear bare on the command line (and so
# must not be mistaken for compiler flags).
_INPUT_FILE_SUFFIXES = (*CXX_SOURCE_SUFFIXES, ".c", ".o", ".S", ".s")
_INPUT_FILE_SUFFIXES = (*_CXX_SUFFIXES, ".c", ".o", ".S", ".s")
# Path marker identifying an ESPHome source translation unit.
_ESPHOME_SRC_MARKER = "/src/esphome/"
@@ -63,11 +63,11 @@ def _is_esphome_src(file: str) -> bool:
"""Whether ``file`` is an ESPHome C++ translation unit; normalized to
``/`` first since Windows compile DBs use backslashes."""
return _ESPHOME_SRC_MARKER in file.replace("\\", "/") and file.endswith(
CXX_SOURCE_SUFFIXES
_CXX_SUFFIXES
)
def split_command(command: str) -> list[str]:
def _split_command(command: str) -> list[str]:
r"""Tokenize a compile_commands.json / response-file command string.
On Windows, tokenize per Windows ``argv`` rules via ``CommandLineToArgvW``.
@@ -103,7 +103,7 @@ def split_command(command: str) -> list[str]:
ctypes.windll.kernel32.LocalFree(argv)
def expand_response_files(tokens: list[str], directory: Path) -> list[str]:
def _expand_response_files(tokens: list[str], directory: Path) -> list[str]:
"""Inline any ``@response-file`` arguments (paths relative to ``directory``).
GCC response files embed flags that must be expanded so GCC-only flags
@@ -118,8 +118,8 @@ def expand_response_files(tokens: list[str], directory: Path) -> list[str]:
rf = directory / rf
try:
out.extend(
expand_response_files(
split_command(rf.read_text(encoding="utf-8")), directory
_expand_response_files(
_split_command(rf.read_text(encoding="utf-8")), directory
)
)
continue
@@ -138,7 +138,7 @@ def _pick_entry(entries: list[dict]) -> dict:
if _is_esphome_src(entry["file"]):
return entry
for entry in entries:
if entry["file"].endswith(CXX_SOURCE_SUFFIXES):
if entry["file"].endswith(_CXX_SUFFIXES):
return entry
raise ValueError("no C++ translation unit found in compile_commands.json")
@@ -148,25 +148,16 @@ def _pick_entry(entries: list[dict]) -> dict:
_LAUNCHER_STEMS = frozenset({"ccache", "sccache", "distcc", "icecc", "buildcache"})
def is_launcher(token: str) -> bool:
def _is_launcher(token: str) -> bool:
return Path(token).stem.lower() in _LAUNCHER_STEMS
def is_joined_include(tok: str) -> bool:
"""The joined ``-includefoo.h`` spelling; excludes clang's -include-pch."""
return (
tok.startswith("-include")
and tok != "-include"
and not tok.startswith("-include-")
)
def parse_entry(
entry: dict, launcher: str | None = None
) -> tuple[str, list[str], list[str], list[str]]:
"""Parse one compile_commands entry -> (cxx_path, defines, includes, cxx_flags)."""
directory = Path(entry["directory"])
tokens = expand_response_files(split_command(entry["command"]), directory)
tokens = _expand_response_files(_split_command(entry["command"]), directory)
def _include(raw: str) -> str:
# Resolve against the entry's ``directory`` so cached idedata works
@@ -182,7 +173,7 @@ def parse_entry(
if not tokens:
# An empty command, or one that was only the launcher; fail by name
raise ValueError(f"empty compile command for {entry.get('file')}")
if is_launcher(tokens[0]) and len(tokens) > 1 and not tokens[1].startswith("-"):
if _is_launcher(tokens[0]) and len(tokens) > 1 and not tokens[1].startswith("-"):
# Stale DB built with a launcher this run no longer configures; the
# real compiler is the next token
_LOGGER.warning("Stripping unconfigured launcher %s", tokens[0])
@@ -195,23 +186,11 @@ def parse_entry(
defines: list[str] = []
includes: list[str] = []
cxx_flags: list[str] = []
unresolved_force_includes: list[str] = []
it = iter(tokens[1:])
for tok in it:
if tok in ("-c", "-o"):
next(it, None) # drop the flag and its argument (input/output)
elif tok == "-include" or is_joined_include(tok):
# Re-anchor only names next to the compile (the pch); a name
# meant for the -I chain must stay untouched
raw = next(it, "") if tok == "-include" else tok[len("-include") :]
if not raw:
_LOGGER.warning("Dropping -include with no argument")
elif Path(resolved := _include(raw)).is_file():
cxx_flags.extend(("-include", resolved))
else:
unresolved_force_includes.append(raw)
cxx_flags.extend(("-include", raw))
elif tok.startswith("-D"):
# ``.strip()`` handles tokens like ``-D CONFIGURED=1`` (a single
# quoted arg with a space after -D) that some flags arrive as.
@@ -230,14 +209,6 @@ def parse_entry(
pass # input/output files
else:
cxx_flags.append(tok)
for raw in unresolved_force_includes:
# A deleted build artifact would otherwise surface only downstream
if not any((Path(inc) / raw).is_file() for inc in includes):
_LOGGER.warning(
"-include %s found neither next to the compile nor on the "
"include path; cached idedata may not resolve it",
raw,
)
return cxx_path, defines, includes, cxx_flags
@@ -302,7 +273,7 @@ def _cache_usable(cached: object) -> bool:
if not isinstance(cached, dict) or "cc_path" not in cached:
return False
cxx_path = cached.get("cxx_path")
if not isinstance(cxx_path, str) or is_launcher(cxx_path):
if not isinstance(cxx_path, str) or _is_launcher(cxx_path):
return False
includes = cached.get("includes")
return isinstance(includes, dict) and isinstance(includes.get("build"), list)
@@ -350,7 +321,7 @@ def load_or_build_idedata(
def reject_launcher_compiler(cxx_path: str) -> None:
"""Reject a compile DB naming a launcher (ccache) as the compiler; it
must never be probed, cached, or consumed."""
if is_launcher(cxx_path):
if _is_launcher(cxx_path):
raise EsphomeError(
f"compile_commands.json names the launcher {cxx_path} as the "
"compiler; the compile database is unusable"
-22
View File
@@ -7,21 +7,16 @@ platform component package or the backend itself.
from __future__ import annotations
from collections.abc import Collection
import importlib
import logging
from types import ModuleType
from esphome.const import Toolchain
from esphome.core import CORE, EsphomeError
_LOGGER = logging.getLogger(__name__)
# Native build backend per (target platform, toolchain)
NATIVE_TOOLCHAIN_MODULES = {
("esp32", Toolchain.ESP_IDF): "esphome.espidf.toolchain",
("esp8266", Toolchain.ARDUINO): "esphome.arduino8266.toolchain",
("host", Toolchain.HOST): "esphome.host.toolchain",
}
@@ -55,20 +50,3 @@ def analysis_backend() -> ModuleType | None:
return native
module_path = ANALYSIS_TOOLCHAIN_MODULES.get((CORE.target_platform, CORE.toolchain))
return importlib.import_module(module_path) if module_path else None
def warn_ignored_platformio_options(consumed: Collection[str]) -> None:
"""Warn for component-added platformio options a native build drops.
User-supplied keys were already routed or warned about by
``core/config.py``; what survives into ``CORE.platformio_options`` came
from ``cg.add_platformio_option`` calls in components.
"""
for key in sorted(CORE.platformio_options or {}):
if key not in consumed:
_LOGGER.warning(
"platformio_options->%s is ignored when building with the "
"native '%s' toolchain",
key,
CORE.toolchain.value,
)
+3 -93
View File
@@ -2,17 +2,14 @@
from __future__ import annotations
import json
import logging
import os
from pathlib import Path
import re
import shutil
import subprocess
from esphome.core import EsphomeError
from esphome.framework_helpers import strip_win_long_path_prefix, tool_version_runs
from esphome.helpers import write_file_if_changed
_LOGGER = logging.getLogger(__name__)
@@ -26,26 +23,11 @@ def _ninja_runs(binary: str) -> bool:
)
# Compile rule names the generators emit; ninja's compdb tool is asked for
# exactly these, so a renamed rule fails the build instead of stranding idedata
COMPILE_RULES = ("c", "cxx", "aspp", "asm")
def absolute_tool(tool: str | Path) -> str:
"""A tool path that still resolves from the build directory.
``shutil.which`` returns a relative path for a relative PATH entry, and
ninja runs the commands from ``.pioenvs/<name>``. Symlinks are kept:
ccache's compiler links depend on the name they are called by.
"""
return strip_win_long_path_prefix(str(Path(tool).absolute()))
def find_ninja() -> Path:
"""Locate the ninja binary: a runnable PATH hit first, else the ninja
PyPI wheel."""
if binary := shutil.which("ninja"):
binary = absolute_tool(binary)
binary = strip_win_long_path_prefix(binary)
if _ninja_runs(binary):
return Path(binary)
import_error: ImportError | None = None
@@ -67,18 +49,8 @@ def find_ninja() -> Path:
def escape(value: Path | str) -> str:
"""Escape a path or token for a ninja file.
ninja has no escape for ``|`` or a line break in a path, so those fail
here by name instead of producing a build file ninja misreads.
"""
text = str(value)
if bad := next((c for c in "|\r\n" if c in text), None):
raise EsphomeError(
f"Path {text!r} contains {bad!r}, which a ninja build file cannot "
"express; rename the file or directory"
)
return text.replace("$", "$$").replace(":", "$:").replace(" ", "$ ")
"""Escape a path or token for a ninja file."""
return str(value).replace("$", "$$").replace(":", "$:").replace(" ", "$ ")
def quote_arg(tok: str) -> str:
@@ -118,65 +90,3 @@ def shell_token(tok: str, force: bool = False) -> str:
def quote_path(value: Path | str) -> str:
"""Force-quote a path for the ninja command line (shell/CreateProcess)."""
return shell_token(str(value), force=True)
def refresh_compile_commands(
ninja_path: Path, build_dir: Path, env: dict[str, str], ninja_changed: bool
) -> None:
"""Regenerate the compile DB (a pure function of build.ninja) when stale.
Freshness rides a stamp: the DB itself is written through
write_file_if_changed (its mtime feeds the idedata cache), so a
regeneration with identical content would stay "stale" forever. An
interrupted previous run may have rewritten the manifest without
regenerating the DB, hence the mtime comparison.
"""
compdb = build_dir / "compile_commands.json"
compdb_stamp = build_dir / ".compile_commands.stamp"
ninja_file = build_dir / "build.ninja"
if (
ninja_changed
or not compdb.is_file()
or not compdb_stamp.is_file()
or compdb_stamp.stat().st_mtime < ninja_file.stat().st_mtime
):
write_compile_commands(ninja_path, build_dir, env)
compdb_stamp.touch()
def write_compile_commands(
ninja_path: Path, build_dir: Path, env: dict[str, str]
) -> None:
compdb = build_dir / "compile_commands.json"
result = subprocess.run(
[str(ninja_path), "-C", str(build_dir), "-t", "compdb", *COMPILE_RULES],
env=env,
capture_output=True,
text=True,
check=False,
close_fds=False,
)
if result.returncode != 0:
# Drop any stale database so consumers (IDE integration, clang-tidy,
# the memory analyzer) can't silently read outdated data
compdb.unlink(missing_ok=True)
raise EsphomeError(f"Could not generate compile_commands.json: {result.stderr}")
try:
entries = json.loads(result.stdout)
except ValueError as err:
compdb.unlink(missing_ok=True)
raise EsphomeError(
f"ninja produced an unparsable compile database: {err} "
f"(output starts {result.stdout[:120]!r})"
) from err
if not entries:
# compdb exits 0 with [] for unknown rule names; a renamed compile
# rule must fail the build, not silently strand every consumer
compdb.unlink(missing_ok=True)
raise EsphomeError(
"ninja produced an empty compile database; the generator's rule "
"names no longer match"
)
# write_file_if_changed keeps the mtime stable on no-op builds so the
# idedata cache stays valid
write_file_if_changed(compdb, result.stdout)
-338
View File
@@ -1,338 +0,0 @@
"""The parts of a ``build.ninja`` every native backend emits the same way.
Rule names match ``SOURCE_KIND_FOR_SUFFIX`` values (c, cxx, asm, aspp) and
``ninja.COMPILE_RULES``, which the compile database is asked for.
"""
from __future__ import annotations
from collections.abc import Collection, Sequence
import logging
import os
from pathlib import Path
import sys
from typing import TYPE_CHECKING
from esphome.build_helpers.ccache import effective_ccache_basedir
from esphome.build_helpers.ninja import (
escape as _e,
quote_path as _q,
shell_token as _shell_token,
)
from esphome.build_helpers.pch import (
PCH_HEADER_NAME,
mark_pch_emitted,
pch_checksum,
pch_consumer_escalation,
pch_degraded,
pch_disabled_degraded,
pch_enabled,
pch_header_text,
pch_probe_args,
pch_strict,
)
from esphome.core import CORE
from esphome.framework_helpers import strip_win_long_path_prefix
from esphome.helpers import write_file_if_changed
from esphome.platformio.library import SOURCE_KIND_FOR_SUFFIX
if TYPE_CHECKING:
from esphome.arduino.library import ArduinoLibrary
_LOGGER = logging.getLogger(__name__)
_BUILD_TOOL = Path(__file__).parent.parent / "build_gen" / "build_tool.py"
def collect_sources(root: Path, exclude: Collection[str] = frozenset()) -> list[Path]:
return sorted(
p
for p in root.rglob("*")
if p.suffix in SOURCE_KIND_FOR_SUFFIX and p.name not in exclude
)
def common_parent(paths: list[Path]) -> Path:
return Path(os.path.commonpath([str(p.parent) for p in paths]))
def _command(words: Sequence[Path | str]) -> str:
program, *args = words
return " ".join([_q(program), *(_shell_token(str(a)) for a in args)])
def tool_lines(
cc: Sequence[Path | str], cxx: Sequence[Path | str], ccache: str | None
) -> list[str]:
"""The file header: the compilers and the helper tools as variables.
A compiler is its program followed by any arguments it always takes.
"""
return [
"# Auto-generated by ESPHome",
"ninja_required_version = 1.5",
f"cc = {_command(cc)}",
f"cxx = {_command(cxx)}",
# The NSIS launcher starts Python with a \\?\ extended-length path
# that cmd.exe cannot spawn; same strip every other emitted binary
# path gets
f"python = {_q(strip_win_long_path_prefix(sys.executable))}",
f"buildtool = {_q(_BUILD_TOOL)}",
f"ccache = {_q(ccache) if ccache else ''}",
"",
]
def compile_rule_lines() -> list[str]:
"""The compile rules; ``$own_includes`` is empty unless an edge sets it."""
return [
"rule c",
" command = $ccache $cc -MMD -MF $out.d $own_includes $cflags $flags -c $in -o $out",
" depfile = $out.d",
" deps = gcc",
" description = CC $out",
"rule cxx",
" command = $ccache $cxx -MMD -MF $out.d $own_includes $cxxflags $flags -c $in -o $out",
" depfile = $out.d",
" deps = gcc",
" description = CXX $out",
"rule aspp",
" command = $ccache $cc -MMD -MF $out.d -x assembler-with-cpp $own_includes $asflags $flags -c $in -o $out",
" depfile = $out.d",
" deps = gcc",
" description = AS $out",
# Plain assembler, as SCons's ASCOM: no preprocessor, so no
# depfile and no $flags (defines/includes) either
"rule asm",
" command = $ccache $cc -x assembler $asflags -c $in -o $out",
" description = AS $out",
]
def pch_rule_lines() -> list[str]:
"""The precompiled header rule, for a generator that emits one."""
return [
# No $ccache: the .gch is compiled once per build dir and ccache
# cannot cache it usefully (its bytes embed build-dir paths)
"rule pch",
" command = $cxx -MMD -MF $out.d -x c++-header $cxxflags $flags -c $in -o $out",
" depfile = $out.d",
" deps = gcc",
" description = PCH $out",
]
def pch_edges(
lines: list[str],
build_dir: Path,
src_dir: Path,
headers: Sequence[str],
cxxflags: Sequence[str],
src_flags: Sequence[str],
ccache: str | None,
identity: Sequence[str],
) -> tuple[str, str] | None:
"""Emit the precompiled header for the C++ src edges.
``headers`` are folded into one prefix header, ``src_flags`` are the
flags every src edge carries, and ``identity`` names what the compile
depends on beyond the headers and flags (versioned install paths).
Returns the ``cxx_override`` for ``compile_edges``, or None when the
build goes without a precompiled header.
"""
if not pch_enabled():
pch_disabled_degraded()
return None
if any(tok.startswith("-include") for tok in cxxflags):
# $cxxflags expands first, so a user -include there means GCC would
# never load the .gch
_LOGGER.warning(
"A -include in build_flags prevents the precompiled header from "
"loading; compiling without it"
)
pch_degraded("a user -include precedes the pch")
return None
pch_header = build_dir / PCH_HEADER_NAME
pch_text = pch_header_text(headers)
checksum = None
try:
if ccache:
# The .sum exists only for CCACHE_PCH_EXTSUM; ninja's depfile
# handles staleness. Strip resolved and raw build paths
# (symlinks) so identical configs share cache entries
flags_id = (
" ".join(cxxflags)
.replace(effective_ccache_basedir(), "")
.replace(str(CORE.build_path), "")
)
# The header text covers include order
checksum = pch_checksum(src_dir, headers, (pch_text, *identity, flags_id))
except (OSError, UnicodeError) as err:
# Identity unknown: a stale cache entry must never be served
_LOGGER.warning(
"Could not establish the pch identity; compiling without it: %s", err
)
pch_degraded(f"identity unknown: {err}")
return None
_LOGGER.info(
"Compiling with a precompiled header (set ESPHOME_PCH_ENABLE=0 to disable)"
)
write_file_if_changed(pch_header, pch_text)
sum_path = build_dir / f"{PCH_HEADER_NAME}.gch.sum"
if checksum is not None:
# Generate-time stamp: a hand-run ninja can rebuild the .gch
# while this .sum lags
write_file_if_changed(sum_path, checksum + "\n")
else:
# A stale .sum from an earlier ccache run must not survive
sum_path.unlink(missing_ok=True)
gch = _e(f"{PCH_HEADER_NAME}.gch")
lines.append(f"build {gch}: pch {_e(pch_header)}")
if src_flags:
lines.append(f" flags = {' '.join(src_flags)}")
# Relative -include: absolute would break cross-device ccache.
# -Wno-error keeps a rejected .gch a warning under user -Werror;
# strict inverts it so any consumer rejection reds the build
# (rejection is per-process, so the probe alone cannot prove
# the consumers)
cxx_parts = [
*src_flags,
f"-Winvalid-pch {pch_consumer_escalation()} -include {PCH_HEADER_NAME}",
]
lines.append(f"srccxxflags = {' '.join(cxx_parts)}")
pch_dep = gch
if pch_strict():
# Consumers wait on the probe stamp, so an unloadable .gch
# reds the build here instead of warning ~100 times
probe = " ".join(pch_probe_args(PCH_HEADER_NAME, source=os.devnull))
lines.append("rule pchprobe")
# $out only expands in rule text, hence the inline stamp
lines.append(
f" command = $cxx $cxxflags $flags {probe}"
" && $python $buildtool touch $out"
)
lines.append(" description = PCHPROBE $out")
# Runs when the .gch is (re)built; strict consumer -Werror
# covers a cached .gch this process cannot load
lines.append(f"build esphome_pch.probe: pchprobe {gch}")
if src_flags:
lines.append(f" flags = {' '.join(src_flags)}")
pch_dep = f"{gch} esphome_pch.probe"
mark_pch_emitted()
return ("$srccxxflags", pch_dep)
def ar_rule_lines(ar: Path | str) -> list[str]:
return [
"rule ar",
f" command = $python $buildtool ar {_q(ar)} $out $out.rsp",
" rspfile = $out.rsp",
" rspfile_content = $in_newline",
" description = AR $out",
]
def compile_edges(
lines: list[str],
sources: list[Path],
root: Path,
group: str,
flags: str = "",
own_includes: str = "",
cxx_override: tuple[str, str] | None = None,
) -> list[str]:
"""Emit compile edges for ``sources``; return the object paths.
``cxx_override`` is a (flags, implicit-dep) pair applied to C++ edges
only, replacing ``flags`` (used for the precompiled header).
"""
objects = []
for src in sources:
rel = src.relative_to(root).as_posix()
obj = f"obj/{group}/{rel}.o"
escaped_obj = _e(obj)
kind = SOURCE_KIND_FOR_SUFFIX[src.suffix]
override = cxx_override if kind == "cxx" else None
implicit = f" | {override[1]}" if override else ""
lines.append(f"build {escaped_obj}: {kind} {_e(src)}{implicit}")
if own_includes:
lines.append(f" own_includes = {own_includes}")
if edge_flags := override[0] if override else flags:
lines.append(f" flags = {edge_flags}")
# Escaped once here: the returned paths only ever appear in build
# statements (archive/link inputs), which use ninja escaping
objects.append(escaped_obj)
return objects
def library_edges(
lines: list[str], libraries: list[ArduinoLibrary]
) -> tuple[list[str], list[str]]:
"""Emit every library's compile and archive edges.
Returns the archive names and the objects that link directly. A
library's own include dirs lead its compile lines, as PlatformIO searched
them first: the include path is one global list, so another library's
header of the same name would shadow them.
"""
archives: list[str] = []
direct_objs: list[str] = []
for lib in libraries:
if not lib.sources:
# Header-only libraries are legitimate; the log makes an empty
# srcFilter or broken tree traceable before link errors do
_LOGGER.debug(
"Library %s has no source files; contributing includes only",
lib.name,
)
continue
objs = compile_edges(
lines,
lib.sources,
common_parent(lib.sources),
f"lib/{lib.name}",
flags=" ".join(_shell_token(f) for f in lib.flags),
own_includes=" ".join(f"-I{_q(d)}" for d in lib.include_dirs),
)
if not lib.lib_archive:
# libArchive: false / dot_a_linkage=false: hand the objects to
# the linker directly so unreferenced-but-required symbols
# (exception handlers, weak overrides) survive
direct_objs.extend(objs)
continue
archive = f"lib{lib.name}.a"
lines.append(f"build {_e(archive)}: ar {' '.join(objs)}")
archives.append(archive)
return archives, direct_objs
# One build flag: the flag, plus its argument when that is a separate token
Flag = tuple[str, ...]
# Flags whose path operand is the next token; gcc also takes it glued on
PATH_ARG_FLAGS = ("-include", "-imacros", "-isystem", "-iquote", "-idirafter")
# Flags whose path operand is glued on
PATH_PREFIXES = ("-I", "-L", *PATH_ARG_FLAGS)
def _anchor(path: str, base: Path) -> str:
if not path or Path(path).is_absolute():
return path
return str(base / path)
def anchor_path_flag(flag: Flag, base: Path) -> Flag:
"""Anchor a flag's relative path operand at ``base``.
PlatformIO ran the compiler from the build path; ninja runs it from
``.pioenvs/<name>``, where a relative operand would point elsewhere.
"""
name, *args = flag
if args:
if name in PATH_ARG_FLAGS:
return (name, _anchor(args[0], base))
return flag
for prefix in PATH_PREFIXES:
if name.startswith(prefix):
return (prefix + _anchor(name[len(prefix) :], base),)
return flag
-549
View File
@@ -1,549 +0,0 @@
"""Shared precompiled-header policy for the build backends.
The prefix either mirrors the TUs' own force-includes (ESP8266) or is a
curated core-header set (ESP-IDF). ``esphome: includes:`` sources receive
it too; Arduino.h visibility there is intended (esphome#8693).
"""
from __future__ import annotations
from collections.abc import Iterable
from contextlib import suppress
from dataclasses import dataclass
import hashlib
import json
import logging
import os
from pathlib import Path
import posixpath
import re
import stat
import subprocess
from esphome.build_helpers.ccache import effective_ccache_basedir, parse_enable_env
from esphome.build_helpers.idedata import (
CXX_SOURCE_SUFFIXES,
expand_response_files,
is_launcher,
split_command,
)
_DOMAIN = "pch"
@dataclass
class _PCHData:
emitted: bool = False
def _pch_data() -> _PCHData:
from esphome.core import CORE
if _DOMAIN not in CORE.data:
CORE.data[_DOMAIN] = _PCHData()
return CORE.data[_DOMAIN]
def mark_pch_emitted() -> None:
"""Record that this build's consumers reference the pch."""
_pch_data().emitted = True
_LOGGER = logging.getLogger(__name__)
# The header and its .gch/.sum sidecars live in the build directory.
PCH_HEADER_NAME = "esphome_pch.h"
# Every artifact the pch machinery can leave behind, for cleanup.
PCH_ARTIFACT_NAMES = (
PCH_HEADER_NAME,
f"{PCH_HEADER_NAME}.gch",
f"{PCH_HEADER_NAME}.gch.sum",
f"{PCH_HEADER_NAME}.gch.failed",
)
# The core defines header every backend anchors its prefix on.
PCH_CORE_HEADER = "esphome/core/defines.h"
# Prefix-header contents for backends that inject a curated set (rather
# than mirroring the TUs' own force-includes), defines.h first so USE_*
# macros exist for the rest. Deliberately hard-coded: frequency-derived
# sets measured no better and kept selecting headers that cannot compile
# standalone (X-macro, platform-variant). Every entry must be safe to
# include first in an empty TU. Caveat: application.h/automation.h become
# ambiently visible, so a TU missing those #includes still builds on such
# backends; ESPHOME_PCH_ENABLE=0 restores the strict view.
PCH_DEFAULT_HEADERS = (
PCH_CORE_HEADER,
"esphome/core/component.h",
"esphome/core/helpers.h",
"esphome/core/log.h",
"esphome/core/application.h",
"esphome/core/automation.h",
)
# ccache cannot hash through a .gch; CCACHE_PCH_EXTSUM makes it hash the
# .sum sidecar instead of the .gch bytes, which are not reproducible.
# Keep in sync with the literals in platformio/pch.py.script.
_CCACHE_PCH_ENV = {
"CCACHE_SLOPPINESS": "pch_defines,time_macros",
"CCACHE_PCH_EXTSUM": "true",
}
# Both include forms: an angle include resolving under src/ must enter the
# digest too; ones that do not resolve simply end the walk
# Compiler failures that clear on their own must not latch the .failed marker
_TRANSIENT_ERRORS = ("No space left", "Cannot allocate", "Resource temporarily")
_INCLUDE_RE = re.compile(rb'^\s*#\s*include\s+["<]([^">]+)[">]', re.MULTILINE)
def pch_enabled() -> bool:
"""Precompiled-header knob: default on, ``ESPHOME_PCH_ENABLE=0`` opts out."""
return parse_enable_env("ESPHOME_PCH_ENABLE") is not False
def pch_strict() -> bool:
"""CI knob: ``ESPHOME_PCH_STRICT=1`` turns pch degrade paths fatal.
A set-but-unrecognized value raises: a typo must not silently turn
the gate into a no-op that proves nothing.
"""
return parse_enable_env("ESPHOME_PCH_STRICT", strict=True) is True
def pch_degraded(reason: str) -> None:
"""Every degrade path funnels through here; strict mode raises."""
if pch_strict():
from esphome.core import EsphomeError
raise EsphomeError(f"ESPHOME_PCH_STRICT: {reason}")
def pch_disabled_degraded() -> None:
"""Strict CI must not read "no pch at all" as success."""
pch_degraded("pch disabled by ESPHOME_PCH_ENABLE")
def pch_probe_tail(source: str = "-") -> list[str]:
"""The syntax-only compile shared by the probe and its baseline."""
return ["-fsyntax-only", "-x", "c++", source]
def pch_probe_args(header: str, source: str = "-") -> list[str]:
"""Flags that load-check a built .gch via a syntax-only compile.
Rejection must be a nonzero exit (never just a wording match), so the
invalid-pch class is always escalated. ``source`` defaults to stdin
(host independent); the ninja probe edge passes a real file.
"""
return [
"-Winvalid-pch",
"-Werror=invalid-pch",
"-include",
header,
*pch_probe_tail(source),
]
def pch_consumer_escalation() -> str:
"""Consumer-side invalid-pch flag: strict reds the build on rejection
(per-process, so the probe alone cannot prove the consumers)."""
return "-Werror=invalid-pch" if pch_strict() else "-Wno-error=invalid-pch"
def ccache_pch_env() -> dict[str, str]:
"""Settings ccache needs to cache compiles that consume the .gch;
empty unless this build actually emitted one. User-set values win.
Native backends export these process-wide; only time_macros affects
non-pch TUs."""
if not (pch_enabled() and _pch_data().emitted):
return {}
extsum = os.environ.get("CCACHE_PCH_EXTSUM")
if extsum is not None and extsum.strip().lower() not in ("1", "true", "yes", "on"):
# ccache then hashes the non-reproducible .gch bytes: permanent misses
_LOGGER.warning("CCACHE_PCH_EXTSUM=%s disables pch caching", extsum)
env = {k: v for k, v in _CCACHE_PCH_ENV.items() if k not in os.environ}
user_sloppiness = os.environ.get("CCACHE_SLOPPINESS")
if user_sloppiness is not None and (
missing := [
t
for t in ("pch_defines", "time_macros")
if t not in {tok.strip() for tok in user_sloppiness.split(",")}
]
):
# Without these ccache declines every pch-consuming compile
env["CCACHE_SLOPPINESS"] = ",".join((user_sloppiness, *missing))
_LOGGER.warning(
"Adding %s to CCACHE_SLOPPINESS so ccache can cache compiles "
"that use the precompiled header",
",".join(missing),
)
return env
def pch_extra_scripts() -> list[str]:
"""The extra_scripts entries a PlatformIO platform registers for the
pch; empty when disabled (the script itself has no enable check)."""
if not pch_enabled():
pch_disabled_degraded()
return []
return ["post:pch.py"]
def pch_header_text(include_headers: Iterable[str]) -> str:
"""The prefix-header source: exactly these includes, in order."""
return "".join(f'#include "{name}"\n' for name in include_headers)
def _resolves(path: Path) -> bool:
"""False when missing; other stat failures propagate (identity unknown,
unlike is_file(), which would silently drop the header)."""
try:
return stat.S_ISREG(path.stat().st_mode)
except (FileNotFoundError, NotADirectoryError):
return False
def _include_closure(src_dir: Path, roots: Iterable[str]) -> dict[str, bytes]:
"""Include closure of ``roots``: src-relative name -> contents.
Resolution mirrors the compiler (includer's dir, then src root); names
outside ``src_dir`` end the walk and are versioned by the caller. No
#ifdef evaluation: over-approximating is the safe direction.
"""
seen: dict[str, bytes] = {}
stack: list[tuple[str, str]] = [(name, "") for name in roots]
while stack:
name, from_dir = stack.pop()
for candidate in (f"{from_dir}/{name}" if from_dir else name, name):
rel = posixpath.normpath(candidate)
if not rel.startswith("..") and _resolves(src_dir / rel):
break
else:
continue
if rel in seen:
continue
try:
data = (src_dir / rel).read_bytes()
except OSError as err:
# A marker would truncate the transitive walk; fail closed
_LOGGER.warning("Could not read %s for the pch checksum: %s", rel, err)
raise
seen[rel] = data
parent = posixpath.dirname(rel)
stack.extend(
# surrogateescape: a non-UTF-8 name just fails to resolve
(inc.decode(errors="surrogateescape"), parent)
for inc in _INCLUDE_RE.findall(data)
)
return seen
def pch_checksum(
src_dir: Path, include_headers: Iterable[str], extra: Iterable[str]
) -> str:
"""Digest standing in for the .gch in ccache's hash: the include closure
of the prefix header plus caller-supplied identity strings (versioned
install paths, flags). Raises OSError when a header's identity cannot
be established at all; callers must then compile without a pch."""
digest = hashlib.sha256()
closure = _include_closure(src_dir, include_headers)
for name in sorted(closure):
digest.update(name.encode(errors="surrogateescape"))
digest.update(closure[name])
digest.update(b"\0")
for item in extra:
digest.update(item.encode(errors="surrogateescape"))
digest.update(b"\0")
return digest.hexdigest()
# Tokens dropped when retargeting a TU's flags at the prefix header
# (the pch compile must not touch depfiles)
_PCH_STRIP_FLAGS_WITH_ARG = frozenset({"-o", "-c", "-MT", "-MF", "-MQ"})
_PCH_STRIP_FLAGS = frozenset({"-MD", "-MMD", "-MP", "-MM", "-M"})
def pch_compile_command(
build_dir: Path, header: Path, gch: Path
) -> tuple[list[str], Path] | None:
"""The exact src C++ flags from compile_commands.json retargeted at the
header, with the directory they resolve against (relative -I paths must
be expanded and executed from the same root); None (logged) when no
configured C++ TU is available yet."""
from esphome.core import CORE
try:
entries = json.loads(
(build_dir / "compile_commands.json").read_text(encoding="utf-8")
)
except (OSError, json.JSONDecodeError) as err:
# Configure already succeeded, so an unusable DB is a real anomaly
_LOGGER.warning("No usable compile database, skipping pch: %s", err)
return None
if not isinstance(entries, list):
_LOGGER.warning("Malformed compile database, skipping pch")
return None
# CMake may spell paths through a symlink differently than CORE does
# (macOS /tmp vs /private/tmp), so compare resolved paths
src_root = Path(CORE.relative_src_path()).resolve()
entry = next(
(
e
for e in entries
if isinstance(e, dict)
and isinstance(e.get("file"), str)
and e["file"].endswith(CXX_SOURCE_SUFFIXES)
and Path(e["file"]).resolve().is_relative_to(src_root)
),
None,
)
if entry is None:
_LOGGER.warning("No src C++ entry in the compile database, skipping pch")
return None
directory = entry.get("directory")
cmd_dir = Path(directory) if isinstance(directory, str) and directory else build_dir
command = entry.get("command")
tokens = expand_response_files(
split_command(command if isinstance(command, str) else ""), cmd_dir
)
# A DB recorded with ccache enabled prefixes the compiler with the
# launcher; the .gch must be compiled directly
if tokens and is_launcher(tokens[0]):
tokens = tokens[1:]
if not tokens:
# "arguments"-style or empty entries must skip, not spawn "-x ..."
_LOGGER.warning("Compile database entry has no usable command, skipping pch")
return None
args: list[str] = []
arg_it = iter(tokens)
for tok in arg_it:
if tok in _PCH_STRIP_FLAGS_WITH_ARG:
next(arg_it, None)
continue
if tok in _PCH_STRIP_FLAGS:
continue
if tok == "-include":
# Drop only the injected prefix; user force-includes must reach
# the .gch compile or GCC rejects it over the macro mismatch
inc = next(arg_it, "")
if not inc.endswith(PCH_HEADER_NAME):
args.extend(("-include", inc))
continue
args.append(tok)
return [*args, "-x", "c++-header", "-c", str(header), "-o", str(gch)], cmd_dir
def _log_pch_in_use() -> None:
# The only place a user can discover the knob; emitted only once a
# .gch is actually fresh or being built
_LOGGER.info(
"Compiling with a precompiled header (set ESPHOME_PCH_ENABLE=0 to disable)"
)
def _read_stamp(path: Path) -> str:
"""A corrupt sidecar must read as stale, not kill the pch forever."""
try:
return path.read_text(encoding="utf-8").strip()
except (OSError, UnicodeDecodeError):
return ""
def discard_pch(build_dir: Path) -> None:
"""Remove the pch sidecars so a stale .gch is never consumed.
Bumps the header only when a .gch was actually removed: TUs compiled
against it have incomplete depfiles, while a repeat failure with no
.gch must not force a full rebuild every build. A .gch that survives
an unlink failure would be consumed silently (wrong output, not a
slow build), so that raises.
"""
header = build_dir / PCH_HEADER_NAME
gch = Path(f"{header}.gch")
had_gch = gch.is_file()
errors = []
for sidecar in (gch, Path(f"{gch}.sum")):
try:
sidecar.unlink(missing_ok=True)
except OSError as err:
if sidecar.is_file():
from esphome.core import EsphomeError
raise EsphomeError(
f"Could not discard the stale precompiled header: {err}"
) from err
errors.append(err)
for err in errors:
_LOGGER.warning("Could not discard the pch sidecars: %s", err)
if had_gch and header.is_file():
with suppress(OSError):
os.utime(header)
def prepare_pch(
build_dir: Path, include_headers: tuple[str, ...], extra: Iterable[str]
) -> None:
"""Compile ``build_dir``'s .gch from compile_commands.json flags and
write its ccache .sum.
The .sum doubles as the freshness stamp and folds in the compile
command, so a flag-only change rebuilds the .gch; ``extra`` carries
backend identity (framework version, sdkconfig, ...). A failed
compile falls back to the plain header include.
"""
from esphome.core import CORE
header = build_dir / PCH_HEADER_NAME
gch = Path(f"{header}.gch")
sum_path = Path(f"{gch}.sum")
cmd_and_dir = pch_compile_command(build_dir, header, gch)
if cmd_and_dir is None:
# Freshness cannot be validated; a leftover .gch must not be consumed
discard_pch(build_dir)
pch_degraded("no usable compile command")
return
cmd, cmd_dir = cmd_and_dir
# Strip like ccache's rewriting (user CCACHE_BASEDIR wins); the raw
# build path covers unresolved (symlinked) spellings
cmd_id = (
" ".join(cmd)
.replace(effective_ccache_basedir(), "")
.replace(str(CORE.build_path), "")
)
try:
checksum = pch_checksum(
CORE.relative_src_path(),
include_headers,
(
# The closure is sorted, so root order only enters via the text
pch_header_text(include_headers),
*extra,
cmd_id,
),
)
except (OSError, UnicodeError) as err:
# Identity unknown: a stale cache entry must never be served
_LOGGER.warning(
"Could not establish the pch identity; compiling without it: %s", err
)
discard_pch(build_dir)
pch_degraded(f"identity unknown: {err}")
return
failed_marker = Path(f"{gch}.failed")
def _run(
run_cmd: list[str], what: str, stdin: str | None = None
) -> subprocess.CompletedProcess | None:
"""Spawn one pch tool step; environmental failures discard and
degrade (None): spawn/IO/timeout errors and signal kills never
latch the marker."""
try:
proc = subprocess.run(
run_cmd,
cwd=cmd_dir,
# C locale keeps diagnostics matchable by _TRANSIENT_ERRORS
env={**os.environ, "LC_ALL": "C"},
input=stdin,
capture_output=True,
text=True,
check=False,
timeout=300,
)
except (OSError, subprocess.SubprocessError) as err:
_LOGGER.warning("Precompiled header %s did not run: %s", what, err)
discard_pch(build_dir)
pch_degraded(f"{what} did not run: {err}")
return None
if proc.returncode < 0:
# Killed by a signal (OOM, ^C): environmental, do not latch
_LOGGER.warning(
"Precompiled header %s was killed (signal %d); retrying next build",
what,
-proc.returncode,
)
discard_pch(build_dir)
pch_degraded(f"{what} killed by signal {-proc.returncode}")
return None
return proc
def _fail(error: str, reason: str, latch: bool) -> None:
"""Discard and degrade; deterministic failures latch when asked."""
_LOGGER.warning(
"Precompiled header failed; compiling without it: %s", error[:400]
)
# Latching paths keep the full compiler output recoverable
_LOGGER.debug("Full pch output: %s", error)
discard_pch(build_dir)
if latch and not any(m in error for m in _TRANSIENT_ERRORS):
# Skip retries until a header/flag/backend-identity/command change
failed_marker.write_text(checksum + "\n", encoding="utf-8")
os.utime(header)
pch_degraded(f"{reason}: {error[:200]}")
def _probe(latch: bool = True) -> None:
"""Load-check the built .gch: some toolchains build one they then
refuse to load (per-process ASLR). Dep flags are already stripped
from cmd, so no -MF is needed; cmd ends with the fixed
"-x c++-header -c -o" tail. A cached-header rejection may not
reproduce (per-process), so that caller passes latch=False."""
if cmd[-6:-4] != ["-x", "c++-header"]:
# The slice below depends on pch_compile_command's fixed tail
_LOGGER.warning("Unexpected pch command shape: %s", cmd[-6:])
discard_pch(build_dir)
pch_degraded("unexpected pch command shape")
return
base = cmd[:-6]
probe = _run([*base, *pch_probe_args(str(header))], "probe", stdin="")
if probe is None:
return
if probe.returncode != 0:
# Disambiguate: only blame the pch when the same compile passes
# without it; a failing baseline is its own (latchable) problem
baseline = _run([*base, *pch_probe_tail()], "probe baseline", stdin="")
if baseline is None:
return
if baseline.returncode == 0:
error = probe.stderr.strip() or f"exit code {probe.returncode}"
_fail(error, "toolchain cannot load the pch", latch=latch)
else:
error = baseline.stderr.strip() or f"exit code {baseline.returncode}"
_fail(error, "probe cannot run at all", latch=latch)
if gch.is_file() and _read_stamp(sum_path) == checksum:
_log_pch_in_use()
if pch_strict():
# Rejection is per-process, so a cached .gch must re-prove
# loadability for the strict gate (CI-only cost); no latch,
# since the rejection may not reproduce either
_probe(latch=False)
return
if _read_stamp(failed_marker) == checksum:
_LOGGER.info(
"Precompiled header disabled after an earlier failure; delete %s to retry",
failed_marker,
)
pch_degraded("earlier failure latched")
return
_log_pch_in_use()
result = _run(cmd, "compile")
if result is None:
return
error = None
if result.returncode != 0:
error = result.stderr.strip() or f"exit code {result.returncode}"
elif not gch.is_file():
error = "compiler produced no .gch"
if error is not None:
_fail(error, "compile failed", latch=True)
return
_probe()
if not gch.is_file():
# The probe discarded a rejected or unrunnable .gch
return
failed_marker.unlink(missing_ok=True)
sum_path.write_text(checksum + "\n", encoding="utf-8")
# Consumers depend on the header (depfiles cannot see through a .gch);
# bump it so users of the previous .gch recompile
os.utime(header)
+1 -8
View File
@@ -33,11 +33,4 @@ def tools_cache_path(env_var: str, subdir: str) -> Path:
IDF_TOOLS_CACHE = ("ESPHOME_ESP_IDF_PREFIX", "idf")
SDK_NRF_TOOLS_CACHE = ("ESPHOME_SDK_NRF_PREFIX", "sdk-nrf")
ARDUINO8266_TOOLS_CACHE = ("ESPHOME_ARDUINO8266_PREFIX", "arduino8266")
# The host backend installs nothing; the entry only holds its ccache dir
HOST_TOOLS_CACHE = ("ESPHOME_HOST_PREFIX", "host")
TOOLS_CACHE_SPECS = (
IDF_TOOLS_CACHE,
SDK_NRF_TOOLS_CACHE,
ARDUINO8266_TOOLS_CACHE,
HOST_TOOLS_CACHE,
)
TOOLS_CACHE_SPECS = (IDF_TOOLS_CACHE, SDK_NRF_TOOLS_CACHE, ARDUINO8266_TOOLS_CACHE)
-1
View File
@@ -42,7 +42,6 @@ from esphome.cpp_generator import ( # noqa: F401
set_cpp_standard,
statement,
static_const_array,
static_function,
templatable,
variable,
with_local_variable,
@@ -85,11 +85,20 @@ async def wave_base_to_code(var: MockObj, config: ConfigType) -> None:
await ble_client.register_ble_node(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_HUMIDITY, var.set_humidity)
await sensors(CONF_TEMPERATURE, var.set_temperature)
await sensors(CONF_PRESSURE, var.set_pressure)
await sensors(CONF_TVOC, var.set_tvoc)
await sensors(CONF_BATTERY_VOLTAGE, var.set_battery_voltage)
if config_humidity := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(config_humidity)
cg.add(var.set_humidity(sens))
if config_temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(config_temperature)
cg.add(var.set_temperature(sens))
if config_pressure := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(config_pressure)
cg.add(var.set_pressure(sens))
if config_tvoc := config.get(CONF_TVOC):
sens = await sensor.new_sensor(config_tvoc)
cg.add(var.set_tvoc(sens))
if config_battery_voltage := config.get(CONF_BATTERY_VOLTAGE):
sens = await sensor.new_sensor(config_battery_voltage)
cg.add(var.set_battery_voltage(sens))
if config_battery_update_interval := config.get(CONF_BATTERY_UPDATE_INTERVAL):
cg.add(var.set_battery_update_interval(config_battery_update_interval))
@@ -87,9 +87,16 @@ async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await airthings_wave_base.wave_base_to_code(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_RADON, var.set_radon)
await sensors(CONF_RADON_LONG_TERM, var.set_radon_long_term)
await sensors(CONF_CO2, var.set_co2)
await sensors(CONF_ILLUMINANCE, var.set_illuminance)
if config_radon := config.get(CONF_RADON):
sens = await sensor.new_sensor(config_radon)
cg.add(var.set_radon(sens))
if config_radon_long_term := config.get(CONF_RADON_LONG_TERM):
sens = await sensor.new_sensor(config_radon_long_term)
cg.add(var.set_radon_long_term(sens))
if config_co2 := config.get(CONF_CO2):
sens = await sensor.new_sensor(config_co2)
cg.add(var.set_co2(sens))
if config_illuminance := config.get(CONF_ILLUMINANCE):
sens = await sensor.new_sensor(config_illuminance)
cg.add(var.set_illuminance(sens))
cg.add(var.set_device_type(config[CONF_DEVICE_TYPE]))
+15 -6
View File
@@ -85,9 +85,18 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature)
await sensors(CONF_HUMIDITY, var.set_humidity)
await sensors(CONF_BATTERY_LEVEL, var.set_battery_level)
await sensors(CONF_BATTERY_VOLTAGE, var.set_battery_voltage)
await sensors(CONF_SIGNAL_STRENGTH, var.set_signal_strength)
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))
if battery_level_config := config.get(CONF_BATTERY_LEVEL):
sens = await sensor.new_sensor(battery_level_config)
cg.add(var.set_battery_level(sens))
if battery_voltage_config := config.get(CONF_BATTERY_VOLTAGE):
sens = await sensor.new_sensor(battery_voltage_config)
cg.add(var.set_battery_voltage(sens))
if signal_strength_config := config.get(CONF_SIGNAL_STRENGTH):
sens = await sensor.new_sensor(signal_strength_config)
cg.add(var.set_signal_strength(sens))
+24 -9
View File
@@ -129,15 +129,30 @@ 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)
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
await sensors(CONF_CURRENT, var.set_current_sensor)
await sensors(CONF_POWER, var.set_power_sensor)
await sensors(CONF_REACTIVE_POWER, var.set_reactive_power_sensor)
await sensors(CONF_POWER_FACTOR, var.set_power_factor_sensor)
await sensors(CONF_FORWARD_ACTIVE_ENERGY, var.set_forward_active_energy_sensor)
await sensors(CONF_REVERSE_ACTIVE_ENERGY, var.set_reverse_active_energy_sensor)
await sensors(CONF_FREQUENCY, var.set_freq_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))
if reactive_power_config := config.get(CONF_REACTIVE_POWER):
sens = await sensor.new_sensor(reactive_power_config)
cg.add(var.set_reactive_power_sensor(sens))
if power_factor_config := config.get(CONF_POWER_FACTOR):
sens = await sensor.new_sensor(power_factor_config)
cg.add(var.set_power_factor_sensor(sens))
if forward_active_energy_config := config.get(CONF_FORWARD_ACTIVE_ENERGY):
sens = await sensor.new_sensor(forward_active_energy_config)
cg.add(var.set_forward_active_energy_sensor(sens))
if reverse_active_energy_config := config.get(CONF_REVERSE_ACTIVE_ENERGY):
sens = await sensor.new_sensor(reverse_active_energy_config)
cg.add(var.set_reverse_active_energy_sensor(sens))
if frequency_config := config.get(CONF_FREQUENCY):
sens = await sensor.new_sensor(frequency_config)
cg.add(var.set_freq_sensor(sens))
cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY]))
cg.add(var.set_meter_constant(config[CONF_METER_CONSTANT]))
cg.add(var.set_pl_const(config[CONF_PL_CONST]))
+2 -11
View File
@@ -61,16 +61,14 @@ from esphome.const import (
DEVICE_CLASS_VIBRATION,
DEVICE_CLASS_WINDOW,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
SubEntities,
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
setup_entity,
)
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType
from esphome.cpp_generator import MockObjClass
from esphome.util import Registry
CODEOWNERS = ["@esphome/core"]
@@ -634,13 +632,6 @@ async def new_binary_sensor(config, *args):
return var
def sub_binary_sensors(
config: ConfigType, *, parent: MockObj | ID | None = None
) -> SubEntities:
"""Return a SubEntities bound to new_binary_sensor."""
return SubEntities(new_binary_sensor, config, parent)
BINARY_SENSOR_CONDITION_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(BinarySensor),
+16 -6
View File
@@ -150,10 +150,15 @@ 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_FREQUENCY, var.set_frequency_sensor)
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
if frequency_config := config.get(CONF_FREQUENCY):
sens = await sensor.new_sensor(frequency_config)
cg.add(var.set_frequency_sensor(sens))
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
if voltage_config := config.get(CONF_VOLTAGE):
sens = await sensor.new_sensor(voltage_config)
cg.add(var.set_voltage_sensor(sens))
for i in range(6):
if channel_config := config.get(f"{CONF_CHANNEL}_{i + 1}"):
@@ -167,5 +172,10 @@ async def to_code(config: ConfigType) -> None:
sens = await sensor.new_sensor(energy_config)
cg.add(getattr(var, f"set_energy_{i + 1}_sensor")(sens))
await sensors(CONF_TOTAL_POWER, var.set_total_power_sensor)
await sensors(CONF_TOTAL_ENERGY, var.set_total_energy_sensor)
if total_power_config := config.get(CONF_TOTAL_POWER):
sens = await sensor.new_sensor(total_power_config)
cg.add(var.set_total_power_sensor(sens))
if total_energy_config := config.get(CONF_TOTAL_ENERGY):
sens = await sensor.new_sensor(total_energy_config)
cg.add(var.set_total_energy_sensor(sens))
+24 -9
View File
@@ -94,12 +94,27 @@ 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_VOLTAGE, var.set_voltage_sensor)
await sensors(CONF_CURRENT_1, var.set_current_sensor_1)
await sensors(CONF_CURRENT_2, var.set_current_sensor_2)
await sensors(CONF_ACTIVE_POWER_1, var.set_power_sensor_1)
await sensors(CONF_ACTIVE_POWER_2, var.set_power_sensor_2)
await sensors(CONF_ENERGY_1, var.set_energy_sensor_1)
await sensors(CONF_ENERGY_2, var.set_energy_sensor_2)
await sensors(CONF_ENERGY_TOTAL, var.set_energy_sensor_sum)
if voltage_config := config.get(CONF_VOLTAGE):
sens = await sensor.new_sensor(voltage_config)
cg.add(var.set_voltage_sensor(sens))
if current_1_config := config.get(CONF_CURRENT_1):
sens = await sensor.new_sensor(current_1_config)
cg.add(var.set_current_sensor_1(sens))
if current_2_config := config.get(CONF_CURRENT_2):
sens = await sensor.new_sensor(current_2_config)
cg.add(var.set_current_sensor_2(sens))
if active_power_1_config := config.get(CONF_ACTIVE_POWER_1):
sens = await sensor.new_sensor(active_power_1_config)
cg.add(var.set_power_sensor_1(sens))
if active_power_2_config := config.get(CONF_ACTIVE_POWER_2):
sens = await sensor.new_sensor(active_power_2_config)
cg.add(var.set_power_sensor_2(sens))
if energy_1_config := config.get(CONF_ENERGY_1):
sens = await sensor.new_sensor(energy_1_config)
cg.add(var.set_energy_sensor_1(sens))
if energy_2_config := config.get(CONF_ENERGY_2):
sens = await sensor.new_sensor(energy_2_config)
cg.add(var.set_energy_sensor_2(sens))
if energy_total_config := config.get(CONF_ENERGY_TOTAL):
sens = await sensor.new_sensor(energy_total_config)
cg.add(var.set_energy_sensor_sum(sens))
+18 -7
View File
@@ -231,13 +231,24 @@ 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_VOLTAGE, var.set_voltage_sensor)
await sensors(CONF_CURRENT, var.set_current_sensor)
await sensors(CONF_POWER, var.set_power_sensor)
await sensors(CONF_ENERGY, var.set_energy_sensor)
await sensors(CONF_INTERNAL_TEMPERATURE, var.set_internal_temperature_sensor)
await sensors(CONF_EXTERNAL_TEMPERATURE, var.set_external_temperature_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))
if energy_config := config.get(CONF_ENERGY):
sens = await sensor.new_sensor(energy_config)
cg.add(var.set_energy_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))
if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE):
sens = await sensor.new_sensor(external_temperature_config)
cg.add(var.set_external_temperature_sensor(sens))
# enable legacy mode
cg.add(var.set_legacy_mode(config.get(CONF_LEGACY_MODE)))
+15 -6
View File
@@ -101,12 +101,21 @@ 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_VOLTAGE, var.set_voltage_sensor)
await sensors(CONF_CURRENT, var.set_current_sensor)
await sensors(CONF_POWER, var.set_power_sensor)
await sensors(CONF_ENERGY, var.set_energy_sensor)
await sensors(CONF_FREQUENCY, var.set_frequency_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))
if energy_config := config.get(CONF_ENERGY):
sens = await sensor.new_sensor(energy_config)
cg.add(var.set_energy_sensor(sens))
if frequency_config := config.get(CONF_FREQUENCY):
sens = await sensor.new_sensor(frequency_config)
cg.add(var.set_frequency_sensor(sens))
cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY]))
cg.add(var.set_address(config[CONF_ADDRESS]))
cg.add(var.set_reset(config[CONF_RESET]))
-2
View File
@@ -7,7 +7,6 @@ from esphome.components.uart import (
uart_ns,
)
from esphome.components.zephyr import zephyr_add_prj_conf
from esphome.components.zephyr_ble_server import request_ble_l2cap_mtu
import esphome.config_validation as cv
from esphome.const import (
CONF_DEBUG,
@@ -62,7 +61,6 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
zephyr_add_prj_conf("BT_NUS", True)
request_ble_l2cap_mtu(247) # DLE sweet spot: one NUS frame per LL PDU
expose_log = config[CONF_TYPE] == CONF_LOGS
cg.add(var.set_expose_log(expose_log))
if expose_log:
+14 -8
View File
@@ -90,14 +90,20 @@ 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):
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]))
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]))
cg.add(var.set_iir_filter(config[CONF_IIR_FILTER]))
@@ -72,9 +72,18 @@ async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await setup_bthome_mithermometer(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature)
await sensors(CONF_HUMIDITY, var.set_humidity)
await sensors(CONF_BATTERY_LEVEL, var.set_battery_level)
await sensors(CONF_BATTERY_VOLTAGE, var.set_battery_voltage)
await sensors(CONF_SIGNAL_STRENGTH, var.set_signal_strength)
if temp_sens := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temp_sens)
cg.add(var.set_temperature(sens))
if humi_sens := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humi_sens)
cg.add(var.set_humidity(sens))
if batl_sens := config.get(CONF_BATTERY_LEVEL):
sens = await sensor.new_sensor(batl_sens)
cg.add(var.set_battery_level(sens))
if batv_sens := config.get(CONF_BATTERY_VOLTAGE):
sens = await sensor.new_sensor(batv_sens)
cg.add(var.set_battery_voltage(sens))
if sgnl_sens := config.get(CONF_SIGNAL_STRENGTH):
sens = await sensor.new_sensor(sgnl_sens)
cg.add(var.set_signal_strength(sens))
+1 -9
View File
@@ -16,9 +16,8 @@ from esphome.const import (
DEVICE_CLASS_RESTART,
DEVICE_CLASS_UPDATE,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
SubEntities,
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
@@ -115,13 +114,6 @@ async def new_button(config: ConfigType, *args: SafeExpType) -> MockObj:
return var
def sub_buttons(
config: ConfigType, *, parent: MockObj | ID | None = None
) -> SubEntities:
"""Return a SubEntities bound to new_button."""
return SubEntities(new_button, config, parent)
BUTTON_PRESS_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(Button),
-1
View File
@@ -15,7 +15,6 @@ CONF_CLIMATE_ID = "climate_id"
CONF_CO2_EQUIVALENT = "co2_equivalent"
CONF_COLOR_DEPTH = "color_depth"
CONF_COLUMNS = "columns"
CONF_CONVERSION_RATE = "conversion_rate"
CONF_CRC_ENABLE = "crc_enable"
CONF_DATA_BITS = "data_bits"
CONF_DESCRIPTION = "description"
-1
View File
@@ -1 +0,0 @@
CODEOWNERS = ["@clydebarrow"]
@@ -1,28 +0,0 @@
#include "counter_sensor.h"
#include "esphome/core/log.h"
namespace esphome::counter {
static const char *const TAG = "counter";
void CounterSensor::setup() {
if (this->restore_) {
this->pref_ = this->make_entity_preference<int64_t>();
this->pref_.load(&this->value_);
}
this->publish_state(static_cast<float>(this->value_));
}
void CounterSensor::set_value(int64_t value) {
this->value_ = value;
this->publish_state(static_cast<float>(value));
if (this->restore_)
this->pref_.save(&this->value_);
}
void CounterSensor::dump_config() {
LOG_SENSOR("", "Counter", this);
ESP_LOGCONFIG(TAG, " Restore: %s", YESNO(this->restore_));
}
} // namespace esphome::counter
@@ -1,47 +0,0 @@
#pragma once
#include <cstdint>
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome::counter {
/// A counter held as a 64-bit integer. The published sensor state is a float, so it is exact only up to 2^24.
class CounterSensor final : public sensor::Sensor, public Component {
public:
/// The counter starts at initial_value unless a stored value is restored.
CounterSensor(bool restore, int64_t initial_value) : value_(initial_value), restore_(restore) {}
void setup() override;
void dump_config() override;
// restore value before `on_boot` automations run
float get_setup_priority() const override { return setup_priority::DATA + 50.0f; }
/// Increment by one each time the given sensor publishes a state.
void count_updates_from(sensor::Sensor *source) {
source->add_on_state_callback([this](float) { this->increment(); });
}
/// Increment by one each time the given binary sensor changes to true.
template<typename T> void count_true_from(T *source) {
source->add_on_state_callback([this](bool state) {
if (state)
this->increment();
});
}
void set_value(int64_t value);
/// overflow on addition of signed numbers is undefined - use the well defined unsigned version
void increment(int64_t amount = 1) {
this->set_value(static_cast<int64_t>(static_cast<uint64_t>(this->value_) + static_cast<uint64_t>(amount)));
}
protected:
ESPPreferenceObject pref_;
int64_t value_;
bool restore_;
};
} // namespace esphome::counter
-85
View File
@@ -1,85 +0,0 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import binary_sensor, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_BINARY_SENSOR,
CONF_ID,
CONF_INITIAL_VALUE,
CONF_RESTORE,
CONF_SENSOR,
CONF_VALUE,
ICON_COUNTER,
)
from esphome.types import ConfigType
counter_ns = cg.esphome_ns.namespace("counter")
CounterSensor = counter_ns.class_("CounterSensor", sensor.Sensor, cg.Component)
# The lowest value is left out because its C++ literal cannot be written portably.
INT64_MAX = 2**63 - 1
COUNTER_RANGE = cv.int_range(min=-INT64_MAX, max=INT64_MAX)
COUNTER_VALUE = cv.templatable(COUNTER_RANGE)
def _not_own_source(config: ConfigType) -> ConfigType:
if (source := config.get(CONF_SENSOR)) is not None and source.id == config[
CONF_ID
].id:
raise cv.Invalid("A counter cannot count its own updates", [CONF_SENSOR])
return config
CONFIG_SCHEMA = cv.All(
sensor.sensor_schema(
CounterSensor,
icon=ICON_COUNTER,
accuracy_decimals=0,
)
.extend(
{
cv.Optional(CONF_RESTORE, default=True): cv.boolean,
cv.Optional(CONF_INITIAL_VALUE, default=0): COUNTER_RANGE,
cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
cv.Optional(CONF_BINARY_SENSOR): cv.use_id(binary_sensor.BinarySensor),
}
)
.extend(cv.COMPONENT_SCHEMA),
_not_own_source,
)
async def to_code(config):
var = cg.new_Pvariable(
config[CONF_ID], config[CONF_RESTORE], config[CONF_INITIAL_VALUE]
)
await cg.register_component(var, config)
await sensor.register_sensor(var, config)
if (source := config.get(CONF_SENSOR)) is not None:
cg.add(var.count_updates_from(await cg.get_variable(source)))
if (source := config.get(CONF_BINARY_SENSOR)) is not None:
cg.add(var.count_true_from(await cg.get_variable(source)))
automation.register_apply_action(
"counter.set_value",
cv.maybe_simple_value(
{
cv.GenerateID(CONF_ID): cv.use_id(CounterSensor),
cv.Required(CONF_VALUE): COUNTER_VALUE,
},
key=CONF_VALUE,
),
automation.ApplyField(CONF_VALUE, "set_value", cg.int64),
)
automation.register_apply_action(
"counter.increment",
automation.maybe_simple_id(
{
cv.GenerateID(CONF_ID): cv.use_id(CounterSensor),
cv.Optional(CONF_VALUE, default=1): COUNTER_VALUE,
}
),
automation.ApplyField(CONF_VALUE, "increment", cg.int64),
)
+21 -8
View File
@@ -98,11 +98,24 @@ 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_VOLTAGE, var.set_voltage_sensor)
await sensors(CONF_CURRENT, var.set_current_sensor)
await sensors(CONF_POWER, var.set_power_sensor)
await sensors(CONF_ENERGY, var.set_energy_sensor)
await sensors(CONF_APPARENT_POWER, var.set_apparent_power_sensor)
await sensors(CONF_REACTIVE_POWER, var.set_reactive_power_sensor)
await sensors(CONF_POWER_FACTOR, var.set_power_factor_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))
if energy_config := config.get(CONF_ENERGY):
sens = await sensor.new_sensor(energy_config)
cg.add(var.set_energy_sensor(sens))
if apparent_power_config := config.get(CONF_APPARENT_POWER):
sens = await sensor.new_sensor(apparent_power_config)
cg.add(var.set_apparent_power_sensor(sens))
if reactive_power_config := config.get(CONF_REACTIVE_POWER):
sens = await sensor.new_sensor(reactive_power_config)
cg.add(var.set_reactive_power_sensor(sens))
if power_factor_config := config.get(CONF_POWER_FACTOR):
sens = await sensor.new_sensor(power_factor_config)
cg.add(var.set_power_factor_sensor(sens))
+1 -3
View File
@@ -1,5 +1,4 @@
import esphome.codegen as cg
from esphome.components.nrf52.framework import include_west_project
from esphome.components.zephyr import zephyr_add_prj_conf
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
@@ -52,8 +51,7 @@ async def to_code(config: ConfigType) -> None:
zephyr_add_prj_conf("HWINFO", True)
# gdb thread support
zephyr_add_prj_conf("DEBUG_THREAD_INFO", True)
# RTT sources live in the segger project
include_west_project("segger")
# RTT
zephyr_add_prj_conf("USE_SEGGER_RTT", True)
zephyr_add_prj_conf("RTT_CONSOLE", True)
zephyr_add_prj_conf("LOG", True)
+28 -9
View File
@@ -106,13 +106,32 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_DEBUG_ID])
debug_component = await cg.get_variable(config[CONF_DEBUG_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_FREE, hub.set_free_sensor)
await sensors(CONF_BLOCK, hub.set_block_sensor)
await sensors(CONF_FRAGMENTATION, hub.set_fragmentation_sensor)
await sensors(CONF_MIN_FREE, hub.set_min_free_sensor)
await sensors(CONF_LOOP_TIME, hub.set_loop_time_sensor)
await sensors(CONF_PSRAM, hub.set_psram_sensor)
await sensors(CONF_CPU_FREQUENCY, hub.set_cpu_frequency_sensor)
if free_conf := config.get(CONF_FREE):
sens = await sensor.new_sensor(free_conf)
cg.add(debug_component.set_free_sensor(sens))
if block_conf := config.get(CONF_BLOCK):
sens = await sensor.new_sensor(block_conf)
cg.add(debug_component.set_block_sensor(sens))
if fragmentation_conf := config.get(CONF_FRAGMENTATION):
sens = await sensor.new_sensor(fragmentation_conf)
cg.add(debug_component.set_fragmentation_sensor(sens))
if min_free_conf := config.get(CONF_MIN_FREE):
sens = await sensor.new_sensor(min_free_conf)
cg.add(debug_component.set_min_free_sensor(sens))
if loop_time_conf := config.get(CONF_LOOP_TIME):
sens = await sensor.new_sensor(loop_time_conf)
cg.add(debug_component.set_loop_time_sensor(sens))
if psram_conf := config.get(CONF_PSRAM):
sens = await sensor.new_sensor(psram_conf)
cg.add(debug_component.set_psram_sensor(sens))
if cpu_freq_conf := config.get(CONF_CPU_FREQUENCY):
sens = await sensor.new_sensor(cpu_freq_conf)
cg.add(debug_component.set_cpu_frequency_sensor(sens))
+7 -4
View File
@@ -31,8 +31,11 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_DEBUG_ID])
debug_component = await cg.get_variable(config[CONF_DEBUG_ID])
text_sensors = text_sensor.sub_text_sensors(config)
await text_sensors(CONF_DEVICE, hub.set_device_info_sensor)
await text_sensors(CONF_RESET_REASON, hub.set_reset_reason_sensor)
if CONF_DEVICE in config:
sens = await text_sensor.new_text_sensor(config[CONF_DEVICE])
cg.add(debug_component.set_device_info_sensor(sens))
if CONF_RESET_REASON in config:
sens = await text_sensor.new_text_sensor(config[CONF_RESET_REASON])
cg.add(debug_component.set_reset_reason_sensor(sens))
+1 -1
View File
@@ -1,6 +1,5 @@
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.const import CONF_CONVERSION_RATE
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INVERTED, CONF_RESOLUTION
from esphome.types import ConfigType
@@ -14,6 +13,7 @@ MULTI_CONF = True
CONF_PWM = "pwm"
CONF_DIVIDER = "divider"
CONF_DAC = "dac"
CONF_CONVERSION_RATE = "conversion_rate"
CONF_EMC2101_ID = "emc2101_id"
+1 -4
View File
@@ -6,7 +6,6 @@ import subprocess
from typing import Any
from esphome.build_helpers.native import native_backend
from esphome.build_helpers.pch import pch_extra_scripts
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
@@ -35,7 +34,7 @@ from esphome.core import (
)
from esphome.core.config import BOARD_MAX_LENGTH
from esphome.helpers import IS_MACOS, copy_file_if_changed
from esphome.platformio.toolchain import copy_ccache_script, copy_pch_script
from esphome.platformio.toolchain import copy_ccache_script
from esphome.storage_json import StorageJSON
from esphome.types import ConfigType
@@ -381,7 +380,6 @@ async def to_code(config: ConfigType) -> None:
]
if not enable_scanf_float:
extra_scripts.append("pre:remove_float_scanf.py")
extra_scripts.extend(pch_extra_scripts())
extra_scripts.append("post:post_build.py")
cg.add_platformio_option("extra_scripts", extra_scripts)
@@ -538,7 +536,6 @@ def copy_files() -> None:
CORE.relative_build_path(f"{script}.py"),
)
copy_ccache_script()
copy_pch_script()
# ESP logs stack trace decoder, based on https://github.com/me-no-dev/EspExceptionDecoder
+8 -4
View File
@@ -31,8 +31,12 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config):
hub = await cg.get_variable(config[CONF_ID])
parent = await cg.get_variable(config[CONF_ID])
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(CONF_PUMP_STATE, hub.set_is_dosing)
await binary_sensors(CONF_IS_PAUSED, hub.set_is_paused)
if CONF_PUMP_STATE in config:
sens = await binary_sensor.new_binary_sensor(config[CONF_PUMP_STATE])
cg.add(parent.set_is_dosing(sens))
if CONF_IS_PAUSED in config:
sens = await binary_sensor.new_binary_sensor(config[CONF_IS_PAUSED])
cg.add(parent.set_is_paused(sens))
+24 -8
View File
@@ -76,12 +76,28 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config):
hub = await cg.get_variable(config[CONF_ID])
parent = await cg.get_variable(config[CONF_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_CURRENT_VOLUME_DOSED, hub.set_current_volume_dosed)
await sensors(CONF_LAST_VOLUME_REQUESTED, hub.set_last_volume_requested)
await sensors(CONF_TOTAL_VOLUME_DOSED, hub.set_total_volume_dosed)
await sensors(CONF_ABSOLUTE_TOTAL_VOLUME_DOSED, hub.set_absolute_total_volume_dosed)
await sensors(CONF_PUMP_VOLTAGE, hub.set_pump_voltage)
await sensors(CONF_MAX_FLOW_RATE, hub.set_max_flow_rate)
if CONF_CURRENT_VOLUME_DOSED in config:
sens = await sensor.new_sensor(config[CONF_CURRENT_VOLUME_DOSED])
cg.add(parent.set_current_volume_dosed(sens))
if CONF_LAST_VOLUME_REQUESTED in config:
sens = await sensor.new_sensor(config[CONF_LAST_VOLUME_REQUESTED])
cg.add(parent.set_last_volume_requested(sens))
if CONF_TOTAL_VOLUME_DOSED in config:
sens = await sensor.new_sensor(config[CONF_TOTAL_VOLUME_DOSED])
cg.add(parent.set_total_volume_dosed(sens))
if CONF_ABSOLUTE_TOTAL_VOLUME_DOSED in config:
sens = await sensor.new_sensor(config[CONF_ABSOLUTE_TOTAL_VOLUME_DOSED])
cg.add(parent.set_absolute_total_volume_dosed(sens))
if CONF_PUMP_VOLTAGE in config:
sens = await sensor.new_sensor(config[CONF_PUMP_VOLTAGE])
cg.add(parent.set_pump_voltage(sens))
if CONF_MAX_FLOW_RATE in config:
sens = await sensor.new_sensor(config[CONF_MAX_FLOW_RATE])
cg.add(parent.set_max_flow_rate(sens))
+8 -4
View File
@@ -24,8 +24,12 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config):
hub = await cg.get_variable(config[CONF_ID])
parent = await cg.get_variable(config[CONF_ID])
text_sensors = text_sensor.sub_text_sensors(config)
await text_sensors(CONF_DOSING_MODE, hub.set_dosing_mode)
await text_sensors(CONF_CALIBRATION_STATUS, hub.set_calibration_status)
if CONF_DOSING_MODE in config:
sens = await text_sensor.new_text_sensor(config[CONF_DOSING_MODE])
cg.add(parent.set_dosing_mode(sens))
if CONF_CALIBRATION_STATUS in config:
sens = await text_sensor.new_text_sensor(config[CONF_CALIBRATION_STATUS])
cg.add(parent.set_calibration_status(sens))
+27 -8
View File
@@ -177,14 +177,33 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_protocol_version(config[CONF_PROTOCOL_VERSION]))
sensors = sensor.sub_sensors(config)
await sensors(CONF_INVERTER_STATUS, var.set_inverter_status_sensor)
await sensors(CONF_FREQUENCY, var.set_grid_frequency_sensor)
await sensors(CONF_ACTIVE_POWER, var.set_grid_active_power_sensor)
await sensors(CONF_PV_ACTIVE_POWER, var.set_pv_active_power_sensor)
await sensors(CONF_ENERGY_PRODUCTION_DAY, var.set_today_production_sensor)
await sensors(CONF_TOTAL_ENERGY_PRODUCTION, var.set_total_energy_production_sensor)
await sensors(CONF_INVERTER_MODULE_TEMP, var.set_inverter_module_temp_sensor)
if CONF_INVERTER_STATUS in config:
sens = await sensor.new_sensor(config[CONF_INVERTER_STATUS])
cg.add(var.set_inverter_status_sensor(sens))
if CONF_FREQUENCY in config:
sens = await sensor.new_sensor(config[CONF_FREQUENCY])
cg.add(var.set_grid_frequency_sensor(sens))
if CONF_ACTIVE_POWER in config:
sens = await sensor.new_sensor(config[CONF_ACTIVE_POWER])
cg.add(var.set_grid_active_power_sensor(sens))
if CONF_PV_ACTIVE_POWER in config:
sens = await sensor.new_sensor(config[CONF_PV_ACTIVE_POWER])
cg.add(var.set_pv_active_power_sensor(sens))
if CONF_ENERGY_PRODUCTION_DAY in config:
sens = await sensor.new_sensor(config[CONF_ENERGY_PRODUCTION_DAY])
cg.add(var.set_today_production_sensor(sens))
if CONF_TOTAL_ENERGY_PRODUCTION in config:
sens = await sensor.new_sensor(config[CONF_TOTAL_ENERGY_PRODUCTION])
cg.add(var.set_total_energy_production_sensor(sens))
if CONF_INVERTER_MODULE_TEMP in config:
sens = await sensor.new_sensor(config[CONF_INVERTER_MODULE_TEMP])
cg.add(var.set_inverter_module_temp_sensor(sens))
for i, phase in enumerate([CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]):
if phase not in config:
+59 -18
View File
@@ -229,24 +229,65 @@ 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_FREQUENCY, var.set_frequency_sensor)
await sensors(CONF_ACTIVE_POWER, var.set_active_power_sensor)
await sensors(CONF_REACTIVE_POWER, var.set_reactive_power_sensor)
await sensors(CONF_ENERGY_PRODUCTION_DAY, var.set_today_production_sensor)
await sensors(CONF_TOTAL_ENERGY_PRODUCTION, var.set_total_energy_production_sensor)
await sensors(CONF_TOTAL_GENERATION_TIME, var.set_total_generation_time_sensor)
await sensors(CONF_TODAY_GENERATION_TIME, var.set_today_generation_time_sensor)
await sensors(CONF_INVERTER_MODULE_TEMP, var.set_inverter_module_temp_sensor)
await sensors(CONF_INVERTER_INNER_TEMP, var.set_inverter_inner_temp_sensor)
await sensors(CONF_INVERTER_BUS_VOLTAGE, var.set_inverter_bus_voltage_sensor)
await sensors(
CONF_INSULATION_OF_PV_N_TO_GROUND, var.set_insulation_pv_n_to_ground_sensor
)
await sensors(CONF_GFCI_VALUE, var.set_gfci_value_sensor)
await sensors(CONF_DCI_OF_R, var.set_dci_of_r_sensor)
await sensors(CONF_DCI_OF_S, var.set_dci_of_s_sensor)
await sensors(CONF_DCI_OF_T, var.set_dci_of_t_sensor)
if CONF_FREQUENCY in config:
sens = await sensor.new_sensor(config[CONF_FREQUENCY])
cg.add(var.set_frequency_sensor(sens))
if CONF_ACTIVE_POWER in config:
sens = await sensor.new_sensor(config[CONF_ACTIVE_POWER])
cg.add(var.set_active_power_sensor(sens))
if CONF_REACTIVE_POWER in config:
sens = await sensor.new_sensor(config[CONF_REACTIVE_POWER])
cg.add(var.set_reactive_power_sensor(sens))
if CONF_ENERGY_PRODUCTION_DAY in config:
sens = await sensor.new_sensor(config[CONF_ENERGY_PRODUCTION_DAY])
cg.add(var.set_today_production_sensor(sens))
if CONF_TOTAL_ENERGY_PRODUCTION in config:
sens = await sensor.new_sensor(config[CONF_TOTAL_ENERGY_PRODUCTION])
cg.add(var.set_total_energy_production_sensor(sens))
if CONF_TOTAL_GENERATION_TIME in config:
sens = await sensor.new_sensor(config[CONF_TOTAL_GENERATION_TIME])
cg.add(var.set_total_generation_time_sensor(sens))
if CONF_TODAY_GENERATION_TIME in config:
sens = await sensor.new_sensor(config[CONF_TODAY_GENERATION_TIME])
cg.add(var.set_today_generation_time_sensor(sens))
if CONF_INVERTER_MODULE_TEMP in config:
sens = await sensor.new_sensor(config[CONF_INVERTER_MODULE_TEMP])
cg.add(var.set_inverter_module_temp_sensor(sens))
if CONF_INVERTER_INNER_TEMP in config:
sens = await sensor.new_sensor(config[CONF_INVERTER_INNER_TEMP])
cg.add(var.set_inverter_inner_temp_sensor(sens))
if CONF_INVERTER_BUS_VOLTAGE in config:
sens = await sensor.new_sensor(config[CONF_INVERTER_BUS_VOLTAGE])
cg.add(var.set_inverter_bus_voltage_sensor(sens))
if CONF_INSULATION_OF_PV_N_TO_GROUND in config:
sens = await sensor.new_sensor(config[CONF_INSULATION_OF_PV_N_TO_GROUND])
cg.add(var.set_insulation_pv_n_to_ground_sensor(sens))
if CONF_GFCI_VALUE in config:
sens = await sensor.new_sensor(config[CONF_GFCI_VALUE])
cg.add(var.set_gfci_value_sensor(sens))
if CONF_DCI_OF_R in config:
sens = await sensor.new_sensor(config[CONF_DCI_OF_R])
cg.add(var.set_dci_of_r_sensor(sens))
if CONF_DCI_OF_S in config:
sens = await sensor.new_sensor(config[CONF_DCI_OF_S])
cg.add(var.set_dci_of_s_sensor(sens))
if CONF_DCI_OF_T in config:
sens = await sensor.new_sensor(config[CONF_DCI_OF_T])
cg.add(var.set_dci_of_t_sensor(sens))
for i, phase in enumerate([CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]):
if phase not in config:
+12 -5
View File
@@ -107,11 +107,18 @@ async def to_code(config: ConfigType) -> None:
cf1 = await cg.gpio_pin_expression(config[CONF_CF1_PIN])
cg.add(var.set_cf1_pin(cf1))
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)
await sensors(CONF_ENERGY, var.set_energy_sensor)
if CONF_VOLTAGE in config:
sens = await sensor.new_sensor(config[CONF_VOLTAGE])
cg.add(var.set_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))
if CONF_ENERGY in config:
sens = await sensor.new_sensor(config[CONF_ENERGY])
cg.add(var.set_energy_sensor(sens))
cg.add(var.set_current_resistor(config[CONF_CURRENT_RESISTOR]))
cg.add(var.set_voltage_divider(config[CONF_VOLTAGE_DIVIDER]))
cg.add(var.set_initial_mode(INITIAL_MODES[config[CONF_INITIAL_MODE]]))
+11 -4
View File
@@ -92,10 +92,17 @@ 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_PM_1_0, var.set_pm_1_0_sensor)
await sensors(CONF_PM_2_5, var.set_pm_2_5_sensor)
await sensors(CONF_PM_10_0, var.set_pm_10_0_sensor)
if CONF_PM_1_0 in config:
sens = await sensor.new_sensor(config[CONF_PM_1_0])
cg.add(var.set_pm_1_0_sensor(sens))
if CONF_PM_2_5 in config:
sens = await sensor.new_sensor(config[CONF_PM_2_5])
cg.add(var.set_pm_2_5_sensor(sens))
if CONF_PM_10_0 in config:
sens = await sensor.new_sensor(config[CONF_PM_10_0])
cg.add(var.set_pm_10_0_sensor(sens))
# Remove before 2026.12.0
if CONF_AQI in config:
@@ -1,9 +1,6 @@
#include "hoermann_hcp.h"
#include <algorithm>
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::hoermann_hcp {
@@ -63,61 +60,6 @@ static bool is_moving(DoorState state) {
}
}
#ifdef USE_HOERMANN_HCP_IDENTITY
// The command byte of a status poll. Only its answer can carry a request.
static constexpr uint8_t STATUS_COMMAND = 0x03;
// A status answer with this code in the low byte of its second register asks the bus controller for a value,
// named in the high byte of the third.
static constexpr uint8_t ANSWER_REQUEST = 0x22;
static constexpr uint8_t REQUEST_SERIAL = 0x05;
static constexpr uint8_t REQUEST_FIRMWARE = 0x06;
// Each request goes out in one answer, up to this many times, this far apart.
static constexpr uint8_t IDENTITY_MAX_ATTEMPTS = 3;
static constexpr uint32_t IDENTITY_RETRY_MS = 30000;
// The value comes back as a payload transfer: this command in the low byte of the first command register, a sub
// code in the high byte of the second, the payload from the third on.
static constexpr uint8_t TRANSFER_COMMAND = 0x04;
static constexpr uint8_t TRANSFER_SUB_SERIAL = 0x0C;
static constexpr uint8_t TRANSFER_SUB_FIRMWARE = 0x0D;
static constexpr uint8_t TRANSFER_ACK = 0xFD;
static constexpr size_t TRANSFER_PAYLOAD_REG = 2;
// Marks the first half of the serial number in the counter byte, and is not part of the count.
static constexpr uint8_t COUNTER_FIRST_HALF = 0x80;
// Older motors (index B1 seen) send the whole serial number in one frame, without the half marker. It is as long
// as a second half, so one copy path serves both.
static constexpr size_t SERIAL_SINGLE_FRAME_REGS = 6;
static_assert(SERIAL_SINGLE_FRAME_REGS == SERIAL_SECOND_HALF_REGS, "one copy path serves both serial frames");
// Registers hold two payload bytes each, high byte first.
static void copy_payload(const modbus::RegisterValues &registers, size_t count, char *out) {
for (size_t i = 0; i < count; i++) {
const uint16_t value = registers[TRANSFER_PAYLOAD_REG + i];
out[2 * i] = static_cast<char>(value >> 8);
out[2 * i + 1] = static_cast<char>(value);
}
}
// Length of the printable text at the start, without trailing spaces. The padding after it varies.
static size_t text_length(const char *text, size_t len) {
size_t at = 0;
while (at < len && text[at] >= 0x20 && text[at] <= 0x7E)
at++;
while (at > 0 && text[at - 1] == ' ')
at--;
return at;
}
static void terminate_text(char *text, size_t len) { text[text_length(text, len)] = '\0'; }
// dump_config() is replayed to remote log clients, so they see the outcome of the exchange at boot.
static void log_identity_value(text_sensor::TextSensor *sensor) {
if (sensor != nullptr) {
ESP_LOGCONFIG(TAG, " Value: %s",
sensor->has_state() ? sensor->get_state().c_str() : LOG_STR_LITERAL("not received"));
}
}
#endif
void HoermannHcp::update() {
const uint32_t now = millis();
// Time out the connection flag if the bus controller stopped polling.
@@ -149,9 +91,6 @@ void HoermannHcp::update() {
ESP_LOGW(TAG, "Door did not report the lamp changing, giving up on the toggle");
this->forget_light_toggles_();
}
#ifdef USE_HOERMANN_HCP_IDENTITY
this->publish_identity_();
#endif
if (this->changed_) {
this->changed_ = false;
this->state_callback_.call();
@@ -163,12 +102,6 @@ void HoermannHcp::dump_config() {
"Hoermann HCP bridge:\n"
" Modbus server address: 0x%02X",
this->get_address());
#ifdef USE_HOERMANN_HCP_IDENTITY
LOG_TEXT_SENSOR(" ", "Serial Number", this->serial_number_text_sensor_);
log_identity_value(this->serial_number_text_sensor_);
LOG_TEXT_SENSOR(" ", "Firmware Version", this->version_text_sensor_);
log_identity_value(this->version_text_sensor_);
#endif
}
modbus::ResponseStatus HoermannHcp::on_read_holding_registers(uint16_t start_address, uint16_t number_of_registers,
@@ -180,14 +113,6 @@ modbus::ResponseStatus HoermannHcp::on_read_holding_registers(uint16_t start_add
this->record_response_();
#ifdef USE_HOERMANN_HCP_IDENTITY
// Acknowledge the transfer taken by the write half of this frame.
if (this->transfer_answer_counter_ != NO_TRANSFER_ANSWER) {
this->push_transfer_answer_(registers, number_of_registers);
return {};
}
#endif
// 0x17 read half: STATE_REG is read back right after COMMAND_REG was written, so echo the stored message
// counter (high byte) and command (low byte). The read length identifies which internal block is requested.
const uint16_t counter = this->command_reg_value_ & 0xFF00;
@@ -200,9 +125,6 @@ modbus::ResponseStatus HoermannHcp::on_read_holding_registers(uint16_t start_add
registers.push_back(static_cast<uint16_t>(0x0001 | command));
this->push_command_registers_(registers);
push_zeros(registers, 4);
#ifdef USE_HOERMANN_HCP_IDENTITY
this->add_identity_request_(registers, command);
#endif
break;
case 2:
// Empty command request.
@@ -234,9 +156,6 @@ modbus::ResponseStatus HoermannHcp::on_write_registers(uint16_t start_address,
// command byte back from STATE_REG. The hub always runs the write before the read within one request.
this->record_response_();
this->command_reg_value_ = registers[0];
#ifdef USE_HOERMANN_HCP_IDENTITY
this->transfer_answer_counter_ = this->take_identity_transfer_(registers);
#endif
return {};
}
@@ -298,155 +217,6 @@ void HoermannHcp::push_command_registers_(modbus::RegisterValues &registers) {
registers.push_back(command->released_value_2);
}
#ifdef USE_HOERMANN_HCP_IDENTITY
void HoermannHcp::add_identity_request_(modbus::RegisterValues &registers, uint16_t command) {
if (static_cast<uint8_t>(this->command_reg_value_) != STATUS_COMMAND)
return;
// Like Hoermann's own bus accessory, only after one ordinary answer.
if (this->identity_phase_ == IdentityPhase::IDENTITY_PHASE_IDLE) {
if (this->serial_number_text_sensor_ != nullptr || this->version_text_sensor_ != nullptr)
this->arm_identity_request_(IdentityPhase::IDENTITY_PHASE_SERIAL);
return;
}
// Uses the registers of a key press, so it waits while one is pending.
if (this->next_command_ != nullptr || registers[2] != 0 || registers[3] != 0 || !this->take_identity_request_())
return;
registers[1] = static_cast<uint16_t>(ANSWER_REQUEST | command);
registers[2] = encode_uint16(this->identity_request_(), 0);
}
void HoermannHcp::arm_identity_request_(IdentityPhase phase) {
this->identity_phase_ = phase;
this->identity_attempts_ = 0;
}
bool HoermannHcp::take_identity_request_() {
const uint8_t request = this->identity_request_();
if (request == 0)
return false;
const uint32_t now = millis();
if (this->identity_attempts_ != 0 && now - this->identity_asked_at_ <= IDENTITY_RETRY_MS)
return false;
if (this->identity_attempts_ >= IDENTITY_MAX_ATTEMPTS) {
this->identity_unanswered_ = request;
if (request == REQUEST_SERIAL) {
// Still ask for the firmware version, and drop a leftover first half.
this->serial_number_[0] = '\0';
this->arm_identity_request_(IdentityPhase::IDENTITY_PHASE_FIRMWARE);
} else {
this->identity_phase_ = IdentityPhase::IDENTITY_PHASE_DONE;
}
return false;
}
this->identity_attempts_++;
this->identity_asked_at_ = now;
return true;
}
uint8_t HoermannHcp::take_identity_transfer_(const modbus::RegisterValues &registers) {
if (this->identity_phase_ == IdentityPhase::IDENTITY_PHASE_IDLE || registers.size() < TRANSFER_PAYLOAD_REG ||
static_cast<uint8_t>(registers[0]) != TRANSFER_COMMAND)
return NO_TRANSFER_ANSWER;
const uint8_t counter = static_cast<uint8_t>(registers[0] >> 8);
const uint8_t sub_code = static_cast<uint8_t>(registers[1] >> 8);
if (sub_code != TRANSFER_SUB_SERIAL && sub_code != TRANSFER_SUB_FIRMWARE)
return NO_TRANSFER_ANSWER;
// Acknowledged whether kept or not, as Hoermann's own bus accessory does. What was not kept is asked for again.
const uint8_t answer = counter & ~COUNTER_FIRST_HALF;
// Only an answer to the request that went out is kept.
const uint8_t request = sub_code == TRANSFER_SUB_SERIAL ? REQUEST_SERIAL : REQUEST_FIRMWARE;
if (this->identity_attempts_ == 0 || this->identity_request_() != request)
return answer;
const size_t payload_regs = registers.size() - TRANSFER_PAYLOAD_REG;
if (request == REQUEST_FIRMWARE) {
const size_t regs = std::min(payload_regs, FIRMWARE_REGS);
copy_payload(registers, regs, this->firmware_version_);
if (regs == FIRMWARE_REGS && text_length(this->firmware_version_, 2 * regs) != 0) {
terminate_text(this->firmware_version_, 2 * regs);
// Clear an earlier unreadable answer, so this one is shown.
this->firmware_unreadable_ = false;
this->identity_phase_ = IdentityPhase::IDENTITY_PHASE_DONE;
return answer;
}
// Left as received for update() to log.
this->firmware_unreadable_ = true;
this->firmware_unreadable_len_ = 2 * regs;
// A short answer is asked for again; unreadable text would come back the same.
if (regs == FIRMWARE_REGS)
this->identity_phase_ = IdentityPhase::IDENTITY_PHASE_DONE;
return answer;
}
if ((counter & COUNTER_FIRST_HALF) != 0) {
// Even a half too short to keep says the number comes in two halves; a first half already kept stays kept.
if (this->identity_phase_ == IdentityPhase::IDENTITY_PHASE_SERIAL)
this->identity_phase_ = IdentityPhase::IDENTITY_PHASE_SERIAL_SPLIT;
if (payload_regs >= SERIAL_FIRST_HALF_REGS) {
copy_payload(registers, SERIAL_FIRST_HALF_REGS, this->serial_number_);
this->identity_phase_ = IdentityPhase::IDENTITY_PHASE_SERIAL_SECOND_HALF;
}
return answer;
}
// Without a half marker seen, the frame is the whole number; after one, it can only be the second half.
if (this->identity_phase_ == IdentityPhase::IDENTITY_PHASE_SERIAL_SPLIT || payload_regs < SERIAL_SINGLE_FRAME_REGS)
return answer;
const size_t at =
this->identity_phase_ == IdentityPhase::IDENTITY_PHASE_SERIAL_SECOND_HALF ? 2 * SERIAL_FIRST_HALF_REGS : 0;
copy_payload(registers, SERIAL_SINGLE_FRAME_REGS, this->serial_number_ + at);
terminate_text(this->serial_number_, at + 2 * SERIAL_SINGLE_FRAME_REGS);
this->serial_unreadable_ = this->serial_number_[0] == '\0';
this->arm_identity_request_(IdentityPhase::IDENTITY_PHASE_FIRMWARE);
return answer;
}
void HoermannHcp::push_transfer_answer_(modbus::RegisterValues &registers, uint16_t number_of_registers) {
const uint16_t answer[] = {encode_uint16(this->transfer_answer_counter_, 0),
encode_uint16(TRANSFER_COMMAND, TRANSFER_ACK)};
this->transfer_answer_counter_ = NO_TRANSFER_ANSWER;
for (uint16_t i = 0; i < number_of_registers; i++)
registers.push_back(i < 2 ? answer[i] : 0x0000);
}
void HoermannHcp::publish_identity_() {
// Buffers are cleared once published, so each value goes out once. The serial number is whole once the firmware
// version is requested.
if (this->serial_number_text_sensor_ != nullptr && this->identity_request_() != REQUEST_SERIAL &&
this->serial_number_[0] != '\0') {
this->serial_number_text_sensor_->publish_state(this->serial_number_);
this->serial_number_[0] = '\0';
}
if (this->serial_unreadable_) {
this->serial_unreadable_ = false;
ESP_LOGW(TAG, "Unreadable serial number");
}
// Checked first: the buffer then holds raw bytes, not text.
if (this->firmware_unreadable_) {
this->firmware_unreadable_ = false;
const uint8_t len = this->firmware_unreadable_len_;
// All zeros: the motor does not report a version (index B1 seen).
if (len == 2 * FIRMWARE_REGS &&
std::all_of(this->firmware_version_, this->firmware_version_ + len, [](char c) { return c == '\0'; })) {
ESP_LOGD(TAG, "Motor does not report its firmware version");
} else {
char hex[format_hex_size(2 * FIRMWARE_REGS)];
ESP_LOGW(TAG, "Unreadable firmware version (%u bytes): %s", len,
format_hex_to(hex, reinterpret_cast<const uint8_t *>(this->firmware_version_), len));
}
this->firmware_version_[0] = '\0';
}
if (this->version_text_sensor_ != nullptr && this->firmware_version_[0] != '\0') {
this->version_text_sensor_->publish_state(this->firmware_version_);
this->firmware_version_[0] = '\0';
}
if (this->identity_unanswered_ != 0) {
ESP_LOGW(TAG, "No usable %s received",
this->identity_unanswered_ == REQUEST_SERIAL ? LOG_STR_LITERAL("serial number")
: LOG_STR_LITERAL("firmware version"));
this->identity_unanswered_ = 0;
}
}
#endif
void HoermannHcp::on_position_reg_(uint16_t value) {
// Low byte: current position.
const uint8_t position = static_cast<uint8_t>(value);
@@ -601,9 +371,6 @@ void HoermannHcp::set_valid_(bool valid) {
this->forget_light_toggles_();
// The lamp can be switched at the door while the bus is quiet, so what was last read is no longer trusted.
this->set_light_seen_(false);
// The same holds for the door. The next broadcast is decoded even if it repeats the last one.
this->door_state_seen_ = false;
this->prev_state_reg_ = 0xFFFF;
this->short_broadcast_logged_ = false;
}
@@ -643,11 +410,6 @@ void HoermannHcp::forget_light_toggles_() {
}
void HoermannHcp::set_door_state_(DoorState state) {
// The first state may equal the default, so being seen is a change of its own.
if (!this->door_state_seen_) {
this->door_state_seen_ = true;
this->changed_ = true;
}
if (this->door_state_ == state)
return;
this->door_state_ = state;
@@ -4,11 +4,7 @@
#include "esphome/components/modbus/modbus.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#ifdef USE_HOERMANN_HCP_IDENTITY
#include "esphome/components/text_sensor/text_sensor.h"
#endif
namespace esphome::hoermann_hcp {
@@ -25,28 +21,6 @@ enum class DoorState : uint8_t {
STOPPED,
};
#ifdef USE_HOERMANN_HCP_IDENTITY
// Payload registers of each value, two bytes each.
static constexpr size_t SERIAL_FIRST_HALF_REGS = 7;
static constexpr size_t SERIAL_SECOND_HALF_REGS = 6;
static constexpr size_t FIRMWARE_REGS = 6;
// Counter of the transfer the read half of the frame acknowledges. Real counters have the 0x80 half marker
// stripped, so this value never occurs.
static constexpr uint8_t NO_TRANSFER_ANSWER = 0xFF;
// Where the identity exchange stands. The low nibble is the code the motor is asked with, 0 while nothing is
// outstanding. Like Hoermann's own bus accessory, the first status answer stays ordinary. Older motors (index B1
// seen) send the whole serial number in one frame, without the half marker.
enum class IdentityPhase : uint8_t {
IDENTITY_PHASE_IDLE = 0x00,
IDENTITY_PHASE_DONE = 0x10,
IDENTITY_PHASE_SERIAL = 0x05, // A plain frame is the whole serial number.
IDENTITY_PHASE_SERIAL_SPLIT = 0x15, // A half marker was seen, so a plain frame can only be the second half.
IDENTITY_PHASE_SERIAL_SECOND_HALF = 0x25, // The first half is in.
IDENTITY_PHASE_FIRMWARE = 0x06,
};
#endif
// A HCP command is a simulated key press: the pressed value is presented to the bus controller, then after a
// short delay the released value. Each half also carries a second register, which names the buttons that do
// not fit into the first.
@@ -61,12 +35,6 @@ struct HoermannHcpCommand {
};
class HoermannHcp : public PollingComponent, public modbus::ModbusServerDevice {
#ifdef USE_HOERMANN_HCP_IDENTITY
// The motor is asked for these only when one of them is configured.
SUB_TEXT_SENSOR(serial_number)
SUB_TEXT_SENSOR(version)
#endif
public:
void update() override;
void dump_config() override;
@@ -95,9 +63,6 @@ class HoermannHcp : public PollingComponent, public modbus::ModbusServerDevice {
bool toggle_light();
DoorState get_door_state() const { return this->door_state_; }
// False until a broadcast has carried a state the door is known to report. Bus traffic alone makes the
// connection valid, so get_door_state() would still be its default.
bool is_door_state_known() const { return this->door_state_seen_; }
float get_current_position() const { return this->current_position_; }
bool is_valid() const { return this->valid_; }
bool is_light_on() const { return this->light_on_; }
@@ -130,21 +95,6 @@ class HoermannHcp : public PollingComponent, public modbus::ModbusServerDevice {
void on_position_reg_(uint16_t value);
void on_state_reg_(uint16_t value);
void on_light_reg_(uint16_t value);
#ifdef USE_HOERMANN_HCP_IDENTITY
// Puts a due request into a status answer.
void add_identity_request_(modbus::RegisterValues &registers, uint16_t command);
void arm_identity_request_(IdentityPhase phase);
uint8_t identity_request_() const { return static_cast<uint8_t>(this->identity_phase_) & 0x0F; }
// True when the request is due, counting the attempt. Gives up after the last one.
bool take_identity_request_();
// Takes a value the motor hands over as a payload transfer. Returns the counter to acknowledge it with, kept or
// not, or NO_TRANSFER_ANSWER when the frame was something else.
uint8_t take_identity_transfer_(const modbus::RegisterValues &registers);
// Acknowledges the transfer taken by the write half of the same frame.
void push_transfer_answer_(modbus::RegisterValues &registers, uint16_t number_of_registers);
// Runs from update(), outside the bus callbacks.
void publish_identity_();
#endif
void set_valid_(bool valid);
void set_door_state_(DoorState state);
@@ -195,24 +145,7 @@ class HoermannHcp : public PollingComponent, public modbus::ModbusServerDevice {
bool changed_{false};
bool light_on_{false};
bool light_seen_{false};
bool door_state_seen_{false};
bool short_broadcast_logged_{false};
#ifdef USE_HOERMANN_HCP_IDENTITY
uint32_t identity_asked_at_{0};
IdentityPhase identity_phase_{IdentityPhase::IDENTITY_PHASE_IDLE};
uint8_t identity_attempts_{0};
// The request given up on, for update() to report.
uint8_t identity_unanswered_{0};
uint8_t transfer_answer_counter_{NO_TRANSFER_ANSWER};
// Length of an unreadable firmware version left in firmware_version_.
uint8_t firmware_unreadable_len_{0};
// A serial number arrived without text, for update() to log.
bool serial_unreadable_{false};
bool firmware_unreadable_{false};
char serial_number_[2 * (SERIAL_FIRST_HALF_REGS + SERIAL_SECOND_HALF_REGS) + 1]{};
char firmware_version_[2 * FIRMWARE_REGS + 1]{};
#endif
};
} // namespace esphome::hoermann_hcp
@@ -1,50 +0,0 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import CONF_VERSION, ENTITY_CATEGORY_DIAGNOSTIC, ICON_CHIP
from esphome.types import ConfigType
from .. import CONF_HOERMANN_HCP_ID, HoermannHcp, hoermann_hcp_ns
DEPENDENCIES = ["hoermann_hcp"]
CONF_DOOR_STATE = "door_state"
CONF_SERIAL_NUMBER = "serial_number"
HoermannHcpDoorStateTextSensor = hoermann_hcp_ns.class_(
"HoermannHcpDoorStateTextSensor", text_sensor.TextSensor, cg.Component
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(CONF_HOERMANN_HCP_ID): cv.use_id(HoermannHcp),
cv.Optional(CONF_DOOR_STATE): text_sensor.text_sensor_schema(
HoermannHcpDoorStateTextSensor, icon="mdi:garage"
).extend(cv.COMPONENT_SCHEMA),
cv.Optional(CONF_SERIAL_NUMBER): text_sensor.text_sensor_schema(
icon="mdi:data-matrix", entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema(
icon=ICON_CHIP, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
}
),
cv.has_at_least_one_key(CONF_DOOR_STATE, CONF_SERIAL_NUMBER, CONF_VERSION),
)
async def to_code(config: ConfigType) -> None:
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, 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")
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))
@@ -1,49 +0,0 @@
#include "hoermann_hcp_text_sensor.h"
#include <cstring>
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::hoermann_hcp {
static const char *const TAG = "hoermann_hcp.text_sensor";
// Indexed by DoorState. Each fits the 15 characters std::string keeps inline, so publishing never allocates.
PROGMEM_STRING_TABLE(DoorStateStrings, "Open", "Opening", "Closed", "Closing", "Half open", "Moving to vent",
"Vent position", "Moving to half", "Stopped");
static_assert(DoorStateStrings::COUNT == static_cast<size_t>(DoorState::STOPPED) + 1,
"DoorStateStrings must list every DoorState in order");
void HoermannHcpDoorStateTextSensor::setup() {
this->parent_->add_on_state_callback([this]() { this->update_from_state_(); });
// A state decoded before the callback was added would otherwise wait for the next change.
this->update_from_state_();
}
void HoermannHcpDoorStateTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Hoermann HCP Door State", this); }
void HoermannHcpDoorStateTextSensor::update_from_state_() {
// The last state stays while the bus controller is gone, and is published again once it is back.
if (!this->parent_->is_valid()) {
this->published_ = false;
return;
}
if (!this->parent_->is_door_state_known())
return;
// Any hub change runs this, so only a new door state is published.
const DoorState state = this->parent_->get_door_state();
if (this->published_ && state == this->published_state_)
return;
this->published_ = true;
this->published_state_ = state;
char text[16];
ESPHOME_strncpy_P(text,
reinterpret_cast<ESPHOME_PGM_P>(
DoorStateStrings::get_progmem_str(static_cast<uint8_t>(state), DoorStateStrings::LAST_INDEX)),
sizeof(text));
text[sizeof(text) - 1] = '\0';
this->publish_state(text);
}
} // namespace esphome::hoermann_hcp
@@ -1,24 +0,0 @@
#pragma once
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/core/component.h"
#include "../hoermann_hcp.h"
namespace esphome::hoermann_hcp {
class HoermannHcpDoorStateTextSensor : public text_sensor::TextSensor, public Component {
public:
explicit HoermannHcpDoorStateTextSensor(HoermannHcp *parent) : parent_(parent) {}
void setup() override;
void dump_config() override;
protected:
void update_from_state_();
HoermannHcp *const parent_;
DoorState published_state_{DoorState::CLOSED};
bool published_{false};
};
} // namespace esphome::hoermann_hcp
+11 -25
View File
@@ -1,5 +1,3 @@
import sys
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
@@ -10,9 +8,9 @@ from esphome.const import (
KEY_TARGET_PLATFORM,
PLATFORM_HOST,
ThreadModel,
Toolchain,
)
from esphome.core import CORE, EsphomeError
from esphome.core import CORE
from esphome.platformio.toolchain import copy_ccache_script
from esphome.types import ConfigType
from .const import KEY_HOST
@@ -20,9 +18,6 @@ from .const import KEY_HOST
# force import gpio to register pin schema
from .gpio import host_pin_to_code # noqa: F401
# The curated headers the host build precompiles
HOST_PCH_PREFIX = "esphome/core/pch_prefix.h"
CODEOWNERS = ["@esphome/core", "@clydebarrow"]
AUTO_LOAD = ["network", "preferences"]
IS_TARGET_PLATFORM = True
@@ -42,9 +37,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_MAC_ADDRESS, default="98:35:69:ab:f6:79"): cv.mac_address,
}
),
# The host builds with the machine's own compiler through ninja; there
# is no PlatformIO backend, so a CLI --toolchain must name this one
cv.resolve_toolchain("host", (Toolchain.HOST,), Toolchain.HOST),
cv.require_platformio_toolchain("host"),
set_core_data,
)
@@ -55,23 +48,16 @@ async def to_code(config: ConfigType) -> None:
# The prefs file finds stored preferences by key, so key migration is possible
cg.add_define("USE_PREFERENCE_KEY_LOOKUP")
cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts)
cg.set_cpp_standard("gnu++20")
cg.add_build_flag("-std=gnu++20")
cg.add_define("ESPHOME_BOARD", "host")
cg.add_define("ESPHOME_VARIANT", "HOST")
cg.add_define(ThreadModel.MULTI_ATOMICS)
cg.add_platformio_option("platform", "platformio/native")
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")
cg.add_platformio_option("extra_scripts", ["pre:ccache.py"])
# Called by __main__.compile_program; True means this platform built the
# program itself instead of falling through to the PlatformIO toolchain.
def run_compile(args: object, config: ConfigType) -> bool:
from esphome.host import toolchain
if sys.platform == "win32":
# Here, not in validation: `esphome config` must keep working
raise EsphomeError(
"The host platform uses POSIX interfaces and does not build on "
"Windows; use WSL or a Linux or macOS machine"
)
if toolchain.run_compile(config, CORE.verbose) != 0:
raise EsphomeError("Host build failed")
return True
# Called by writer.py
def copy_files() -> None:
copy_ccache_script()
+15 -5
View File
@@ -80,8 +80,18 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_max_current_a(config[CONF_MAX_CURRENT]))
cg.add(var.set_max_voltage_v(config[CONF_MAX_VOLTAGE]))
sensors = sensor.sub_sensors(config)
await sensors(CONF_BUS_VOLTAGE, var.set_bus_voltage_sensor)
await sensors(CONF_SHUNT_VOLTAGE, var.set_shunt_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_SHUNT_VOLTAGE in config:
sens = await sensor.new_sensor(config[CONF_SHUNT_VOLTAGE])
cg.add(var.set_shunt_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))
+15 -5
View File
@@ -133,8 +133,18 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_adc_avg_samples(config[CONF_ADC_AVERAGING]))
sensors = sensor.sub_sensors(config)
await sensors(CONF_BUS_VOLTAGE, var.set_bus_voltage_sensor)
await sensors(CONF_SHUNT_VOLTAGE, var.set_shunt_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_SHUNT_VOLTAGE in config:
sens = await sensor.new_sensor(config[CONF_SHUNT_VOLTAGE])
cg.add(var.set_shunt_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))
+35 -10
View File
@@ -225,13 +225,38 @@ async def setup_ina2xx(var: MockObj, config: ConfigType) -> None:
cg.add(var.set_adc_time_shunt_voltage(adc_time_config))
cg.add(var.set_adc_time_die_temperature(adc_time_config))
sensors = sensor.sub_sensors(config)
await sensors(CONF_SHUNT_VOLTAGE, var.set_shunt_voltage_sensor)
await sensors(CONF_BUS_VOLTAGE, var.set_bus_voltage_sensor)
await sensors(CONF_TEMPERATURE, var.set_die_temperature_sensor)
await sensors(CONF_CURRENT, var.set_current_sensor)
await sensors(CONF_POWER, var.set_power_sensor)
await sensors(CONF_ENERGY, var.set_energy_sensor_wh)
await sensors(CONF_ENERGY_JOULES, var.set_energy_sensor_j)
await sensors(CONF_CHARGE, var.set_charge_sensor_ah)
await sensors(CONF_CHARGE_COULOMBS, var.set_charge_sensor_c)
if conf := config.get(CONF_SHUNT_VOLTAGE):
sens = await sensor.new_sensor(conf)
cg.add(var.set_shunt_voltage_sensor(sens))
if conf := config.get(CONF_BUS_VOLTAGE):
sens = await sensor.new_sensor(conf)
cg.add(var.set_bus_voltage_sensor(sens))
if conf := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(conf)
cg.add(var.set_die_temperature_sensor(sens))
if conf := config.get(CONF_CURRENT):
sens = await sensor.new_sensor(conf)
cg.add(var.set_current_sensor(sens))
if conf := config.get(CONF_POWER):
sens = await sensor.new_sensor(conf)
cg.add(var.set_power_sensor(sens))
if conf := config.get(CONF_ENERGY):
sens = await sensor.new_sensor(conf)
cg.add(var.set_energy_sensor_wh(sens))
if conf := config.get(CONF_ENERGY_JOULES):
sens = await sensor.new_sensor(conf)
cg.add(var.set_energy_sensor_j(sens))
if conf := config.get(CONF_CHARGE):
sens = await sensor.new_sensor(conf)
cg.add(var.set_charge_sensor_ah(sens))
if conf := config.get(CONF_CHARGE_COULOMBS):
sens = await sensor.new_sensor(conf)
cg.add(var.set_charge_sensor_c(sens))
+13 -8
View File
@@ -48,11 +48,16 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2410_ID])
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(CONF_HAS_TARGET, hub.set_target_binary_sensor)
await binary_sensors(CONF_HAS_MOVING_TARGET, hub.set_moving_target_binary_sensor)
await binary_sensors(CONF_HAS_STILL_TARGET, hub.set_still_target_binary_sensor)
await binary_sensors(
CONF_OUT_PIN_PRESENCE_STATUS, hub.set_out_pin_presence_status_binary_sensor
)
ld2410_component = await cg.get_variable(config[CONF_LD2410_ID])
if has_target_config := config.get(CONF_HAS_TARGET):
sens = await binary_sensor.new_binary_sensor(has_target_config)
cg.add(ld2410_component.set_target_binary_sensor(sens))
if has_moving_target_config := config.get(CONF_HAS_MOVING_TARGET):
sens = await binary_sensor.new_binary_sensor(has_moving_target_config)
cg.add(ld2410_component.set_moving_target_binary_sensor(sens))
if has_still_target_config := config.get(CONF_HAS_STILL_TARGET):
sens = await binary_sensor.new_binary_sensor(has_still_target_config)
cg.add(ld2410_component.set_still_target_binary_sensor(sens))
if out_pin_presence_status_config := config.get(CONF_OUT_PIN_PRESENCE_STATUS):
sens = await binary_sensor.new_binary_sensor(out_pin_presence_status_config)
cg.add(ld2410_component.set_out_pin_presence_status_binary_sensor(sens))
+13 -5
View File
@@ -46,8 +46,16 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2410_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(CONF_FACTORY_RESET, hub.set_factory_reset_button)
await buttons(CONF_RESTART, hub.set_restart_button)
await buttons(CONF_QUERY_PARAMS, hub.set_query_button)
ld2410_component = await cg.get_variable(config[CONF_LD2410_ID])
if factory_reset_config := config.get(CONF_FACTORY_RESET):
b = await button.new_button(factory_reset_config)
await cg.register_parented(b, config[CONF_LD2410_ID])
cg.add(ld2410_component.set_factory_reset_button(b))
if restart_config := config.get(CONF_RESTART):
b = await button.new_button(restart_config)
await cg.register_parented(b, config[CONF_LD2410_ID])
cg.add(ld2410_component.set_restart_button(b))
if query_params_config := config.get(CONF_QUERY_PARAMS):
b = await button.new_button(query_params_config)
await cg.register_parented(b, config[CONF_LD2410_ID])
cg.add(ld2410_component.set_query_button(b))
+29 -30
View File
@@ -87,32 +87,31 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2410_ID])
numbers = number.sub_numbers(config, parent=hub)
await numbers(
CONF_TIMEOUT, hub.set_timeout_number, min_value=0, max_value=65535, step=1
)
await numbers(
CONF_MAX_MOVE_DISTANCE_GATE,
hub.set_max_move_distance_gate_number,
min_value=2,
max_value=8,
step=1,
)
await numbers(
CONF_MAX_STILL_DISTANCE_GATE,
hub.set_max_still_distance_gate_number,
min_value=2,
max_value=8,
step=1,
)
await numbers(
CONF_LIGHT_THRESHOLD,
hub.set_light_threshold_number,
min_value=0,
max_value=255,
step=1,
)
ld2410_component = await cg.get_variable(config[CONF_LD2410_ID])
if timeout_config := config.get(CONF_TIMEOUT):
n = await number.new_number(
timeout_config, min_value=0, max_value=65535, step=1
)
await cg.register_parented(n, config[CONF_LD2410_ID])
cg.add(ld2410_component.set_timeout_number(n))
if max_move_distance_gate_config := config.get(CONF_MAX_MOVE_DISTANCE_GATE):
n = await number.new_number(
max_move_distance_gate_config, min_value=2, max_value=8, step=1
)
await cg.register_parented(n, config[CONF_LD2410_ID])
cg.add(ld2410_component.set_max_move_distance_gate_number(n))
if max_still_distance_gate_config := config.get(CONF_MAX_STILL_DISTANCE_GATE):
n = await number.new_number(
max_still_distance_gate_config, min_value=2, max_value=8, step=1
)
await cg.register_parented(n, config[CONF_LD2410_ID])
cg.add(ld2410_component.set_max_still_distance_gate_number(n))
if light_threshold_config := config.get(CONF_LIGHT_THRESHOLD):
n = await number.new_number(
light_threshold_config, min_value=0, max_value=255, step=1
)
await cg.register_parented(n, config[CONF_LD2410_ID])
cg.add(ld2410_component.set_light_threshold_number(n))
for x in range(9):
if gate_conf := config.get(f"g{x}"):
move_config = gate_conf[CONF_MOVE_THRESHOLD]
@@ -120,13 +119,13 @@ async def to_code(config: ConfigType) -> None:
await number.register_number(
n, move_config, min_value=0, max_value=100, step=1
)
await cg.register_parented(n, hub)
cg.add(hub.set_gate_move_threshold_number(x, n))
await cg.register_parented(n, config[CONF_LD2410_ID])
cg.add(ld2410_component.set_gate_move_threshold_number(x, n))
still_config = gate_conf[CONF_STILL_THRESHOLD]
n = cg.new_Pvariable(still_config[CONF_ID], x)
await number.register_number(
n, still_config, min_value=0, max_value=100, step=1
)
await cg.register_parented(n, hub)
cg.add(hub.set_gate_still_threshold_number(x, n))
await cg.register_parented(n, config[CONF_LD2410_ID])
cg.add(ld2410_component.set_gate_still_threshold_number(x, n))
+33 -29
View File
@@ -50,32 +50,36 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2410_ID])
selects = select.sub_selects(config, parent=hub)
await selects(
CONF_DISTANCE_RESOLUTION,
hub.set_distance_resolution_select,
options=["0.2m", "0.75m"],
)
await selects(
CONF_OUT_PIN_LEVEL, hub.set_out_pin_level_select, options=["low", "high"]
)
await selects(
CONF_LIGHT_FUNCTION,
hub.set_light_function_select,
options=["off", "below", "above"],
)
await selects(
CONF_BAUD_RATE,
hub.set_baud_rate_select,
options=[
"9600",
"19200",
"38400",
"57600",
"115200",
"230400",
"256000",
"460800",
],
)
ld2410_component = await cg.get_variable(config[CONF_LD2410_ID])
if distance_resolution_config := config.get(CONF_DISTANCE_RESOLUTION):
s = await select.new_select(
distance_resolution_config, options=["0.2m", "0.75m"]
)
await cg.register_parented(s, config[CONF_LD2410_ID])
cg.add(ld2410_component.set_distance_resolution_select(s))
if out_pin_level_config := config.get(CONF_OUT_PIN_LEVEL):
s = await select.new_select(out_pin_level_config, options=["low", "high"])
await cg.register_parented(s, config[CONF_LD2410_ID])
cg.add(ld2410_component.set_out_pin_level_select(s))
if light_function_config := config.get(CONF_LIGHT_FUNCTION):
s = await select.new_select(
light_function_config, options=["off", "below", "above"]
)
await cg.register_parented(s, config[CONF_LD2410_ID])
cg.add(ld2410_component.set_light_function_select(s))
if baud_rate_config := config.get(CONF_BAUD_RATE):
s = await select.new_select(
baud_rate_config,
options=[
"9600",
"19200",
"38400",
"57600",
"115200",
"230400",
"256000",
"460800",
],
)
await cg.register_parented(s, config[CONF_LD2410_ID])
cg.add(ld2410_component.set_baud_rate_select(s))
+21 -10
View File
@@ -157,19 +157,30 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2410_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_MOVING_DISTANCE, hub.set_moving_target_distance_sensor)
await sensors(CONF_STILL_DISTANCE, hub.set_still_target_distance_sensor)
await sensors(CONF_MOVING_ENERGY, hub.set_moving_target_energy_sensor)
await sensors(CONF_STILL_ENERGY, hub.set_still_target_energy_sensor)
await sensors(CONF_LIGHT, hub.set_light_sensor)
await sensors(CONF_DETECTION_DISTANCE, hub.set_detection_distance_sensor)
ld2410_component = await cg.get_variable(config[CONF_LD2410_ID])
if moving_distance_config := config.get(CONF_MOVING_DISTANCE):
sens = await sensor.new_sensor(moving_distance_config)
cg.add(ld2410_component.set_moving_target_distance_sensor(sens))
if still_distance_config := config.get(CONF_STILL_DISTANCE):
sens = await sensor.new_sensor(still_distance_config)
cg.add(ld2410_component.set_still_target_distance_sensor(sens))
if moving_energy_config := config.get(CONF_MOVING_ENERGY):
sens = await sensor.new_sensor(moving_energy_config)
cg.add(ld2410_component.set_moving_target_energy_sensor(sens))
if still_energy_config := config.get(CONF_STILL_ENERGY):
sens = await sensor.new_sensor(still_energy_config)
cg.add(ld2410_component.set_still_target_energy_sensor(sens))
if light_config := config.get(CONF_LIGHT):
sens = await sensor.new_sensor(light_config)
cg.add(ld2410_component.set_light_sensor(sens))
if detection_distance_config := config.get(CONF_DETECTION_DISTANCE):
sens = await sensor.new_sensor(detection_distance_config)
cg.add(ld2410_component.set_detection_distance_sensor(sens))
for x in range(9):
if gate_conf := config.get(f"g{x}"):
if move_config := gate_conf.get(CONF_MOVE_ENERGY):
sens = await sensor.new_sensor(move_config)
cg.add(hub.set_gate_move_sensor(x, sens))
cg.add(ld2410_component.set_gate_move_sensor(x, sens))
if still_config := gate_conf.get(CONF_STILL_ENERGY):
sens = await sensor.new_sensor(still_config)
cg.add(hub.set_gate_still_sensor(x, sens))
cg.add(ld2410_component.set_gate_still_sensor(x, sens))
+9 -4
View File
@@ -37,7 +37,12 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2410_ID])
switches = switch.sub_switches(config, parent=hub)
await switches(CONF_ENGINEERING_MODE, hub.set_engineering_mode_switch)
await switches(CONF_BLUETOOTH, hub.set_bluetooth_switch)
ld2410_component = await cg.get_variable(config[CONF_LD2410_ID])
if engineering_mode_config := config.get(CONF_ENGINEERING_MODE):
s = await switch.new_switch(engineering_mode_config)
await cg.register_parented(s, config[CONF_LD2410_ID])
cg.add(ld2410_component.set_engineering_mode_switch(s))
if bluetooth_config := config.get(CONF_BLUETOOTH):
s = await switch.new_switch(bluetooth_config)
await cg.register_parented(s, config[CONF_LD2410_ID])
cg.add(ld2410_component.set_bluetooth_switch(s))
+7 -4
View File
@@ -28,7 +28,10 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2410_ID])
text_sensors = text_sensor.sub_text_sensors(config)
await text_sensors(CONF_VERSION, hub.set_version_text_sensor)
await text_sensors(CONF_MAC_ADDRESS, hub.set_mac_text_sensor)
ld2410_component = await cg.get_variable(config[CONF_LD2410_ID])
if version_config := config.get(CONF_VERSION):
sens = await text_sensor.new_text_sensor(version_config)
cg.add(ld2410_component.set_version_text_sensor(sens))
if mac_address_config := config.get(CONF_MAC_ADDRESS):
sens = await text_sensor.new_text_sensor(mac_address_config)
cg.add(ld2410_component.set_mac_text_sensor(sens))
+21 -9
View File
@@ -50,12 +50,24 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2412_ID])
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(
CONF_DYNAMIC_BACKGROUND_CORRECTION_STATUS,
hub.set_dynamic_background_correction_status_binary_sensor,
)
await binary_sensors(CONF_HAS_TARGET, hub.set_target_binary_sensor)
await binary_sensors(CONF_HAS_MOVING_TARGET, hub.set_moving_target_binary_sensor)
await binary_sensors(CONF_HAS_STILL_TARGET, hub.set_still_target_binary_sensor)
LD2412_component = await cg.get_variable(config[CONF_LD2412_ID])
if dynamic_background_correction_status_config := config.get(
CONF_DYNAMIC_BACKGROUND_CORRECTION_STATUS
):
sens = await binary_sensor.new_binary_sensor(
dynamic_background_correction_status_config
)
cg.add(
LD2412_component.set_dynamic_background_correction_status_binary_sensor(
sens
)
)
if has_target_config := config.get(CONF_HAS_TARGET):
sens = await binary_sensor.new_binary_sensor(has_target_config)
cg.add(LD2412_component.set_target_binary_sensor(sens))
if has_moving_target_config := config.get(CONF_HAS_MOVING_TARGET):
sens = await binary_sensor.new_binary_sensor(has_moving_target_config)
cg.add(LD2412_component.set_moving_target_binary_sensor(sens))
if has_still_target_config := config.get(CONF_HAS_STILL_TARGET):
sens = await binary_sensor.new_binary_sensor(has_still_target_config)
cg.add(LD2412_component.set_still_target_binary_sensor(sens))
+19 -9
View File
@@ -56,12 +56,22 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2412_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(CONF_FACTORY_RESET, hub.set_factory_reset_button)
await buttons(CONF_QUERY_PARAMS, hub.set_query_button)
await buttons(CONF_RESTART, hub.set_restart_button)
await buttons(
CONF_START_DYNAMIC_BACKGROUND_CORRECTION,
hub.set_start_dynamic_background_correction_button,
)
LD2412_component = await cg.get_variable(config[CONF_LD2412_ID])
if factory_reset_config := config.get(CONF_FACTORY_RESET):
b = await button.new_button(factory_reset_config)
await cg.register_parented(b, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_factory_reset_button(b))
if query_params_config := config.get(CONF_QUERY_PARAMS):
b = await button.new_button(query_params_config)
await cg.register_parented(b, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_query_button(b))
if restart_config := config.get(CONF_RESTART):
b = await button.new_button(restart_config)
await cg.register_parented(b, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_restart_button(b))
if start_dynamic_background_correction_config := config.get(
CONF_START_DYNAMIC_BACKGROUND_CORRECTION
):
b = await button.new_button(start_dynamic_background_correction_config)
await cg.register_parented(b, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_start_dynamic_background_correction_button(b))
+35 -38
View File
@@ -87,45 +87,42 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2412_ID])
numbers = number.sub_numbers(config, parent=hub)
await numbers(
CONF_LIGHT_THRESHOLD,
hub.set_light_threshold_number,
min_value=0,
max_value=255,
step=1,
)
await numbers(
CONF_MAX_DISTANCE_GATE,
hub.set_max_distance_gate_number,
min_value=2,
max_value=13,
step=1,
)
await numbers(
CONF_MIN_DISTANCE_GATE,
hub.set_min_distance_gate_number,
min_value=1,
max_value=12,
step=1,
)
LD2412_component = await cg.get_variable(config[CONF_LD2412_ID])
if light_threshold_config := config.get(CONF_LIGHT_THRESHOLD):
n = await number.new_number(
light_threshold_config, min_value=0, max_value=255, step=1
)
await cg.register_parented(n, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_light_threshold_number(n))
if max_distance_gate_config := config.get(CONF_MAX_DISTANCE_GATE):
n = await number.new_number(
max_distance_gate_config, min_value=2, max_value=13, step=1
)
await cg.register_parented(n, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_max_distance_gate_number(n))
if min_distance_gate_config := config.get(CONF_MIN_DISTANCE_GATE):
n = await number.new_number(
min_distance_gate_config, min_value=1, max_value=12, step=1
)
await cg.register_parented(n, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_min_distance_gate_number(n))
for x in range(14):
if gate_conf := config.get(f"gate_{x}"):
move_config = gate_conf[CONF_MOVE_THRESHOLD]
n = await number.new_number(move_config, min_value=0, max_value=100, step=1)
await cg.register_parented(n, hub)
cg.add(hub.set_gate_move_threshold_number(x, n))
still_config = gate_conf[CONF_STILL_THRESHOLD]
n = await number.new_number(
still_config, min_value=0, max_value=100, step=1
n = cg.new_Pvariable(move_config[CONF_ID])
await number.register_number(
n, move_config, min_value=0, max_value=100, step=1
)
await cg.register_parented(n, hub)
cg.add(hub.set_gate_still_threshold_number(x, n))
await numbers(
CONF_TIMEOUT,
hub.set_timeout_number,
min_value=0,
max_value=900,
step=1,
)
await cg.register_parented(n, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_gate_move_threshold_number(x, n))
still_config = gate_conf[CONF_STILL_THRESHOLD]
n = cg.new_Pvariable(still_config[CONF_ID])
await number.register_number(
n, still_config, min_value=0, max_value=100, step=1
)
await cg.register_parented(n, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_gate_still_threshold_number(x, n))
if timeout_config := config.get(CONF_TIMEOUT):
n = await number.new_number(timeout_config, min_value=0, max_value=900, step=1)
await cg.register_parented(n, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_timeout_number(n))
+33 -31
View File
@@ -50,34 +50,36 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2412_ID])
selects = select.sub_selects(config, parent=hub)
await selects(
CONF_BAUD_RATE,
hub.set_baud_rate_select,
options=[
"9600",
"19200",
"38400",
"57600",
"115200",
"230400",
"256000",
"460800",
],
)
await selects(
CONF_DISTANCE_RESOLUTION,
hub.set_distance_resolution_select,
options=["0.2m", "0.5m", "0.75m"],
)
await selects(
CONF_LIGHT_FUNCTION,
hub.set_light_function_select,
options=["off", "below", "above"],
)
await selects(
CONF_OUT_PIN_LEVEL,
hub.set_out_pin_level_select,
options=["low", "high"],
)
LD2412_component = await cg.get_variable(config[CONF_LD2412_ID])
if baud_rate_config := config.get(CONF_BAUD_RATE):
s = await select.new_select(
baud_rate_config,
options=[
"9600",
"19200",
"38400",
"57600",
"115200",
"230400",
"256000",
"460800",
],
)
await cg.register_parented(s, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_baud_rate_select(s))
if distance_resolution_config := config.get(CONF_DISTANCE_RESOLUTION):
s = await select.new_select(
distance_resolution_config, options=["0.2m", "0.5m", "0.75m"]
)
await cg.register_parented(s, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_distance_resolution_select(s))
if light_function_config := config.get(CONF_LIGHT_FUNCTION):
s = await select.new_select(
light_function_config, options=["off", "below", "above"]
)
await cg.register_parented(s, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_light_function_select(s))
if out_pin_level_config := config.get(CONF_OUT_PIN_LEVEL):
s = await select.new_select(out_pin_level_config, options=["low", "high"])
await cg.register_parented(s, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_out_pin_level_select(s))
+21 -10
View File
@@ -158,19 +158,30 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2412_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_DETECTION_DISTANCE, hub.set_detection_distance_sensor)
await sensors(CONF_LIGHT, hub.set_light_sensor)
await sensors(CONF_MOVING_DISTANCE, hub.set_moving_target_distance_sensor)
await sensors(CONF_MOVING_ENERGY, hub.set_moving_target_energy_sensor)
await sensors(CONF_STILL_DISTANCE, hub.set_still_target_distance_sensor)
await sensors(CONF_STILL_ENERGY, hub.set_still_target_energy_sensor)
LD2412_component = await cg.get_variable(config[CONF_LD2412_ID])
if detection_distance_config := config.get(CONF_DETECTION_DISTANCE):
sens = await sensor.new_sensor(detection_distance_config)
cg.add(LD2412_component.set_detection_distance_sensor(sens))
if light_config := config.get(CONF_LIGHT):
sens = await sensor.new_sensor(light_config)
cg.add(LD2412_component.set_light_sensor(sens))
if moving_distance_config := config.get(CONF_MOVING_DISTANCE):
sens = await sensor.new_sensor(moving_distance_config)
cg.add(LD2412_component.set_moving_target_distance_sensor(sens))
if moving_energy_config := config.get(CONF_MOVING_ENERGY):
sens = await sensor.new_sensor(moving_energy_config)
cg.add(LD2412_component.set_moving_target_energy_sensor(sens))
if still_distance_config := config.get(CONF_STILL_DISTANCE):
sens = await sensor.new_sensor(still_distance_config)
cg.add(LD2412_component.set_still_target_distance_sensor(sens))
if still_energy_config := config.get(CONF_STILL_ENERGY):
sens = await sensor.new_sensor(still_energy_config)
cg.add(LD2412_component.set_still_target_energy_sensor(sens))
for x in range(14):
if gate_conf := config.get(f"gate_{x}"):
if move_config := gate_conf.get(CONF_MOVE_ENERGY):
sens = await sensor.new_sensor(move_config)
cg.add(hub.set_gate_move_sensor(x, sens))
cg.add(LD2412_component.set_gate_move_sensor(x, sens))
if still_config := gate_conf.get(CONF_STILL_ENERGY):
sens = await sensor.new_sensor(still_config)
cg.add(hub.set_gate_still_sensor(x, sens))
cg.add(LD2412_component.set_gate_still_sensor(x, sens))
+9 -4
View File
@@ -37,7 +37,12 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2412_ID])
switches = switch.sub_switches(config, parent=hub)
await switches(CONF_BLUETOOTH, hub.set_bluetooth_switch)
await switches(CONF_ENGINEERING_MODE, hub.set_engineering_mode_switch)
LD2412_component = await cg.get_variable(config[CONF_LD2412_ID])
if bluetooth_config := config.get(CONF_BLUETOOTH):
s = await switch.new_switch(bluetooth_config)
await cg.register_parented(s, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_bluetooth_switch(s))
if engineering_mode_config := config.get(CONF_ENGINEERING_MODE):
s = await switch.new_switch(engineering_mode_config)
await cg.register_parented(s, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_engineering_mode_switch(s))
+7 -4
View File
@@ -28,7 +28,10 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2412_ID])
text_sensors = text_sensor.sub_text_sensors(config)
await text_sensors(CONF_VERSION, hub.set_version_text_sensor)
await text_sensors(CONF_MAC_ADDRESS, hub.set_mac_text_sensor)
LD2412_component = await cg.get_variable(config[CONF_LD2412_ID])
if version_config := config.get(CONF_VERSION):
sens = await text_sensor.new_text_sensor(version_config)
cg.add(LD2412_component.set_version_text_sensor(sens))
if mac_address_config := config.get(CONF_MAC_ADDRESS):
sens = await text_sensor.new_text_sensor(mac_address_config)
cg.add(LD2412_component.set_mac_text_sensor(sens))
@@ -27,7 +27,8 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(CONF_HAS_TARGET, var.set_presence_sensor)
hub = await cg.get_variable(config[CONF_LD2420_ID])
cg.add(hub.register_listener(var))
if CONF_HAS_TARGET in config:
sens = await binary_sensor.new_binary_sensor(config[CONF_HAS_TARGET])
cg.add(var.set_presence_sensor(sens))
ld2420 = await cg.get_variable(config[CONF_LD2420_ID])
cg.add(ld2420.register_listener(var))
+17 -6
View File
@@ -52,9 +52,20 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2420_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(CONF_APPLY_CONFIG, hub.set_apply_config_button)
await buttons(CONF_REVERT_CONFIG, hub.set_revert_config_button)
await buttons(CONF_RESTART_MODULE, hub.set_restart_module_button)
await buttons(CONF_FACTORY_RESET, hub.set_factory_reset_button)
ld2420_component = await cg.get_variable(config[CONF_LD2420_ID])
if apply_config := config.get(CONF_APPLY_CONFIG):
b = await button.new_button(apply_config)
await cg.register_parented(b, config[CONF_LD2420_ID])
cg.add(ld2420_component.set_apply_config_button(b))
if revert_config := config.get(CONF_REVERT_CONFIG):
b = await button.new_button(revert_config)
await cg.register_parented(b, config[CONF_LD2420_ID])
cg.add(ld2420_component.set_revert_config_button(b))
if restart_config := config.get(CONF_RESTART_MODULE):
b = await button.new_button(restart_config)
await cg.register_parented(b, config[CONF_LD2420_ID])
cg.add(ld2420_component.set_restart_module_button(b))
if factory_reset := config.get(CONF_FACTORY_RESET):
b = await button.new_button(factory_reset)
await cg.register_parented(b, config[CONF_LD2420_ID])
cg.add(ld2420_component.set_factory_reset_button(b))
+43 -52
View File
@@ -115,65 +115,56 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2420_ID])
numbers = number.sub_numbers(config, parent=hub)
await numbers(
CONF_PRESENCE_TIMEOUT,
hub.set_gate_timeout_number,
min_value=0,
max_value=255,
step=5,
)
await numbers(
CONF_MIN_GATE_DISTANCE,
hub.set_min_gate_distance_number,
min_value=0,
max_value=15,
step=1,
)
await numbers(
CONF_MAX_GATE_DISTANCE,
hub.set_max_gate_distance_number,
min_value=1,
max_value=15,
step=1,
)
await numbers(
CONF_GATE_MOVE_SENSITIVITY,
hub.set_gate_move_sensitivity_factor_number,
min_value=0.05,
max_value=1,
step=0.025,
)
await numbers(
CONF_GATE_STILL_SENSITIVITY,
hub.set_gate_still_sensitivity_factor_number,
min_value=0.05,
max_value=1,
step=0.025,
)
if config.get(CONF_GATE_SELECT):
await numbers(
CONF_GATE_SELECT,
hub.set_gate_select_number,
min_value=0,
max_value=15,
step=1,
LD2420_component = await cg.get_variable(config[CONF_LD2420_ID])
if gate_timeout_config := config.get(CONF_PRESENCE_TIMEOUT):
n = await number.new_number(
gate_timeout_config, min_value=0, max_value=255, step=5
)
await cg.register_parented(n, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_gate_timeout_number(n))
if min_distance_gate_config := config.get(CONF_MIN_GATE_DISTANCE):
n = await number.new_number(
min_distance_gate_config, min_value=0, max_value=15, step=1
)
await cg.register_parented(n, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_min_gate_distance_number(n))
if max_distance_gate_config := config.get(CONF_MAX_GATE_DISTANCE):
n = await number.new_number(
max_distance_gate_config, min_value=1, max_value=15, step=1
)
await cg.register_parented(n, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_max_gate_distance_number(n))
if gate_move_sensitivity_config := config.get(CONF_GATE_MOVE_SENSITIVITY):
n = await number.new_number(
gate_move_sensitivity_config, min_value=0.05, max_value=1, step=0.025
)
await cg.register_parented(n, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_gate_move_sensitivity_factor_number(n))
if gate_still_sensitivity_config := config.get(CONF_GATE_STILL_SENSITIVITY):
n = await number.new_number(
gate_still_sensitivity_config, min_value=0.05, max_value=1, step=0.025
)
await cg.register_parented(n, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_gate_still_sensitivity_factor_number(n))
if config.get(CONF_GATE_SELECT):
if gate_number := config.get(CONF_GATE_SELECT):
n = await number.new_number(gate_number, min_value=0, max_value=15, step=1)
await cg.register_parented(n, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_gate_select_number(n))
if gate_still_threshold := config.get(CONF_STILL_THRESHOLD):
n = cg.new_Pvariable(gate_still_threshold[CONF_ID])
await number.register_number(
n, gate_still_threshold, min_value=0, max_value=65535, step=25
)
await cg.register_parented(n, hub)
cg.add(hub.set_gate_still_threshold_numbers(0, n))
await cg.register_parented(n, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_gate_still_threshold_numbers(0, n))
if gate_move_threshold := config.get(CONF_MOVE_THRESHOLD):
n = cg.new_Pvariable(gate_move_threshold[CONF_ID])
await number.register_number(
n, gate_move_threshold, min_value=0, max_value=65535, step=25
)
await cg.register_parented(n, hub)
cg.add(hub.set_gate_move_threshold_numbers(0, n))
await cg.register_parented(n, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_gate_move_threshold_numbers(0, n))
else:
for x in range(16):
if gate_conf := config.get(f"gate_{x}"):
@@ -182,13 +173,13 @@ async def to_code(config: ConfigType) -> None:
await number.register_number(
n, move_config, min_value=0, max_value=65535, step=25
)
await cg.register_parented(n, hub)
cg.add(hub.set_gate_move_threshold_numbers(x, n))
await cg.register_parented(n, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_gate_move_threshold_numbers(x, n))
still_config = gate_conf[CONF_STILL_THRESHOLD]
n = cg.new_Pvariable(still_config[CONF_ID], x)
await number.register_number(
n, still_config, min_value=0, max_value=65535, step=25
)
await cg.register_parented(n, hub)
cg.add(hub.set_gate_still_threshold_numbers(x, n))
await cg.register_parented(n, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_gate_still_threshold_numbers(x, n))
+8 -5
View File
@@ -25,8 +25,11 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2420_ID])
selects = select.sub_selects(config, parent=hub)
await selects(
CONF_OPERATING_MODE, hub.set_operating_mode_select, options=CONF_SELECTS
)
LD2420_component = await cg.get_variable(config[CONF_LD2420_ID])
if operating_mode_config := config.get(CONF_OPERATING_MODE):
sel = await select.new_select(
operating_mode_config,
options=CONF_SELECTS,
)
await cg.register_parented(sel, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_operating_mode_select(sel))
+8 -5
View File
@@ -34,8 +34,11 @@ CONFIG_SCHEMA = cv.All(
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_MOVING_DISTANCE, var.set_distance_sensor)
await sensors(CONF_GATE_ENERGY, var.set_energy_sensor)
hub = await cg.get_variable(config[CONF_LD2420_ID])
cg.add(hub.register_listener(var))
if CONF_MOVING_DISTANCE in config:
sens = await sensor.new_sensor(config[CONF_MOVING_DISTANCE])
cg.add(var.set_distance_sensor(sens))
if CONF_GATE_ENERGY in config:
sens = await sensor.new_sensor(config[CONF_GATE_ENERGY])
cg.add(var.set_energy_sensor(sens))
ld2420 = await cg.get_variable(config[CONF_LD2420_ID])
cg.add(ld2420.register_listener(var))
@@ -28,7 +28,8 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
text_sensors = text_sensor.sub_text_sensors(config)
await text_sensors(CONF_FW_VERSION, var.set_fw_version_text_sensor)
hub = await cg.get_variable(config[CONF_LD2420_ID])
cg.add(hub.register_listener(var))
if CONF_FW_VERSION in config:
sens = await text_sensor.new_text_sensor(config[CONF_FW_VERSION])
cg.add(var.set_fw_version_text_sensor(sens))
ld2420 = await cg.get_variable(config[CONF_LD2420_ID])
cg.add(ld2420.register_listener(var))
+10 -5
View File
@@ -41,8 +41,13 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2450_ID])
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(CONF_HAS_TARGET, hub.set_target_binary_sensor)
await binary_sensors(CONF_HAS_MOVING_TARGET, hub.set_moving_target_binary_sensor)
await binary_sensors(CONF_HAS_STILL_TARGET, hub.set_still_target_binary_sensor)
ld2450_component = await cg.get_variable(config[CONF_LD2450_ID])
if has_target_config := config.get(CONF_HAS_TARGET):
sens = await binary_sensor.new_binary_sensor(has_target_config)
cg.add(ld2450_component.set_target_binary_sensor(sens))
if has_moving_target_config := config.get(CONF_HAS_MOVING_TARGET):
sens = await binary_sensor.new_binary_sensor(has_moving_target_config)
cg.add(ld2450_component.set_moving_target_binary_sensor(sens))
if has_still_target_config := config.get(CONF_HAS_STILL_TARGET):
sens = await binary_sensor.new_binary_sensor(has_still_target_config)
cg.add(ld2450_component.set_still_target_binary_sensor(sens))
+9 -4
View File
@@ -37,7 +37,12 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2450_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(CONF_FACTORY_RESET, hub.set_factory_reset_button)
await buttons(CONF_RESTART, hub.set_restart_button)
ld2450_component = await cg.get_variable(config[CONF_LD2450_ID])
if factory_reset_config := config.get(CONF_FACTORY_RESET):
b = await button.new_button(factory_reset_config)
await cg.register_parented(b, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_factory_reset_button(b))
if restart_config := config.get(CONF_RESTART):
b = await button.new_button(restart_config)
await cg.register_parented(b, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_restart_button(b))
+15 -14
View File
@@ -80,15 +80,16 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2450_ID])
numbers = number.sub_numbers(config, parent=hub)
await numbers(
CONF_PRESENCE_TIMEOUT,
hub.set_presence_timeout_number,
min_value=0,
max_value=3600,
step=1,
)
ld2450_component = await cg.get_variable(config[CONF_LD2450_ID])
if presence_timeout_config := config.get(CONF_PRESENCE_TIMEOUT):
n = await number.new_number(
presence_timeout_config,
min_value=0,
max_value=3600,
step=1,
)
await cg.register_parented(n, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_presence_timeout_number(n))
for zone_num in range(MAX_ZONES):
if zone_conf := config.get(f"zone_{zone_num + 1}"):
zone_x1_config = zone_conf.get(CONF_X1)
@@ -96,27 +97,27 @@ async def to_code(config: ConfigType) -> None:
await number.register_number(
x1, zone_x1_config, min_value=-4860, max_value=4860, step=1
)
await cg.register_parented(x1, hub)
await cg.register_parented(x1, config[CONF_LD2450_ID])
zone_y1_config = zone_conf.get(CONF_Y1)
y1 = cg.new_Pvariable(zone_y1_config[CONF_ID], zone_num)
await number.register_number(
y1, zone_y1_config, min_value=0, max_value=7560, step=1
)
await cg.register_parented(y1, hub)
await cg.register_parented(y1, config[CONF_LD2450_ID])
zone_x2_config = zone_conf.get(CONF_X2)
x2 = cg.new_Pvariable(zone_x2_config[CONF_ID], zone_num)
await number.register_number(
x2, zone_x2_config, min_value=-4860, max_value=4860, step=1
)
await cg.register_parented(x2, hub)
await cg.register_parented(x2, config[CONF_LD2450_ID])
zone_y2_config = zone_conf.get(CONF_Y2)
y2 = cg.new_Pvariable(zone_y2_config[CONF_ID], zone_num)
await number.register_number(
y2, zone_y2_config, min_value=0, max_value=7560, step=1
)
await cg.register_parented(y2, hub)
await cg.register_parented(y2, config[CONF_LD2450_ID])
cg.add(hub.set_zone_numbers(zone_num, x1, y1, x2, y2))
cg.add(ld2450_component.set_zone_numbers(zone_num, x1, y1, x2, y2))
+28 -25
View File
@@ -33,28 +33,31 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2450_ID])
selects = select.sub_selects(config, parent=hub)
await selects(
CONF_BAUD_RATE,
hub.set_baud_rate_select,
options=[
"9600",
"19200",
"38400",
"57600",
"115200",
"230400",
"256000",
"460800",
],
)
await selects(
CONF_ZONE_TYPE,
hub.set_zone_type_select,
options=[
"Disabled",
"Detection",
"Filter",
],
)
ld2450_component = await cg.get_variable(config[CONF_LD2450_ID])
if baud_rate_config := config.get(CONF_BAUD_RATE):
s = await select.new_select(
baud_rate_config,
options=[
"9600",
"19200",
"38400",
"57600",
"115200",
"230400",
"256000",
"460800",
],
)
await cg.register_parented(s, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_baud_rate_select(s))
if zone_type_config := config.get(CONF_ZONE_TYPE):
s = await select.new_select(
zone_type_config,
options=[
"Disabled",
"Detection",
"Filter",
],
)
await cg.register_parented(s, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_zone_type_select(s))
+21 -14
View File
@@ -228,40 +228,47 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2450_ID])
ld2450_component = await cg.get_variable(config[CONF_LD2450_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_TARGET_COUNT, hub.set_target_count_sensor)
await sensors(CONF_STILL_TARGET_COUNT, hub.set_still_target_count_sensor)
await sensors(CONF_MOVING_TARGET_COUNT, hub.set_moving_target_count_sensor)
if target_count_config := config.get(CONF_TARGET_COUNT):
sens = await sensor.new_sensor(target_count_config)
cg.add(ld2450_component.set_target_count_sensor(sens))
if still_target_count_config := config.get(CONF_STILL_TARGET_COUNT):
sens = await sensor.new_sensor(still_target_count_config)
cg.add(ld2450_component.set_still_target_count_sensor(sens))
if moving_target_count_config := config.get(CONF_MOVING_TARGET_COUNT):
sens = await sensor.new_sensor(moving_target_count_config)
cg.add(ld2450_component.set_moving_target_count_sensor(sens))
for n in range(MAX_TARGETS):
if target_conf := config.get(f"target_{n + 1}"):
if x_config := target_conf.get(CONF_X):
sens = await sensor.new_sensor(x_config)
cg.add(hub.set_move_x_sensor(n, sens))
cg.add(ld2450_component.set_move_x_sensor(n, sens))
if y_config := target_conf.get(CONF_Y):
sens = await sensor.new_sensor(y_config)
cg.add(hub.set_move_y_sensor(n, sens))
cg.add(ld2450_component.set_move_y_sensor(n, sens))
if speed_config := target_conf.get(CONF_SPEED):
sens = await sensor.new_sensor(speed_config)
cg.add(hub.set_move_speed_sensor(n, sens))
cg.add(ld2450_component.set_move_speed_sensor(n, sens))
if angle_config := target_conf.get(CONF_ANGLE):
sens = await sensor.new_sensor(angle_config)
cg.add(hub.set_move_angle_sensor(n, sens))
cg.add(ld2450_component.set_move_angle_sensor(n, sens))
if distance_config := target_conf.get(CONF_DISTANCE):
sens = await sensor.new_sensor(distance_config)
cg.add(hub.set_move_distance_sensor(n, sens))
cg.add(ld2450_component.set_move_distance_sensor(n, sens))
if resolution_config := target_conf.get(CONF_RESOLUTION):
sens = await sensor.new_sensor(resolution_config)
cg.add(hub.set_move_resolution_sensor(n, sens))
cg.add(ld2450_component.set_move_resolution_sensor(n, sens))
for n in range(MAX_ZONES):
if zone_config := config.get(f"zone_{n + 1}"):
if target_count_config := zone_config.get(CONF_TARGET_COUNT):
sens = await sensor.new_sensor(target_count_config)
cg.add(hub.set_zone_target_count_sensor(n, sens))
cg.add(ld2450_component.set_zone_target_count_sensor(n, sens))
if still_target_count_config := zone_config.get(CONF_STILL_TARGET_COUNT):
sens = await sensor.new_sensor(still_target_count_config)
cg.add(hub.set_zone_still_target_count_sensor(n, sens))
cg.add(ld2450_component.set_zone_still_target_count_sensor(n, sens))
if moving_target_count_config := zone_config.get(CONF_MOVING_TARGET_COUNT):
sens = await sensor.new_sensor(moving_target_count_config)
cg.add(hub.set_zone_moving_target_count_sensor(n, sens))
cg.add(ld2450_component.set_zone_moving_target_count_sensor(n, sens))
+9 -4
View File
@@ -37,7 +37,12 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2450_ID])
switches = switch.sub_switches(config, parent=hub)
await switches(CONF_BLUETOOTH, hub.set_bluetooth_switch)
await switches(CONF_MULTI_TARGET, hub.set_multi_target_switch)
ld2450_component = await cg.get_variable(config[CONF_LD2450_ID])
if bluetooth_config := config.get(CONF_BLUETOOTH):
s = await switch.new_switch(bluetooth_config)
await cg.register_parented(s, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_bluetooth_switch(s))
if multi_target_config := config.get(CONF_MULTI_TARGET):
s = await switch.new_switch(multi_target_config)
await cg.register_parented(s, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_multi_target_switch(s))
+8 -5
View File
@@ -51,13 +51,16 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2450_ID])
text_sensors = text_sensor.sub_text_sensors(config)
await text_sensors(CONF_VERSION, hub.set_version_text_sensor)
await text_sensors(CONF_MAC_ADDRESS, hub.set_mac_text_sensor)
ld2450_component = await cg.get_variable(config[CONF_LD2450_ID])
if version_config := config.get(CONF_VERSION):
sens = await text_sensor.new_text_sensor(version_config)
cg.add(ld2450_component.set_version_text_sensor(sens))
if mac_address_config := config.get(CONF_MAC_ADDRESS):
sens = await text_sensor.new_text_sensor(mac_address_config)
cg.add(ld2450_component.set_mac_text_sensor(sens))
for n in range(MAX_TARGETS):
if (direction_conf := config.get(f"target_{n + 1}")) and (
direction_config := direction_conf.get(CONF_DIRECTION)
):
sens = await text_sensor.new_text_sensor(direction_config)
cg.add(hub.set_direction_text_sensor(n, sens))
cg.add(ld2450_component.set_direction_text_sensor(n, sens))
+2 -4
View File
@@ -2,7 +2,6 @@ import json
import logging
from pathlib import Path
from esphome.build_helpers.pch import pch_extra_scripts
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
@@ -27,7 +26,7 @@ from esphome.const import (
from esphome.core import CORE
from esphome.core.config import BOARD_MAX_LENGTH
from esphome.helpers import copy_file_if_changed
from esphome.platformio.toolchain import copy_ccache_script, copy_pch_script
from esphome.platformio.toolchain import copy_ccache_script
from esphome.storage_json import StorageJSON
from . import gpio # noqa: F401
@@ -516,7 +515,7 @@ async def component_to_code(config):
cg.add_platformio_option("build_src_flags", build_src_flags)
# Must run before the platform's builder scripts are loaded; see the script.
cg.add_platformio_option("extra_scripts", ["pre:scons_dont_inherit.py"])
cg.add_platformio_option("extra_scripts", ["pre:ccache.py", *pch_extra_scripts()])
cg.add_platformio_option("extra_scripts", ["pre:ccache.py"])
# IRAM_ATTR is a no-op on BK72xx (SDK masks FIQ+IRQ around flash ops).
# On other families, patch_linker.py routes .sram.text into the right
# RAM-executable output section and prints a post-link placement summary.
@@ -626,4 +625,3 @@ def copy_files() -> None:
CORE.relative_build_path("scons_dont_inherit.py"),
)
copy_ccache_script()
copy_pch_script()
+10 -29
View File
@@ -70,7 +70,6 @@ from .types import ( # noqa: F401
AddressableLightState,
ChannelColors,
ColorMode,
GammaTable,
LightOutput,
LightState,
LightStateRTCState,
@@ -86,8 +85,6 @@ CODEOWNERS = ["@esphome/core"]
IS_PLATFORM_COMPONENT = True
DOMAIN = "light"
CONF_GAMMA_TABLE_ID = "gamma_table_id"
CONF_RESUME_EFFECT = "resume_effect"
@dataclass
@@ -143,29 +140,18 @@ def generate_gamma_table(gamma_correct: float) -> list[HexInt]:
return [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)]
def gamma_table_initializer(gamma_correct: float) -> str:
"""C++ initializer for a light::GammaTable: the lookup table, then gamma * 100."""
lut = ", ".join(f"0x{int(v):04X}" for v in generate_gamma_table(gamma_correct))
# gamma_x100 is a uint16_t; platforms that redefine gamma_correct leave it unbounded, so saturate here
return f"{{{{{lut}}}, {min(0xFFFF, round(gamma_correct * 100))}}}"
def _get_or_create_gamma_table(gamma_correct: float, table_id: ID) -> cg.RawExpression:
def _get_or_create_gamma_table(gamma_correct):
data = _get_data()
if gamma_correct in data.gamma_tables:
return data.gamma_tables[gamma_correct]
# table_id is generated and resolved against every declared ID, so it can't collide with a
# YAML ID; lights sharing a gamma reuse the first light's table.
cg.add(
cg.RawStatement(
f"static constexpr light::GammaTable {table_id} PROGMEM = "
f"{gamma_table_initializer(gamma_correct)};"
)
)
table = cg.RawExpression(f"&{table_id}")
data.gamma_tables[gamma_correct] = table
return table
forward = generate_gamma_table(gamma_correct)
gamma_str = f"{gamma_correct}".replace(".", "_")
fwd_id = ID(f"gamma_{gamma_str}_fwd", is_declaration=True, type=cg.uint16)
fwd_arr = cg.progmem_array(fwd_id, forward)
data.gamma_tables[gamma_correct] = fwd_arr
return fwd_arr
def find_effect_index(effects: list, effect_name: str) -> int | None:
@@ -371,7 +357,6 @@ LIGHT_SCHEMA = (
.extend(
{
cv.GenerateID(): cv.declare_id(LightState),
cv.GenerateID(CONF_GAMMA_TABLE_ID): cv.declare_id(GammaTable),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt.MQTTJSONLightComponent
),
@@ -395,7 +380,6 @@ LIGHT_SCHEMA = (
}
),
cv.Optional(CONF_INITIAL_STATE): LIGHT_STATE_SCHEMA,
cv.Optional(CONF_RESUME_EFFECT, default=False): cv.boolean,
}
)
)
@@ -526,9 +510,6 @@ async def setup_light_core_(light_var, config, output_var):
# runtime actually understands.
initial_state_config = config.get(CONF_INITIAL_STATE)
initial_statements = await _initial_state_statements(initial_state_config)
if config[CONF_RESUME_EFFECT]:
cg.add_define("USE_LIGHT_RESUME_EFFECT")
cg.add(light_var.set_resume_effect(True))
restore_mode = config.get(CONF_RESTORE_MODE)
restore_state_config = config.get(CONF_RESTORE_STATE)
@@ -572,14 +553,14 @@ async def setup_light_core_(light_var, config, output_var):
) is not None and flash_transition_length != cv.time_period(
DEFAULT_FLASH_TRANSITION_LENGTH
):
cg.add_define("USE_LIGHT_FLASH_TRANSITION_LENGTH")
cg.add(light_var.set_flash_transition_length(flash_transition_length))
# Setting an interval opts this light in and compiles the feature in
if (interval := config.get(CONF_TRANSITION_STATE_PUBLISH_INTERVAL)) is not None:
cg.add(light_var.set_transition_state_publish_interval(interval))
cg.add_define("USE_LIGHT_TRANSITION_PUBLISH_INTERVAL")
if (gamma_correct := config.get(CONF_GAMMA_CORRECT)) is not None:
fwd_arr = _get_or_create_gamma_table(gamma_correct, config[CONF_GAMMA_TABLE_ID])
cg.add(light_var.set_gamma_correct(gamma_correct))
fwd_arr = _get_or_create_gamma_table(gamma_correct)
cg.add(light_var.set_gamma_table(fwd_arr))
cg.add_define("USE_LIGHT_GAMMA_LUT")
effects = await cg.build_registry_list(
-6
View File
@@ -545,18 +545,12 @@ async def addressable_flicker_effect_to_code(config, effect_id):
return var
# LightState stores the active effect index in a uint16_t
MAX_EFFECTS = 65535
def validate_effects(allowed_effects):
@schema_extractor("effects")
def validator(value):
if value == SCHEMA_EXTRACT:
return (allowed_effects, EFFECTS_REGISTRY)
if isinstance(value, list) and len(value) > MAX_EFFECTS:
raise cv.Invalid(f"A light supports at most {MAX_EFFECTS} effects")
value = cv.validate_registry("effect", EFFECTS_REGISTRY)(value)
errors = []
names = set()
@@ -5,12 +5,6 @@
namespace esphome::light {
/// A gamma curve as codegen emits it into PROGMEM: the lookup table and the gamma it was built from
struct GammaTable {
uint16_t lut[256];
uint16_t gamma_x100;
};
/// Binary search a monotonically increasing uint16[256] PROGMEM table.
/// Returns the largest index where table[index] <= target.
inline uint8_t gamma_table_reverse_search(const uint16_t *table, uint16_t target) {
-19
View File
@@ -193,11 +193,6 @@ LightColorValues LightCall::validate_() {
auto *name = this->parent_->get_name().c_str();
auto traits = this->parent_->get_traits();
#ifdef USE_LIGHT_RESUME_EFFECT
// Snapshot before the adjustments below add flags of their own
const bool plain_turn_on = this->has_state() && this->state_ && (this->flags_ & ~STATE_ONLY_FLAGS_MASK) == 0;
#endif // USE_LIGHT_RESUME_EFFECT
// Color mode check
if (this->has_color_mode() && !traits.supports_color_mode(this->color_mode_)) {
ESP_LOGW(TAG, "'%s' does not support color mode %s", name, LOG_STR_ARG(color_mode_to_human(this->color_mode_)));
@@ -339,15 +334,6 @@ LightColorValues LightCall::validate_() {
// validate transition length/flash length/effect not used at the same time
bool supports_transition = color_mode & ColorCapability::BRIGHTNESS;
#ifdef USE_LIGHT_RESUME_EFFECT
// A plain turn-on from off brings back the effect that was running when the light was turned off
if (this->parent_->resume_effect_ && plain_turn_on && !this->parent_->remote_values.is_on() &&
this->parent_->previous_effect_index_ != 0) {
this->effect_ = this->parent_->previous_effect_index_;
this->set_flag_(FLAG_HAS_EFFECT);
}
#endif // USE_LIGHT_RESUME_EFFECT
// If effect is already active, remove effect start
if (this->has_effect_() && this->effect_ == this->parent_->active_effect_index_) {
this->clear_flag_(FLAG_HAS_EFFECT);
@@ -390,11 +376,6 @@ LightColorValues LightCall::validate_() {
// Reason: When user turns off the light in frontend, the effect should also stop
bool target_state = this->has_state() ? this->state_ : v.is_on();
if (!this->has_flash_() && !target_state) {
#ifdef USE_LIGHT_RESUME_EFFECT
// Remember what was running, including no effect, when a lit light is explicitly turned off
if (this->parent_->resume_effect_ && explicit_turn_off_request && this->parent_->remote_values.is_on())
this->parent_->previous_effect_index_ = this->parent_->active_effect_index_;
#endif // USE_LIGHT_RESUME_EFFECT
if (this->has_effect_()) {
log_invalid_parameter(name, LOG_STR("cannot start effect when turning off"));
this->clear_flag_(FLAG_HAS_EFFECT);
-2
View File
@@ -215,8 +215,6 @@ class LightCall {
FLAG_SAVE = 1 << 15,
};
static constexpr uint16_t CLAMP_FLAGS_MASK = 0x00FFu; // bits 0-7
// Flags a plain turn-on may carry; any other flag means the caller asked for something specific
static constexpr uint16_t STATE_ONLY_FLAGS_MASK = FLAG_HAS_STATE | FLAG_PUBLISH | FLAG_SAVE;
inline bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; }
inline bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; }
+10 -30
View File
@@ -9,8 +9,6 @@
#include "light_output.h"
#include "transformers.h"
#include <limits>
namespace esphome::light {
static const char *const TAG = "light";
@@ -96,17 +94,10 @@ void LightState::dump_config() {
ESP_LOGCONFIG(TAG, "Light '%s'", this->get_name().c_str());
auto traits = this->get_traits();
if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) {
#ifdef USE_LIGHT_GAMMA_LUT
// Read the stored gamma * 100 directly so dump_config does not pull in get_gamma_correct()
const unsigned gamma_x100 =
this->gamma_table_ != nullptr ? progmem_read_uint16(&this->gamma_table_->gamma_x100) : 0;
#else
const unsigned gamma_x100 = 0;
#endif
ESP_LOGCONFIG(TAG,
" Default Transition Length: %.1fs\n"
" Gamma Correct: %u.%02u",
this->default_transition_length_ / 1e3f, gamma_x100 / 100, gamma_x100 % 100);
" Gamma Correct: %.2f",
this->default_transition_length_ / 1e3f, this->gamma_correct_);
#ifdef USE_LIGHT_TRANSITION_PUBLISH_INTERVAL
// The define is build wide; only lights that set the option have an interval
if (this->transition_state_publish_interval_ != 0) {
@@ -305,14 +296,6 @@ void LightState::current_values_as_ct(float *color_temperature, float *white_bri
*white_brightness = this->gamma_correct_lut(*white_brightness);
}
float LightState::get_gamma_correct() const {
#ifdef USE_LIGHT_GAMMA_LUT
if (this->gamma_table_ != nullptr)
return progmem_read_uint16(&this->gamma_table_->gamma_x100) * 0.01f;
#endif // USE_LIGHT_GAMMA_LUT
return 0.0f;
}
#ifdef USE_LIGHT_GAMMA_LUT
float LightState::gamma_correct_lut(float value) const {
if (value <= 0.0f)
@@ -324,10 +307,10 @@ float LightState::gamma_correct_lut(float value) const {
float scaled = value * 255.0f;
auto idx = static_cast<uint8_t>(scaled);
if (idx >= 255)
return progmem_read_uint16(&this->gamma_table_->lut[255]) / 65535.0f;
return progmem_read_uint16(&this->gamma_table_[255]) / 65535.0f;
float frac = scaled - idx;
float a = progmem_read_uint16(&this->gamma_table_->lut[idx]);
float b = progmem_read_uint16(&this->gamma_table_->lut[idx + 1]);
float a = progmem_read_uint16(&this->gamma_table_[idx]);
float b = progmem_read_uint16(&this->gamma_table_[idx + 1]);
return (a + frac * (b - a)) / 65535.0f;
}
float LightState::gamma_uncorrect_lut(float value) const {
@@ -338,12 +321,12 @@ float LightState::gamma_uncorrect_lut(float value) const {
if (this->gamma_table_ == nullptr)
return value;
uint16_t target = static_cast<uint16_t>(value * 65535.0f);
uint8_t lo = gamma_table_reverse_search(this->gamma_table_->lut, target);
uint8_t lo = gamma_table_reverse_search(this->gamma_table_, target);
if (lo >= 255)
return 1.0f;
// Interpolate between lo and lo+1
uint16_t a = progmem_read_uint16(&this->gamma_table_->lut[lo]);
uint16_t b = progmem_read_uint16(&this->gamma_table_->lut[lo + 1]);
uint16_t a = progmem_read_uint16(&this->gamma_table_[lo]);
uint16_t b = progmem_read_uint16(&this->gamma_table_[lo + 1]);
if (b == a)
return lo / 255.0f;
float frac = static_cast<float>(target - a) / static_cast<float>(b - a);
@@ -352,14 +335,11 @@ float LightState::gamma_uncorrect_lut(float value) const {
#endif // USE_LIGHT_GAMMA_LUT
void LightState::start_effect_(uint32_t effect_index) {
// An external add_effects() can exceed the codegen cap; ignore an index the uint16_t can't hold
if (effect_index > std::numeric_limits<uint16_t>::max())
return;
this->stop_effect_();
if (effect_index == 0)
return;
this->active_effect_index_ = static_cast<uint16_t>(effect_index);
this->active_effect_index_ = effect_index;
auto *effect = this->get_active_effect_();
effect->start_internal();
// Enable loop while effect is active
@@ -455,7 +435,7 @@ void LightState::save_remote_values_() {
saved.color_temp = this->remote_values.get_color_temperature();
saved.cold_white = this->remote_values.get_cold_white();
saved.warm_white = this->remote_values.get_warm_white();
saved.effect = static_cast<uint32_t>(this->active_effect_index_); // the saved layout stays uint32_t
saved.effect = this->active_effect_index_;
this->rtc_.save(&saved);
}
+17 -44
View File
@@ -6,7 +6,6 @@
#include "esphome/core/preferences.h"
#include "esphome/core/string_ref.h"
#include "light_call.h"
#include "esp_color_correction.h"
#include "light_color_values.h"
#include "light_effect.h"
#include "light_traits.h"
@@ -152,28 +151,15 @@ class LightState : public EntityBase, public Component {
}
uint32_t get_default_transition_length() const { return this->default_transition_length_; }
#ifdef USE_LIGHT_FLASH_TRANSITION_LENGTH
/// Set the flash transition length; only compiled in when a light configures one
/// Set the flash transition length
void set_flash_transition_length(uint32_t flash_transition_length) {
this->flash_transition_length_ = flash_transition_length;
}
uint32_t get_flash_transition_length() const { return this->flash_transition_length_; }
#else
// Remove before 2027.4.0
ESPDEPRECATED("set_flash_transition_length() does nothing unless flash_transition_length is set in YAML. Removed in "
"2027.4.0",
"2026.10.0")
void set_flash_transition_length(uint32_t flash_transition_length) {}
uint32_t get_flash_transition_length() const { return 0; }
#endif
// Remove before 2027.4.0
ESPDEPRECATED("set_gamma_correct() does nothing; gamma is fixed at build time by gamma_correct in YAML. Removed in "
"2027.4.0",
"2026.10.0")
void set_gamma_correct(float gamma_correct) {}
/// The gamma correction factor, read from the entry after the gamma lookup table; 0 without one
float get_gamma_correct() const;
/// Set the gamma correction factor
void set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; }
float get_gamma_correct() const { return this->gamma_correct_; }
#ifdef USE_LIGHT_TRANSITION_PUBLISH_INTERVAL
void set_transition_state_publish_interval(uint32_t transition_state_publish_interval) {
@@ -183,11 +169,11 @@ class LightState : public EntityBase, public Component {
#endif
#ifdef USE_LIGHT_GAMMA_LUT
/// Set the pre-computed PROGMEM gamma curve
void set_gamma_table(const GammaTable *table) { this->gamma_table_ = table; }
/// Set pre-computed gamma forward lookup table (256-entry uint16 PROGMEM array)
void set_gamma_table(const uint16_t *forward) { this->gamma_table_ = forward; }
/// Get the forward gamma lookup table, 256 PROGMEM entries
const uint16_t *get_gamma_table() const { return this->gamma_table_ != nullptr ? this->gamma_table_->lut : nullptr; }
/// Get the forward gamma lookup table
const uint16_t *get_gamma_table() const { return this->gamma_table_; }
/// Apply gamma correction using the pre-computed forward LUT
float gamma_correct_lut(float value) const;
@@ -208,11 +194,6 @@ class LightState : public EntityBase, public Component {
/// Set whether this light persists its state to preferences at all.
void set_save_enabled(bool save_enabled) { this->save_enabled_ = save_enabled; }
#ifdef USE_LIGHT_RESUME_EFFECT
/// Set whether a plain turn-on restores the effect that was active when the light was turned off.
void set_resume_effect(bool resume_effect) { this->resume_effect_ = resume_effect; }
#endif // USE_LIGHT_RESUME_EFFECT
/// Return whether the light has any effects that meet the trait requirements.
bool supports_effects() const { return !this->effects_.empty(); }
@@ -381,40 +362,32 @@ class LightState : public EntityBase, public Component {
/// Values live in flash as function body; no per-instance data storage beyond this pointer.
void (*state_callback_)(LightStateRTCState &, bool restored){nullptr};
/// Value for storing the index of the currently active effect. 0 if no effect is active
uint32_t active_effect_index_{};
/// Default transition length for all transitions in ms.
uint32_t default_transition_length_{};
#ifdef USE_LIGHT_FLASH_TRANSITION_LENGTH
/// Transition length to use for flash transitions.
uint32_t flash_transition_length_{}; // Keep in sync with DEFAULT_FLASH_TRANSITION_LENGTH in __init__.py
#endif
#ifdef USE_LIGHT_TRANSITION_PUBLISH_INTERVAL
uint32_t transition_state_publish_interval_{0};
uint32_t last_transition_state_publish_{0};
#endif
/// Gamma correction factor for the light.
float gamma_correct_{};
#ifdef USE_LIGHT_GAMMA_LUT
const GammaTable *gamma_table_{nullptr};
const uint16_t *gamma_table_{nullptr};
#endif // USE_LIGHT_GAMMA_LUT
/// 1-based index of the active effect, 0 if none; codegen caps effects at MAX_EFFECTS in effects.py
uint16_t active_effect_index_{};
#ifdef USE_LIGHT_RESUME_EFFECT
/// The effect index that was active when the light was last turned off; shares the active index's word
uint16_t previous_effect_index_{0};
#endif // USE_LIGHT_RESUME_EFFECT
/// Whether the light value should be written in the next cycle.
bool next_write_{true}; // a plain bool: it is the most written flag, and still shares the index's word
bool next_write_{true};
// for effects, true if a transformer (transition) is active.
bool is_transformer_active_ : 1 {false};
bool is_transformer_active_{false};
/// Whether this light persists its state to preferences at all.
bool save_enabled_ : 1 {false};
bool save_enabled_{false};
#ifdef USE_LIGHT_TRANSITION_PUBLISH_INTERVAL
/// True while the active transformer publishes current_values on an interval from loop().
bool transition_publish_enabled_ : 1 {false};
bool transition_publish_enabled_{false};
#endif
#ifdef USE_LIGHT_RESUME_EFFECT
/// Whether a plain turn-on restores the effect that was active when the light was turned off.
bool resume_effect_ : 1 {false};
#endif // USE_LIGHT_RESUME_EFFECT
};
} // namespace esphome::light
-1
View File
@@ -4,7 +4,6 @@ import esphome.codegen as cg
# Base
light_ns = cg.esphome_ns.namespace("light")
LightState = light_ns.class_("LightState", cg.EntityBase, cg.Component)
GammaTable = light_ns.struct("GammaTable")
LightStateRef = LightState.operator("ref")
AddressableLightState = light_ns.class_("AddressableLightState", LightState)
LightOutput = light_ns.class_("LightOutput")
Loaded 100 of 286 files, more files were not shown because too many files have changed in this diff. Show more