Merge branch 'platformio-pch-libretiny' into pch-strict-ci

# Conflicts:
#	esphome/build_gen/arduino8266.py
This commit is contained in:
J. Nick Koston
2026-09-28 11:36:36 +02:00
262 changed files with 8670 additions and 3593 deletions
+30 -35
View File
@@ -193,9 +193,10 @@ jobs:
needs:
- common
# PR-branch cache saves are invisible to other PRs, so dev pushes seed
# the shared entry test-esp8266-native restores. Only dev: the composite
# action saves nowhere else, so a beta/release push would download the
# toolchain and discard it.
# 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:
@@ -399,14 +400,9 @@ jobs:
matrix:
bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }}
env:
# 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
# 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
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -426,14 +422,13 @@ jobs:
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
- 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
- name: Restore integration library cache
# A miss or a changed pin self-heals with a download, so no restore-keys
id: library-cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
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') }}
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') }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
@@ -501,16 +496,16 @@ jobs:
# A full cron period of margin for the weekly refresh
retention-days: 14
- name: Print ccache statistics
# 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
# 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
# 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.pio-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.library-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.INTEGRATION_PIO_CACHE_PATH }}
key: ${{ steps.pio-cache.outputs.cache-primary-key }}
path: ${{ env.INTEGRATION_LIBRARY_CACHE_PATH }}
key: ${{ steps.library-cache.outputs.cache-primary-key }}
import-time:
name: Check import esphome.__main__ time
@@ -1127,6 +1122,11 @@ 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
@@ -1481,12 +1481,9 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- 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: 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: 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'
@@ -1571,11 +1568,9 @@ jobs:
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- 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: Cache the native ESP8266 toolchain
if: fromJSON(needs.determine-jobs.outputs.memory_impact).needs_arduino8266
uses: ./.github/actions/cache-arduino8266
- name: Build, compile, and analyze memory
id: extract
run: |
+19
View File
@@ -322,6 +322,25 @@ 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,6 +125,7 @@ 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.16.0
RUN uv pip install --no-cache-dir esphome-device-builder==1.17.0
RUN \
platformio settings set enable_telemetry No \
+14 -23
View File
@@ -16,7 +16,7 @@ from typing import TYPE_CHECKING, Protocol
# cause them to be loaded before external components are processed, resulting
# in the built-in version being used instead of the external component one.
from esphome import const, platform_hooks
from esphome.build_helpers.native import native_backend
from esphome.build_helpers.native import analysis_backend, native_backend
from esphome.const import (
ALLOWED_NAME_CHARS,
ARGUMENT_HELP_DEVICE,
@@ -1714,26 +1714,12 @@ 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'", _host_program_path(config)
)
_LOGGER.info("Successfully compiled program to path '%s'", CORE.firmware_bin)
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(
@@ -1778,7 +1764,7 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
return exit_code
_LOGGER.info("Successfully compiled program.")
if CORE.is_host:
program_path = _host_program_path(config)
program_path = str(CORE.firmware_bin)
_LOGGER.info("Running program from path '%s'", program_path)
return run_external_process(program_path)
@@ -2015,8 +2001,8 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
from esphome.analyze_memory.ram_strings import RamStringsAnalyzer
# Refuse an unsupported toolchain before paying for a full compile
native_toolchain = native_backend()
if native_toolchain is None and not CORE.using_toolchain_platformio:
analysis_toolchain = analysis_backend()
if analysis_toolchain is None and not CORE.using_toolchain_platformio:
_LOGGER.error(
"analyze-memory is not supported with the '%s' toolchain on %s; "
"re-run with --toolchain platformio",
@@ -2024,6 +2010,11 @@ 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)
@@ -2036,9 +2027,9 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
# Get idedata for analysis
idedata = None
if native_toolchain is not None:
objdump = native_toolchain.get_objdump_path()
readelf = native_toolchain.get_readelf_path()
if analysis_toolchain is not None:
objdump = analysis_toolchain.get_objdump_path()
readelf = analysis_toolchain.get_readelf_path()
for tool in (objdump, readelf):
if not tool.is_file():
# The analyzer would silently fall back to host
@@ -2052,7 +2043,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
objdump_path = str(objdump)
readelf_path = str(readelf)
firmware_elf = native_toolchain.get_elf_path()
firmware_elf = analysis_toolchain.get_elf_path()
if not firmware_elf.is_file():
# The analyzer swallows tool failures, so a missing ELF would
# produce an exit-0 zeroed report
+33 -15
View File
@@ -2,7 +2,9 @@
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.
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.
Deviations from PlatformIO: flat-layout libraries get the recursive default
source filter; ``dot_a_linkage`` is honored; bundled libraries never run a
@@ -342,13 +344,24 @@ def _check_unfulfilled_provides(
def resolve_libraries(
framework_path: Path, *, pio_platform: str, board_mcu: str, cache_key: str
framework_path: Path | None,
*,
pio_platform: str,
board_mcu: str,
cache_key: str,
framework: str | None = "arduino",
manifest_optional: bool = False,
) -> 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.
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.
The returned list is not topologically sorted, so the caller must link
the archives inside one ``--start-group``/``--end-group`` pair (the
@@ -359,18 +372,22 @@ 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()
# 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[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()
)
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
@@ -497,12 +514,13 @@ def resolve_libraries(
backend = LibraryBackend(
platform=pio_platform,
framework="arduino",
framework=framework,
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)
+2 -17
View File
@@ -17,9 +17,8 @@ import os
from pathlib import Path
from typing import NamedTuple
from esphome.build_helpers.ccache import ccache_defaults_env
from esphome.build_helpers.ccache import ccache_env
from esphome.build_helpers.ninja import find_ninja
from esphome.build_helpers.pch import ccache_pch_env
from esphome.build_helpers.tools_cache import ARDUINO8266_TOOLS_CACHE, tools_cache_path
from esphome.core import EsphomeError, Version
from esphome.framework_helpers import str_to_lst_of_str
@@ -144,19 +143,5 @@ 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))
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 {}
env = ccache_defaults_env(get_arduino8266_tools_path() / "ccache")
env.update(ccache_pch_env())
env.update(ccache_env(ccache, ARDUINO8266_TOOLS_CACHE))
return env
+10 -64
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_ccache_path
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.const import (
CONF_COMPILE_PROCESS_LIMIT,
CONF_ESPHOME,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
)
from esphome.core import CORE, EsphomeError
from esphome.helpers import write_file
from esphome.core import CORE
from esphome.types import ConfigType
if TYPE_CHECKING:
@@ -28,20 +28,6 @@ _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")
@@ -84,21 +70,17 @@ 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()
warn_ignored_platformio_options(NATIVE_ARDUINO_CONSUMED_PIO_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_ccache_path()
build_gen.write_project(paths, ccache)
ccache = resolve_absolute_ccache_path()
ninja_changed = build_gen.write_project(paths, ccache)
build_dir = get_build_dir()
env = framework.get_build_env(paths.toolchain, ccache)
# 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)
refresh_compile_commands(paths.ninja, build_dir, env, ninja_changed)
cmd = [str(paths.ninja)]
if verbose:
@@ -140,42 +122,6 @@ 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
@@ -253,7 +199,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_ccache_path()
launcher = ccache or resolve_absolute_ccache_path()
return load_or_build_idedata(
get_build_dir() / "compile_commands.json",
get_elf_path(),
+26 -13
View File
@@ -345,14 +345,28 @@ async def _apply_parent(config: ConfigType, id_key: str = CONF_ID) -> str:
def _apply_lambda_args(args: TemplateArgsType) -> TemplateArgsType:
# Must match ApplyAction::ApplyFn and ApplyCondition::CheckFn exactly for the function
# pointer conversion.
# The generated function's parameters; a std::string arg is never copied.
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,
@@ -403,8 +417,8 @@ def register_apply_action(
) -> None:
"""Register an action that only forwards config values to its parent, with no C++ class.
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.
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.
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.
@@ -449,10 +463,9 @@ def register_apply_action(
*statements,
"apply_call.perform();",
]
apply_lambda = LambdaExpression(
["\n".join(statements)], lambda_args, capture="", return_type=cg.void
return _apply_function(
action_id, cg.void, template_arg, lambda_args, statements
)
return cg.new_Pvariable(action_id, template_arg, apply_lambda)
register_action(name, ApplyAction, schema, synchronous=True)(builder)
@@ -466,7 +479,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 stateless function for ``ApplyCondition<Ts...>``.
Generates one static predicate and an ``ApplyCondition`` templated on it.
"""
call = check if isinstance(check, ApplyCall) else ApplyCall(check)
members = call.members
@@ -492,13 +505,13 @@ def register_apply_condition(
lambda_args,
compare=True,
)
check_lambda = LambdaExpression(
[f"return {parent}->{call.target.format(*exprs)};"],
return _apply_function(
condition_id,
cg.bool_,
template_arg,
lambda_args,
capture="",
return_type=cg.bool_,
[f"return {parent}->{call.target.format(*exprs)};"],
)
return cg.new_Pvariable(condition_id, template_arg, check_lambda)
register_condition(name, ApplyCondition, schema)(builder)
+27 -131
View File
@@ -14,7 +14,6 @@ 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
@@ -23,7 +22,6 @@ 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
@@ -34,6 +32,15 @@ from esphome.build_helpers.ninja import (
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_rule_lines,
tool_lines,
)
from esphome.build_helpers.pch import (
PCH_CORE_HEADER,
PCH_HEADER_NAME,
@@ -61,12 +68,9 @@ 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,
strip_win_long_path_prefix,
)
from esphome.framework_helpers import get_project_cxx_compile_flags
from esphome.helpers import mkdir_p, write_file_if_changed
from esphome.platformio.library import SOURCE_KIND_FOR_SUFFIX, lex_build_flags
from esphome.platformio.library import lex_build_flags
if TYPE_CHECKING:
from esphome.arduino8266.framework import InstalledPaths
@@ -658,14 +662,6 @@ _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.
@@ -883,41 +879,6 @@ def _generate_testing_flash_ld(
)
def _ninja_compile_edges(
lines: list[str],
sources: list[Path],
root: Path,
group: str,
flags: 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" and cxx_override else None
implicit = f" | {override[1]}" if override else ""
lines.append(f"build {escaped_obj}: {kind} {_e(src)}{implicit}")
edge_flags = override[0] if override else flags
if edge_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 _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]
@@ -1104,55 +1065,15 @@ 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 = [
"# 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",
# 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",
# 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",
*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(),
"rule link",
" command = $cxx -o $out $linkflags @$out.rsp $libdirflags -Wl,--start-group $archives $libflags -Wl,--end-group",
" rspfile = $out.rsp",
@@ -1182,16 +1103,15 @@ 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 = _ninja_compile_edges(lines, variant_sources, variant_dir, "variant")
objs = compile_edges(lines, variant_sources, variant_dir, "variant")
lines.append(f"build libFrameworkArduinoVariant.a: ar {' '.join(objs)}")
archives.append("libFrameworkArduinoVariant.a")
core_objs = _ninja_compile_edges(
lines, _collect_sources(core_dir, core_exclude), core_dir, "core"
core_objs = 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
@@ -1202,32 +1122,8 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
lines.append(f"build libFrameworkArduino.a: ar {' '.join(core_objs)}")
archives.append("libFrameworkArduino.a")
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)
lib_archives, direct_objs = library_edges(lines, libraries)
archives += lib_archives
# One source of truth with the PlatformIO path: esp8266/__init__ pins
# build_src_flags (the throw_stubs force-include); -include paths
@@ -1348,9 +1244,9 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
mark_pch_emitted()
else:
pch_disabled_degraded()
src_objs = _ninja_compile_edges(
src_objs = compile_edges(
lines,
_collect_sources(src_dir),
collect_sources(src_dir),
src_dir,
"src",
flags="$srcflags",
+272
View File
@@ -0,0 +1,272 @@
"""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,
tool_lines,
)
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(),
"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)
src_objs = compile_edges(lines, collect_sources(src_dir), src_dir, "src")
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))
+29 -3
View File
@@ -73,6 +73,15 @@ 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).
@@ -89,11 +98,11 @@ def ccache_defaults_env(cache_dir: Path) -> dict[str, str]:
"CORE.build_path must be set before constructing the build environment"
)
defaults = {
"CCACHE_DIR": str(cache_dir),
# ccache expands $VAR in its settings; $$ is a literal $
"CCACHE_DIR": str(cache_dir).replace("$", "$$"),
"CCACHE_NOHASHDIR": "true",
"CCACHE_DEPEND": "1",
# A user value wins via the filter below
"CCACHE_BASEDIR": effective_ccache_basedir(),
"CCACHE_BASEDIR": str(Path(CORE.build_path).resolve()).replace("$", "$$"),
}
return {k: v for k, v in defaults.items() if k not in os.environ}
@@ -108,3 +117,20 @@ def effective_ccache_basedir() -> str:
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
+40
View File
@@ -7,16 +7,21 @@ 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",
}
@@ -32,3 +37,38 @@ def native_backend() -> ModuleType | None:
f"module for platform {CORE.target_platform}"
)
return importlib.import_module(module_path)
# Binutils and the linked image for memory analysis, for toolchains that build
# without PlatformIO but have no native build backend (which supplies them)
ANALYSIS_TOOLCHAIN_MODULES = {
("nrf52", Toolchain.SDK_NRF): "esphome.components.nrf52.toolchain",
}
def analysis_backend() -> ModuleType | None:
"""The module giving objdump, readelf and the ELF of a non-PlatformIO build.
None means PlatformIO's idedata supplies them (or nothing can).
"""
if (native := native_backend()) is not 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,
)
+93 -3
View File
@@ -2,14 +2,17 @@
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__)
@@ -23,11 +26,26 @@ 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 = strip_win_long_path_prefix(binary)
binary = absolute_tool(binary)
if _ninja_runs(binary):
return Path(binary)
import_error: ImportError | None = None
@@ -49,8 +67,18 @@ def find_ninja() -> Path:
def escape(value: Path | str) -> str:
"""Escape a path or token for a ninja file."""
return str(value).replace("$", "$$").replace(":", "$:").replace(" ", "$ ")
"""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(" ", "$ ")
def quote_arg(tok: str) -> str:
@@ -90,3 +118,65 @@ 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)
+223
View File
@@ -0,0 +1,223 @@
"""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.ninja import (
escape as _e,
quote_path as _q,
shell_token as _shell_token,
)
from esphome.framework_helpers import strip_win_long_path_prefix
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 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
+8 -1
View File
@@ -33,4 +33,11 @@ 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")
TOOLS_CACHE_SPECS = (IDF_TOOLS_CACHE, SDK_NRF_TOOLS_CACHE, ARDUINO8266_TOOLS_CACHE)
# 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,
)
+1
View File
@@ -42,6 +42,7 @@ from esphome.cpp_generator import ( # noqa: F401
set_cpp_standard,
statement,
static_const_array,
static_function,
templatable,
variable,
with_local_variable,
@@ -85,20 +85,11 @@ async def wave_base_to_code(var: MockObj, config: ConfigType) -> None:
await ble_client.register_ble_node(var, config)
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))
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_battery_update_interval := config.get(CONF_BATTERY_UPDATE_INTERVAL):
cg.add(var.set_battery_update_interval(config_battery_update_interval))
@@ -87,16 +87,9 @@ async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await airthings_wave_base.wave_base_to_code(var, config)
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))
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)
cg.add(var.set_device_type(config[CONF_DEVICE_TYPE]))
+6 -15
View File
@@ -85,18 +85,9 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
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))
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)
+9 -24
View File
@@ -129,30 +129,15 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await spi.register_spi_device(var, config)
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))
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)
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]))
+11 -2
View File
@@ -61,14 +61,16 @@ from esphome.const import (
DEVICE_CLASS_VIBRATION,
DEVICE_CLASS_WINDOW,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
SubEntities,
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType
from esphome.util import Registry
CODEOWNERS = ["@esphome/core"]
@@ -632,6 +634,13 @@ 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),
+6 -16
View File
@@ -150,15 +150,10 @@ 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)
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))
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)
for i in range(6):
if channel_config := config.get(f"{CONF_CHANNEL}_{i + 1}"):
@@ -172,10 +167,5 @@ 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))
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))
await sensors(CONF_TOTAL_POWER, var.set_total_power_sensor)
await sensors(CONF_TOTAL_ENERGY, var.set_total_energy_sensor)
+9 -24
View File
@@ -94,27 +94,12 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
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))
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)
+7 -18
View File
@@ -231,24 +231,13 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
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))
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)
# enable legacy mode
cg.add(var.set_legacy_mode(config.get(CONF_LEGACY_MODE)))
+6 -15
View File
@@ -101,21 +101,12 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
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))
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)
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,6 +7,7 @@ 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,
@@ -61,6 +62,7 @@ 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:
+8 -14
View File
@@ -90,20 +90,14 @@ async def to_code_base(config: ConfigType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
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]))
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]))
cg.add(var.set_iir_filter(config[CONF_IIR_FILTER]))
@@ -72,18 +72,9 @@ async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await setup_bthome_mithermometer(var, config)
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))
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)
+9 -1
View File
@@ -16,8 +16,9 @@ from esphome.const import (
DEVICE_CLASS_RESTART,
DEVICE_CLASS_UPDATE,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
SubEntities,
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
@@ -114,6 +115,13 @@ 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,6 +15,7 @@ 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
@@ -0,0 +1 @@
CODEOWNERS = ["@clydebarrow"]
@@ -0,0 +1,28 @@
#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
@@ -0,0 +1,47 @@
#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
@@ -0,0 +1,85 @@
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),
)
+8 -21
View File
@@ -98,24 +98,11 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
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))
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)
+3 -1
View File
@@ -1,4 +1,5 @@
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
@@ -51,7 +52,8 @@ 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
# RTT sources live in the segger project
include_west_project("segger")
zephyr_add_prj_conf("USE_SEGGER_RTT", True)
zephyr_add_prj_conf("RTT_CONSOLE", True)
zephyr_add_prj_conf("LOG", True)
+9 -28
View File
@@ -106,32 +106,13 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
debug_component = await cg.get_variable(config[CONF_DEBUG_ID])
hub = await cg.get_variable(config[CONF_DEBUG_ID])
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))
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)
+4 -7
View File
@@ -31,11 +31,8 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config: ConfigType) -> None:
debug_component = await cg.get_variable(config[CONF_DEBUG_ID])
hub = await cg.get_variable(config[CONF_DEBUG_ID])
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))
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)
+1 -1
View File
@@ -1,5 +1,6 @@
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
@@ -13,7 +14,6 @@ MULTI_CONF = True
CONF_PWM = "pwm"
CONF_DIVIDER = "divider"
CONF_DAC = "dac"
CONF_CONVERSION_RATE = "conversion_rate"
CONF_EMC2101_ID = "emc2101_id"
+4 -8
View File
@@ -31,12 +31,8 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config):
parent = await cg.get_variable(config[CONF_ID])
hub = await cg.get_variable(config[CONF_ID])
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))
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)
+8 -24
View File
@@ -76,28 +76,12 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config):
parent = await cg.get_variable(config[CONF_ID])
hub = await cg.get_variable(config[CONF_ID])
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))
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)
+4 -8
View File
@@ -24,12 +24,8 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config):
parent = await cg.get_variable(config[CONF_ID])
hub = await cg.get_variable(config[CONF_ID])
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))
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)
+8 -27
View File
@@ -177,33 +177,14 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_protocol_version(config[CONF_PROTOCOL_VERSION]))
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))
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)
for i, phase in enumerate([CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]):
if phase not in config:
+18 -59
View File
@@ -229,65 +229,24 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await modbus.register_modbus_client_device(var, config)
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))
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)
for i, phase in enumerate([CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]):
if phase not in config:
+5 -12
View File
@@ -107,18 +107,11 @@ async def to_code(config: ConfigType) -> None:
cf1 = await cg.gpio_pin_expression(config[CONF_CF1_PIN])
cg.add(var.set_cf1_pin(cf1))
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))
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)
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]]))
+4 -11
View File
@@ -92,17 +92,10 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
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))
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)
# Remove before 2026.12.0
if CONF_AQI in config:
@@ -1,6 +1,9 @@
#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 {
@@ -60,6 +63,61 @@ 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.
@@ -91,6 +149,9 @@ 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();
@@ -102,6 +163,12 @@ 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,
@@ -113,6 +180,14 @@ 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;
@@ -125,6 +200,9 @@ 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.
@@ -156,6 +234,9 @@ 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 {};
}
@@ -217,6 +298,155 @@ 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);
@@ -371,6 +601,9 @@ 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;
}
@@ -410,6 +643,11 @@ 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,7 +4,11 @@
#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 {
@@ -21,6 +25,28 @@ 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.
@@ -35,6 +61,12 @@ 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;
@@ -63,6 +95,9 @@ 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_; }
@@ -95,6 +130,21 @@ 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);
@@ -145,7 +195,24 @@ 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
@@ -0,0 +1,50 @@
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))
@@ -0,0 +1,49 @@
#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
@@ -0,0 +1,24 @@
#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
+22 -11
View File
@@ -1,3 +1,5 @@
import sys
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
@@ -8,9 +10,9 @@ from esphome.const import (
KEY_TARGET_PLATFORM,
PLATFORM_HOST,
ThreadModel,
Toolchain,
)
from esphome.core import CORE
from esphome.platformio.toolchain import copy_ccache_script
from esphome.core import CORE, EsphomeError
from esphome.types import ConfigType
from .const import KEY_HOST
@@ -37,7 +39,9 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_MAC_ADDRESS, default="98:35:69:ab:f6:79"): cv.mac_address,
}
),
cv.require_platformio_toolchain("host"),
# 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),
set_core_data,
)
@@ -48,16 +52,23 @@ 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.add_build_flag("-std=gnu++20")
cg.set_cpp_standard("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 writer.py
def copy_files() -> None:
copy_ccache_script()
# 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
+5 -15
View File
@@ -80,18 +80,8 @@ 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]))
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))
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)
+5 -15
View File
@@ -133,18 +133,8 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_adc_avg_samples(config[CONF_ADC_AVERAGING]))
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))
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)
+10 -35
View File
@@ -225,38 +225,13 @@ 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))
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))
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)
+8 -13
View File
@@ -48,16 +48,11 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
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))
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
)
+5 -13
View File
@@ -46,16 +46,8 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
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))
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)
+30 -29
View File
@@ -87,31 +87,32 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
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))
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,
)
for x in range(9):
if gate_conf := config.get(f"g{x}"):
move_config = gate_conf[CONF_MOVE_THRESHOLD]
@@ -119,13 +120,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, config[CONF_LD2410_ID])
cg.add(ld2410_component.set_gate_move_threshold_number(x, n))
await cg.register_parented(n, hub)
cg.add(hub.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, config[CONF_LD2410_ID])
cg.add(ld2410_component.set_gate_still_threshold_number(x, n))
await cg.register_parented(n, hub)
cg.add(hub.set_gate_still_threshold_number(x, n))
+29 -33
View File
@@ -50,36 +50,32 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
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))
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",
],
)
+10 -21
View File
@@ -157,30 +157,19 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
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))
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)
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(ld2410_component.set_gate_move_sensor(x, sens))
cg.add(hub.set_gate_move_sensor(x, sens))
if still_config := gate_conf.get(CONF_STILL_ENERGY):
sens = await sensor.new_sensor(still_config)
cg.add(ld2410_component.set_gate_still_sensor(x, sens))
cg.add(hub.set_gate_still_sensor(x, sens))
+4 -9
View File
@@ -37,12 +37,7 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
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))
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)
+4 -7
View File
@@ -28,10 +28,7 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
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))
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)
+9 -21
View File
@@ -50,24 +50,12 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
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))
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)
+9 -19
View File
@@ -56,22 +56,12 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
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))
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,
)
+37 -34
View File
@@ -87,42 +87,45 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
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))
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,
)
for x in range(14):
if gate_conf := config.get(f"gate_{x}"):
move_config = gate_conf[CONF_MOVE_THRESHOLD]
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, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_gate_move_threshold_number(x, n))
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 = cg.new_Pvariable(still_config[CONF_ID])
await number.register_number(
n, still_config, min_value=0, max_value=100, step=1
n = await number.new_number(
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))
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,
)
+31 -33
View File
@@ -50,36 +50,34 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
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))
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"],
)
+10 -21
View File
@@ -158,30 +158,19 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
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))
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)
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(LD2412_component.set_gate_move_sensor(x, sens))
cg.add(hub.set_gate_move_sensor(x, sens))
if still_config := gate_conf.get(CONF_STILL_ENERGY):
sens = await sensor.new_sensor(still_config)
cg.add(LD2412_component.set_gate_still_sensor(x, sens))
cg.add(hub.set_gate_still_sensor(x, sens))
+4 -9
View File
@@ -37,12 +37,7 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
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))
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)
+4 -7
View File
@@ -28,10 +28,7 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
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))
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)
@@ -27,8 +27,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
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))
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))
+6 -17
View File
@@ -52,20 +52,9 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
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))
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)
+52 -43
View File
@@ -115,56 +115,65 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
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))
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):
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))
await numbers(
CONF_GATE_SELECT,
hub.set_gate_select_number,
min_value=0,
max_value=15,
step=1,
)
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, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_gate_still_threshold_numbers(0, n))
await cg.register_parented(n, hub)
cg.add(hub.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, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_gate_move_threshold_numbers(0, n))
await cg.register_parented(n, hub)
cg.add(hub.set_gate_move_threshold_numbers(0, n))
else:
for x in range(16):
if gate_conf := config.get(f"gate_{x}"):
@@ -173,13 +182,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, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_gate_move_threshold_numbers(x, n))
await cg.register_parented(n, hub)
cg.add(hub.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, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_gate_still_threshold_numbers(x, n))
await cg.register_parented(n, hub)
cg.add(hub.set_gate_still_threshold_numbers(x, n))
+5 -8
View File
@@ -25,11 +25,8 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
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))
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
)
+5 -8
View File
@@ -34,11 +34,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)
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))
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))
@@ -28,8 +28,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
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))
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))
+5 -10
View File
@@ -41,13 +41,8 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
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))
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)
+4 -9
View File
@@ -37,12 +37,7 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
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))
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)
+14 -15
View File
@@ -80,16 +80,15 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
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))
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,
)
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)
@@ -97,27 +96,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, config[CONF_LD2450_ID])
await cg.register_parented(x1, hub)
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, config[CONF_LD2450_ID])
await cg.register_parented(y1, hub)
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, config[CONF_LD2450_ID])
await cg.register_parented(x2, hub)
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, config[CONF_LD2450_ID])
await cg.register_parented(y2, hub)
cg.add(ld2450_component.set_zone_numbers(zone_num, x1, y1, x2, y2))
cg.add(hub.set_zone_numbers(zone_num, x1, y1, x2, y2))
+25 -28
View File
@@ -33,31 +33,28 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
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))
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",
],
)
+14 -21
View File
@@ -228,47 +228,40 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
ld2450_component = await cg.get_variable(config[CONF_LD2450_ID])
hub = await cg.get_variable(config[CONF_LD2450_ID])
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))
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)
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(ld2450_component.set_move_x_sensor(n, sens))
cg.add(hub.set_move_x_sensor(n, sens))
if y_config := target_conf.get(CONF_Y):
sens = await sensor.new_sensor(y_config)
cg.add(ld2450_component.set_move_y_sensor(n, sens))
cg.add(hub.set_move_y_sensor(n, sens))
if speed_config := target_conf.get(CONF_SPEED):
sens = await sensor.new_sensor(speed_config)
cg.add(ld2450_component.set_move_speed_sensor(n, sens))
cg.add(hub.set_move_speed_sensor(n, sens))
if angle_config := target_conf.get(CONF_ANGLE):
sens = await sensor.new_sensor(angle_config)
cg.add(ld2450_component.set_move_angle_sensor(n, sens))
cg.add(hub.set_move_angle_sensor(n, sens))
if distance_config := target_conf.get(CONF_DISTANCE):
sens = await sensor.new_sensor(distance_config)
cg.add(ld2450_component.set_move_distance_sensor(n, sens))
cg.add(hub.set_move_distance_sensor(n, sens))
if resolution_config := target_conf.get(CONF_RESOLUTION):
sens = await sensor.new_sensor(resolution_config)
cg.add(ld2450_component.set_move_resolution_sensor(n, sens))
cg.add(hub.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(ld2450_component.set_zone_target_count_sensor(n, sens))
cg.add(hub.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(ld2450_component.set_zone_still_target_count_sensor(n, sens))
cg.add(hub.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(ld2450_component.set_zone_moving_target_count_sensor(n, sens))
cg.add(hub.set_zone_moving_target_count_sensor(n, sens))
+4 -9
View File
@@ -37,12 +37,7 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
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))
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)
+5 -8
View File
@@ -51,16 +51,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
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))
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)
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(ld2450_component.set_direction_text_sensor(n, sens))
cg.add(hub.set_direction_text_sensor(n, sens))
+29 -10
View File
@@ -70,6 +70,7 @@ from .types import ( # noqa: F401
AddressableLightState,
ChannelColors,
ColorMode,
GammaTable,
LightOutput,
LightState,
LightStateRTCState,
@@ -85,6 +86,8 @@ CODEOWNERS = ["@esphome/core"]
IS_PLATFORM_COMPONENT = True
DOMAIN = "light"
CONF_GAMMA_TABLE_ID = "gamma_table_id"
CONF_RESUME_EFFECT = "resume_effect"
@dataclass
@@ -140,18 +143,29 @@ def generate_gamma_table(gamma_correct: float) -> list[HexInt]:
return [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)]
def _get_or_create_gamma_table(gamma_correct):
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:
data = _get_data()
if gamma_correct in data.gamma_tables:
return data.gamma_tables[gamma_correct]
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
# 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
def find_effect_index(effects: list, effect_name: str) -> int | None:
@@ -357,6 +371,7 @@ 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
),
@@ -380,6 +395,7 @@ LIGHT_SCHEMA = (
}
),
cv.Optional(CONF_INITIAL_STATE): LIGHT_STATE_SCHEMA,
cv.Optional(CONF_RESUME_EFFECT, default=False): cv.boolean,
}
)
)
@@ -510,6 +526,9 @@ 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)
@@ -553,14 +572,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:
cg.add(light_var.set_gamma_correct(gamma_correct))
fwd_arr = _get_or_create_gamma_table(gamma_correct)
fwd_arr = _get_or_create_gamma_table(gamma_correct, config[CONF_GAMMA_TABLE_ID])
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,12 +545,18 @@ 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,6 +5,12 @@
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,6 +193,11 @@ 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_)));
@@ -334,6 +339,15 @@ 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);
@@ -376,6 +390,11 @@ 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,6 +215,8 @@ 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; }
+30 -10
View File
@@ -9,6 +9,8 @@
#include "light_output.h"
#include "transformers.h"
#include <limits>
namespace esphome::light {
static const char *const TAG = "light";
@@ -94,10 +96,17 @@ 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: %.2f",
this->default_transition_length_ / 1e3f, this->gamma_correct_);
" Gamma Correct: %u.%02u",
this->default_transition_length_ / 1e3f, gamma_x100 / 100, gamma_x100 % 100);
#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) {
@@ -296,6 +305,14 @@ 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)
@@ -307,10 +324,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_[255]) / 65535.0f;
return progmem_read_uint16(&this->gamma_table_->lut[255]) / 65535.0f;
float frac = scaled - idx;
float a = progmem_read_uint16(&this->gamma_table_[idx]);
float b = progmem_read_uint16(&this->gamma_table_[idx + 1]);
float a = progmem_read_uint16(&this->gamma_table_->lut[idx]);
float b = progmem_read_uint16(&this->gamma_table_->lut[idx + 1]);
return (a + frac * (b - a)) / 65535.0f;
}
float LightState::gamma_uncorrect_lut(float value) const {
@@ -321,12 +338,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_, target);
uint8_t lo = gamma_table_reverse_search(this->gamma_table_->lut, target);
if (lo >= 255)
return 1.0f;
// Interpolate between lo and lo+1
uint16_t a = progmem_read_uint16(&this->gamma_table_[lo]);
uint16_t b = progmem_read_uint16(&this->gamma_table_[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]);
if (b == a)
return lo / 255.0f;
float frac = static_cast<float>(target - a) / static_cast<float>(b - a);
@@ -335,11 +352,14 @@ 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_ = effect_index;
this->active_effect_index_ = static_cast<uint16_t>(effect_index);
auto *effect = this->get_active_effect_();
effect->start_internal();
// Enable loop while effect is active
@@ -435,7 +455,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 = this->active_effect_index_;
saved.effect = static_cast<uint32_t>(this->active_effect_index_); // the saved layout stays uint32_t
this->rtc_.save(&saved);
}
+44 -17
View File
@@ -6,6 +6,7 @@
#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"
@@ -151,15 +152,28 @@ class LightState : public EntityBase, public Component {
}
uint32_t get_default_transition_length() const { return this->default_transition_length_; }
/// Set the flash transition length
#ifdef USE_LIGHT_FLASH_TRANSITION_LENGTH
/// Set the flash transition length; only compiled in when a light configures one
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
/// 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_; }
// 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;
#ifdef USE_LIGHT_TRANSITION_PUBLISH_INTERVAL
void set_transition_state_publish_interval(uint32_t transition_state_publish_interval) {
@@ -169,11 +183,11 @@ class LightState : public EntityBase, public Component {
#endif
#ifdef USE_LIGHT_GAMMA_LUT
/// Set pre-computed gamma forward lookup table (256-entry uint16 PROGMEM array)
void set_gamma_table(const uint16_t *forward) { this->gamma_table_ = forward; }
/// Set the pre-computed PROGMEM gamma curve
void set_gamma_table(const GammaTable *table) { this->gamma_table_ = table; }
/// Get the forward gamma lookup table
const uint16_t *get_gamma_table() const { return this->gamma_table_; }
/// 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; }
/// Apply gamma correction using the pre-computed forward LUT
float gamma_correct_lut(float value) const;
@@ -194,6 +208,11 @@ 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(); }
@@ -362,32 +381,40 @@ 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 uint16_t *gamma_table_{nullptr};
const GammaTable *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};
bool next_write_{true}; // a plain bool: it is the most written flag, and still shares the index's word
// for effects, true if a transformer (transition) is active.
bool is_transformer_active_{false};
bool is_transformer_active_ : 1 {false};
/// Whether this light persists its state to preferences at all.
bool save_enabled_{false};
bool save_enabled_ : 1 {false};
#ifdef USE_LIGHT_TRANSITION_PUBLISH_INTERVAL
/// True while the active transformer publishes current_values on an interval from loop().
bool transition_publish_enabled_{false};
bool transition_publish_enabled_ : 1 {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,6 +4,7 @@ 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")
+7 -23
View File
@@ -219,29 +219,13 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if als_config := config.get(CONF_AMBIENT_LIGHT):
sens = await sensor.new_sensor(als_config)
cg.add(var.set_ambient_light_sensor(sens))
if infrared_cnt_config := config.get(CONF_INFRARED_COUNTS):
sens = await sensor.new_sensor(infrared_cnt_config)
cg.add(var.set_infrared_counts_sensor(sens))
if full_spect_cnt_config := config.get(CONF_FULL_SPECTRUM_COUNTS):
sens = await sensor.new_sensor(full_spect_cnt_config)
cg.add(var.set_full_spectrum_counts_sensor(sens))
if act_gain_config := config.get(CONF_ACTUAL_GAIN):
sens = await sensor.new_sensor(act_gain_config)
cg.add(var.set_actual_gain_sensor(sens))
if act_itime_config := config.get(CONF_ACTUAL_INTEGRATION_TIME):
sens = await sensor.new_sensor(act_itime_config)
cg.add(var.set_actual_integration_time_sensor(sens))
if prox_cnt_config := config.get(CONF_PS_COUNTS):
sens = await sensor.new_sensor(prox_cnt_config)
cg.add(var.set_proximity_counts_sensor(sens))
sensors = sensor.sub_sensors(config)
await sensors(CONF_AMBIENT_LIGHT, var.set_ambient_light_sensor)
await sensors(CONF_INFRARED_COUNTS, var.set_infrared_counts_sensor)
await sensors(CONF_FULL_SPECTRUM_COUNTS, var.set_full_spectrum_counts_sensor)
await sensors(CONF_ACTUAL_GAIN, var.set_actual_gain_sensor)
await sensors(CONF_ACTUAL_INTEGRATION_TIME, var.set_actual_integration_time_sensor)
await sensors(CONF_PS_COUNTS, var.set_proximity_counts_sensor)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
+11 -4
View File
@@ -31,6 +31,7 @@ from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
import esphome.config_validation as cv
from esphome.const import (
CONF_BUFFER_SIZE,
CONF_BUILD_FLAGS,
CONF_ESPHOME,
CONF_GROUP,
CONF_ID,
@@ -169,11 +170,17 @@ def generate_lv_conf_h():
all_defines = set(
df.LV_DEFINES + tuple(f"LV_USE_{w.upper()}" for w in WIDGET_TYPES)
)
build_flags = (
CORE.config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS).get("build_flags", [])
esphome_config = CORE.config[CONF_ESPHOME]
# User build flags come from esphome->build_flags and from the deprecated
# esphome->platformio_options->build_flags (a string or a list).
# Remove before 2026.12.0
pio_build_flags = esphome_config.get(CONF_PLATFORMIO_OPTIONS, {}).get(
CONF_BUILD_FLAGS, []
)
if not isinstance(build_flags, list):
build_flags = [build_flags]
if not isinstance(pio_build_flags, list):
pio_build_flags = [pio_build_flags]
build_flags = [*esphome_config.get(CONF_BUILD_FLAGS, []), *pio_build_flags]
# Extract define names from build flags like '-DLV_USE_CHART=1', '-D LV_USE_CHART',
# or multiple defines in one string.
define_pattern = r'-D\s*([A-Z_][A-Z0-9_]*)(?:=[^\s\'"\]]*)?'
+29 -18
View File
@@ -4,6 +4,7 @@
#ifdef USE_LVGL_ANIMATION
#include "lvgl_esphome.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
namespace esphome::lvgl {
@@ -110,17 +111,8 @@ template<size_t DATA_SIZE, bool AUTO_START = false> class LvAnimation : public C
}
void start() {
if (this->state_ > AnimationState::STOPPED)
this->stop();
if (this->duration_ == 0)
if (!this->prepare_())
return;
// evaluate any lambdas
for (size_t i = 0; i != DATA_SIZE; i++) {
this->data_from_[i] = this->from_[i].value();
this->data_to_[i] = this->to_[i].value();
}
this->start_time_ = millis();
this->state_ = AnimationState::STARTED;
this->loop();
this->start_callback_.call();
}
@@ -143,29 +135,26 @@ template<size_t DATA_SIZE, bool AUTO_START = false> class LvAnimation : public C
if (this->state_ == AnimationState::STOPPED)
return;
uint32_t elapsed = millis() - this->start_time_;
float progress = static_cast<float>(elapsed) / static_cast<float>(this->duration_);
float progress = clamp_at_most(elapsed / static_cast<float>(this->duration_), 1.0f);
switch (this->state_) {
case AnimationState::STARTED:
if (elapsed < this->start_delay_)
return;
this->state_ = AnimationState::RUNNING;
this->start_time_ = millis();
elapsed = 0;
progress = 0.0f;
break;
case AnimationState::RUNNING:
if (progress >= 1.0f) {
progress = 1.0f;
this->stop();
if (this->loop_)
this->start();
}
break;
default:
return;
}
// state here is RUNNING.
for (auto *timing : this->timings_) {
progress = timing->map_progress(progress);
// avoid overshooting
progress = clamp_at_most(timing->map_progress(progress), 1.0f);
}
lv_coord_t data[DATA_SIZE];
for (size_t i = 0; i != DATA_SIZE; i++) {
@@ -173,6 +162,12 @@ template<size_t DATA_SIZE, bool AUTO_START = false> class LvAnimation : public C
roundf(this->data_from_[i] + static_cast<lv_coord_t>(this->data_to_[i] - this->data_from_[i]) * progress));
}
this->update_callback_(data);
if (elapsed >= this->duration_) {
this->stop();
// Restart without drawing the first frame now, so the final frame is not overwritten before it is shown
if (this->loop_ && this->prepare_())
this->start_callback_.call();
}
}
float get_setup_priority() const override { return setup_priority::PROCESSOR - 20.0; }
@@ -187,6 +182,22 @@ template<size_t DATA_SIZE, bool AUTO_START = false> class LvAnimation : public C
template<typename F> void add_on_stop_callback(F &&callback) { this->stop_callback_.add(std::forward<F>(callback)); }
protected:
// Arms a new run. Returns false if there is nothing to run.
bool prepare_() {
if (this->state_ > AnimationState::STOPPED)
this->stop();
if (this->duration_ == 0)
return false;
// evaluate any lambdas
for (size_t i = 0; i != DATA_SIZE; i++) {
this->data_from_[i] = this->from_[i].value();
this->data_to_[i] = this->to_[i].value();
}
this->start_time_ = millis();
this->state_ = AnimationState::STARTED;
return true;
}
void (*const update_callback_)(const lv_coord_t *data);
LazyCallbackManager<void()> start_callback_{};
LazyCallbackManager<void()> stop_callback_{};
+8 -8
View File
@@ -246,20 +246,18 @@ class MipiSpi : public display::Display,
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
this->disable();
} else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) {
// Toggle D/C only while holding the bus; on boards where D/C doubles as
// another bus signal, driving it while another device owns the bus
// corrupts that device's transfer.
this->enable();
this->dc_pin_->digital_write(false);
this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8);
this->dc_pin_->digital_write(true);
this->disable();
// hold the bus between command and data to avoid a glitch on the D/C line
if (len != 0) {
this->enable();
this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8);
this->disable();
}
this->disable();
} else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE) {
// Toggle D/C only while holding the bus; works around a quirk in the CoreS3 and W5500 ethernet combination.
// See https://github.com/esphome/esphome/pull/18529
this->enable();
this->dc_pin_->digital_write(false);
this->write_byte(cmd);
@@ -271,12 +269,14 @@ class MipiSpi : public display::Display,
this->disable();
}
} else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE_16) {
this->enable();
// DC must be stable before CS as the clock is gated by CS
this->dc_pin_->digital_write(false);
this->enable();
this->write_byte(cmd);
this->dc_pin_->digital_write(true);
this->disable();
this->dc_pin_->digital_write(true);
for (size_t i = 0; i != len; i++) {
// must enable and disable for each byte based on empirical testing
this->enable();
this->write_byte(0);
this->write_byte(bytes[i]);
+7 -18
View File
@@ -159,21 +159,10 @@ async def to_code(config: ConfigType) -> None:
) is not None:
cg.add(var.set_min_signal_quality(minimum_signal_quality))
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
cg.add(var.set_temperature(sens))
if CONF_LEVEL in config:
sens = await sensor.new_sensor(config[CONF_LEVEL])
cg.add(var.set_level(sens))
if CONF_DISTANCE in config:
sens = await sensor.new_sensor(config[CONF_DISTANCE])
cg.add(var.set_distance(sens))
if CONF_BATTERY_LEVEL in config:
sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
cg.add(var.set_battery_level(sens))
if CONF_SIGNAL_QUALITY in config:
sens = await sensor.new_sensor(config[CONF_SIGNAL_QUALITY])
cg.add(var.set_signal_quality(sens))
if CONF_IGNORED_READS in config:
sens = await sensor.new_sensor(config[CONF_IGNORED_READS])
cg.add(var.set_ignored_reads(sens))
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature)
await sensors(CONF_LEVEL, var.set_level)
await sensors(CONF_DISTANCE, var.set_distance)
await sensors(CONF_BATTERY_LEVEL, var.set_battery_level)
await sensors(CONF_SIGNAL_QUALITY, var.set_signal_quality)
await sensors(CONF_IGNORED_READS, var.set_ignored_reads)
@@ -108,7 +108,7 @@ void MQTTBackendESP32::loop() {
if ((now - this->last_dropped_log_time_) >= DROP_LOG_INTERVAL_MS) {
uint16_t dropped = this->mqtt_queue_.get_and_reset_dropped_count();
if (dropped > 0) {
ESP_LOGW(TAG, "Dropped %u messages (%us)", dropped, DROP_LOG_INTERVAL_MS / 1000);
ESP_LOGW(TAG, "Dropped %u messages (%" PRIu32 "s)", dropped, DROP_LOG_INTERVAL_MS / 1000);
}
this->last_dropped_log_time_ = now;
}
+84 -68
View File
@@ -2,56 +2,64 @@
#include "nci_message.h"
#include "esphome/core/log.h"
#include <cstdio>
namespace esphome::nfc {
static const char *const TAG = "NciMessage";
NciMessage::NciMessage(const uint8_t message_type, const std::vector<uint8_t> &payload) {
NciMessage::NciMessage(const uint8_t message_type, const std::span<const uint8_t> payload) {
this->set_message(message_type, payload);
}
NciMessage::NciMessage(const uint8_t message_type, const std::initializer_list<uint8_t> payload)
: NciMessage(message_type, std::span<const uint8_t>(payload.begin(), payload.size())) {}
NciMessage::NciMessage(const uint8_t message_type, const uint8_t gid, const uint8_t oid) {
this->reset();
this->set_header(message_type, gid, oid);
}
NciMessage::NciMessage(const uint8_t message_type, const uint8_t gid, const uint8_t oid,
const std::vector<uint8_t> &payload) {
const std::span<const uint8_t> payload) {
this->set_message(message_type, gid, oid, payload);
}
NciMessage::NciMessage(const std::vector<uint8_t> &raw_packet) { this->nci_message_ = raw_packet; };
NciMessage::NciMessage(const uint8_t message_type, const uint8_t gid, const uint8_t oid,
const std::initializer_list<uint8_t> payload)
: NciMessage(message_type, gid, oid, std::span<const uint8_t>(payload.begin(), payload.size())) {}
std::vector<uint8_t> NciMessage::encode() {
this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - nfc::NCI_PKT_HEADER_SIZE;
std::vector<uint8_t> message = this->nci_message_;
return message;
NciMessage::NciMessage(const std::span<const uint8_t> raw_packet) {
this->nci_message_.assign(raw_packet.begin(), raw_packet.end());
}
void NciMessage::reset() { this->nci_message_ = {0, 0, 0}; }
uint8_t NciMessage::get_message_type() const {
return this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & nfc::NCI_PKT_MT_MASK;
std::span<const uint8_t> NciMessage::encode() {
this->nci_message_[NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - NCI_PKT_HEADER_SIZE;
return this->nci_message_;
}
uint8_t NciMessage::get_gid() const { return this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & nfc::NCI_PKT_GID_MASK; }
void NciMessage::reset() {
this->nci_message_.resize(NCI_PKT_HEADER_SIZE);
this->nci_message_[NCI_PKT_MT_GID_OFFSET] = 0;
this->nci_message_[NCI_PKT_OID_OFFSET] = 0;
this->nci_message_[NCI_PKT_LENGTH_OFFSET] = 0;
}
uint8_t NciMessage::get_oid() const { return this->nci_message_[nfc::NCI_PKT_OID_OFFSET] & nfc::NCI_PKT_OID_MASK; }
uint8_t NciMessage::get_message_type() const { return this->nci_message_[NCI_PKT_MT_GID_OFFSET] & NCI_PKT_MT_MASK; }
uint8_t NciMessage::get_gid() const { return this->nci_message_[NCI_PKT_MT_GID_OFFSET] & NCI_PKT_GID_MASK; }
uint8_t NciMessage::get_oid() const { return this->nci_message_[NCI_PKT_OID_OFFSET] & NCI_PKT_OID_MASK; }
uint8_t NciMessage::get_payload_size(const bool recompute) {
if (!this->nci_message_.empty()) {
if (recompute) {
this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - nfc::NCI_PKT_HEADER_SIZE;
this->nci_message_[NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - NCI_PKT_HEADER_SIZE;
}
return this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET];
return this->nci_message_[NCI_PKT_LENGTH_OFFSET];
}
return 0;
}
uint8_t NciMessage::get_simple_status_response() const {
if (this->nci_message_.size() > nfc::NCI_PKT_PAYLOAD_OFFSET) {
return this->nci_message_[nfc::NCI_PKT_PAYLOAD_OFFSET];
if (this->nci_message_.size() > NCI_PKT_PAYLOAD_OFFSET) {
return this->nci_message_[NCI_PKT_PAYLOAD_OFFSET];
}
return STATUS_FAILED;
}
@@ -63,104 +71,112 @@ uint8_t NciMessage::get_message_byte(const uint8_t offset) const {
return 0;
}
std::vector<uint8_t> &NciMessage::get_message() { return this->nci_message_; }
std::span<const uint8_t> NciMessage::get_payload() const {
if (this->nci_message_.size() <= NCI_PKT_HEADER_SIZE) {
return {};
}
return {this->nci_message_.data() + NCI_PKT_HEADER_SIZE, this->nci_message_.size() - NCI_PKT_HEADER_SIZE};
}
bool NciMessage::has_payload() const { return this->nci_message_.size() > nfc::NCI_PKT_HEADER_SIZE; }
bool NciMessage::has_payload() const { return this->nci_message_.size() > NCI_PKT_HEADER_SIZE; }
bool NciMessage::message_type_is(const uint8_t message_type) const {
if (!this->nci_message_.empty()) {
return message_type == (this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & nfc::NCI_PKT_MT_MASK);
return message_type == (this->nci_message_[NCI_PKT_MT_GID_OFFSET] & NCI_PKT_MT_MASK);
}
return false;
}
bool NciMessage::message_length_is(const uint8_t message_length, const bool recompute) {
if (this->nci_message_.size() > nfc::NCI_PKT_LENGTH_OFFSET) {
if (this->nci_message_.size() > NCI_PKT_LENGTH_OFFSET) {
if (recompute) {
this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - nfc::NCI_PKT_HEADER_SIZE;
this->nci_message_[NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - NCI_PKT_HEADER_SIZE;
}
return message_length == this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET];
return message_length == this->nci_message_[NCI_PKT_LENGTH_OFFSET];
}
return false;
}
bool NciMessage::gid_is(const uint8_t gid) const {
if (this->nci_message_.size() > nfc::NCI_PKT_MT_GID_OFFSET) {
return gid == (this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & nfc::NCI_PKT_GID_MASK);
if (this->nci_message_.size() > NCI_PKT_MT_GID_OFFSET) {
return gid == (this->nci_message_[NCI_PKT_MT_GID_OFFSET] & NCI_PKT_GID_MASK);
}
return false;
}
bool NciMessage::oid_is(const uint8_t oid) const {
if (this->nci_message_.size() > nfc::NCI_PKT_OID_OFFSET) {
return oid == (this->nci_message_[nfc::NCI_PKT_OID_OFFSET] & nfc::NCI_PKT_OID_MASK);
if (this->nci_message_.size() > NCI_PKT_OID_OFFSET) {
return oid == (this->nci_message_[NCI_PKT_OID_OFFSET] & NCI_PKT_OID_MASK);
}
return false;
}
bool NciMessage::simple_status_response_is(const uint8_t response) const {
if (this->nci_message_.size() > nfc::NCI_PKT_PAYLOAD_OFFSET) {
return response == this->nci_message_[nfc::NCI_PKT_PAYLOAD_OFFSET];
if (this->nci_message_.size() > NCI_PKT_PAYLOAD_OFFSET) {
return response == this->nci_message_[NCI_PKT_PAYLOAD_OFFSET];
}
return false;
}
void NciMessage::set_header(const uint8_t message_type, const uint8_t gid, const uint8_t oid) {
if (this->nci_message_.size() < nfc::NCI_PKT_HEADER_SIZE) {
this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE);
if (this->nci_message_.size() < NCI_PKT_HEADER_SIZE) {
this->nci_message_.resize(NCI_PKT_HEADER_SIZE);
}
this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] =
(message_type & nfc::NCI_PKT_MT_MASK) | (gid & nfc::NCI_PKT_GID_MASK);
this->nci_message_[nfc::NCI_PKT_OID_OFFSET] = oid & nfc::NCI_PKT_OID_MASK;
this->nci_message_[NCI_PKT_MT_GID_OFFSET] = (message_type & NCI_PKT_MT_MASK) | (gid & NCI_PKT_GID_MASK);
this->nci_message_[NCI_PKT_OID_OFFSET] = oid & NCI_PKT_OID_MASK;
}
void NciMessage::set_message(const uint8_t message_type, const std::vector<uint8_t> &payload) {
this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE);
this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] = message_type & nfc::NCI_PKT_MT_MASK;
this->nci_message_[nfc::NCI_PKT_OID_OFFSET] = 0;
this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = payload.size();
this->nci_message_.insert(this->nci_message_.end(), payload.begin(), payload.end());
void NciMessage::set_message(const uint8_t message_type, const std::span<const uint8_t> payload) {
this->nci_message_.resize(NCI_PKT_HEADER_SIZE);
this->nci_message_[NCI_PKT_MT_GID_OFFSET] = message_type & NCI_PKT_MT_MASK;
this->nci_message_[NCI_PKT_OID_OFFSET] = 0;
this->append(payload);
this->nci_message_[NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - NCI_PKT_HEADER_SIZE;
}
void NciMessage::set_message(const uint8_t message_type, const uint8_t gid, const uint8_t oid,
const std::vector<uint8_t> &payload) {
this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE);
this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] =
(message_type & nfc::NCI_PKT_MT_MASK) | (gid & nfc::NCI_PKT_GID_MASK);
this->nci_message_[nfc::NCI_PKT_OID_OFFSET] = oid & nfc::NCI_PKT_OID_MASK;
this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = payload.size();
this->nci_message_.insert(this->nci_message_.end(), payload.begin(), payload.end());
const std::span<const uint8_t> payload) {
this->nci_message_.resize(NCI_PKT_HEADER_SIZE);
this->set_header(message_type, gid, oid);
this->append(payload);
this->nci_message_[NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - NCI_PKT_HEADER_SIZE;
}
void NciMessage::set_message_type(const uint8_t message_type) {
if (this->nci_message_.size() < nfc::NCI_PKT_HEADER_SIZE) {
this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE);
if (this->nci_message_.size() < NCI_PKT_HEADER_SIZE) {
this->nci_message_.resize(NCI_PKT_HEADER_SIZE);
}
auto mt_masked = this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & ~nfc::NCI_PKT_MT_MASK;
this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] = mt_masked | (message_type & nfc::NCI_PKT_MT_MASK);
auto mt_masked = this->nci_message_[NCI_PKT_MT_GID_OFFSET] & ~NCI_PKT_MT_MASK;
this->nci_message_[NCI_PKT_MT_GID_OFFSET] = mt_masked | (message_type & NCI_PKT_MT_MASK);
}
void NciMessage::set_gid(const uint8_t gid) {
if (this->nci_message_.size() < nfc::NCI_PKT_HEADER_SIZE) {
this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE);
if (this->nci_message_.size() < NCI_PKT_HEADER_SIZE) {
this->nci_message_.resize(NCI_PKT_HEADER_SIZE);
}
auto gid_masked = this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & ~nfc::NCI_PKT_GID_MASK;
this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] = gid_masked | (gid & nfc::NCI_PKT_GID_MASK);
auto gid_masked = this->nci_message_[NCI_PKT_MT_GID_OFFSET] & ~NCI_PKT_GID_MASK;
this->nci_message_[NCI_PKT_MT_GID_OFFSET] = gid_masked | (gid & NCI_PKT_GID_MASK);
}
void NciMessage::set_oid(const uint8_t oid) {
if (this->nci_message_.size() < nfc::NCI_PKT_HEADER_SIZE) {
this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE);
if (this->nci_message_.size() < NCI_PKT_HEADER_SIZE) {
this->nci_message_.resize(NCI_PKT_HEADER_SIZE);
}
this->nci_message_[nfc::NCI_PKT_OID_OFFSET] = oid & nfc::NCI_PKT_OID_MASK;
this->nci_message_[NCI_PKT_OID_OFFSET] = oid & NCI_PKT_OID_MASK;
}
void NciMessage::set_payload(const std::vector<uint8_t> &payload) {
std::vector<uint8_t> message(this->nci_message_.begin(), this->nci_message_.begin() + nfc::NCI_PKT_HEADER_SIZE);
message.insert(message.end(), payload.begin(), payload.end());
message[nfc::NCI_PKT_LENGTH_OFFSET] = payload.size();
this->nci_message_ = message;
void NciMessage::set_payload(const std::span<const uint8_t> payload) {
this->nci_message_.resize(NCI_PKT_HEADER_SIZE);
this->append(payload);
this->nci_message_[NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - NCI_PKT_HEADER_SIZE;
}
void NciMessage::append(const std::span<const uint8_t> data) {
for (const uint8_t byte : data) {
this->nci_message_.push_back(byte);
}
}
void NciMessage::set_payload_size(const uint8_t size) { this->nci_message_.resize(NCI_PKT_HEADER_SIZE + size); }
} // namespace esphome::nfc
+36 -12
View File
@@ -3,19 +3,30 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <vector>
#include <initializer_list>
#include <span>
namespace esphome::nfc {
// An NCI packet is a three-byte header followed by up to 255 payload bytes
static constexpr size_t NCI_PKT_MAX_PAYLOAD_SIZE = 255;
static constexpr size_t NCI_PKT_MAX_SIZE = 3 + NCI_PKT_MAX_PAYLOAD_SIZE;
/// One NCI packet, held in a fixed buffer so building and reading messages never allocates
class NciMessage {
public:
NciMessage() {}
NciMessage(uint8_t message_type, const std::vector<uint8_t> &payload);
NciMessage(uint8_t message_type, uint8_t gid, uint8_t oid);
NciMessage(uint8_t message_type, uint8_t gid, uint8_t oid, const std::vector<uint8_t> &payload);
NciMessage(const std::vector<uint8_t> &raw_packet);
using Buffer = StaticVector<uint8_t, NCI_PKT_MAX_SIZE>;
std::vector<uint8_t> encode();
NciMessage() { this->reset(); }
NciMessage(uint8_t message_type, std::span<const uint8_t> payload);
NciMessage(uint8_t message_type, std::initializer_list<uint8_t> payload);
NciMessage(uint8_t message_type, uint8_t gid, uint8_t oid);
NciMessage(uint8_t message_type, uint8_t gid, uint8_t oid, std::span<const uint8_t> payload);
NciMessage(uint8_t message_type, uint8_t gid, uint8_t oid, std::initializer_list<uint8_t> payload);
explicit NciMessage(std::span<const uint8_t> raw_packet);
/// Stamps the payload length into the header and returns the packet ready to send
std::span<const uint8_t> encode();
void reset();
uint8_t get_message_type() const;
@@ -24,7 +35,10 @@ class NciMessage {
uint8_t get_payload_size(bool recompute = false);
uint8_t get_simple_status_response() const;
uint8_t get_message_byte(uint8_t offset) const;
std::vector<uint8_t> &get_message();
Buffer &get_message() { return this->nci_message_; }
const Buffer &get_message() const { return this->nci_message_; }
/// The payload bytes that follow the header
std::span<const uint8_t> get_payload() const;
bool has_payload() const;
bool message_type_is(uint8_t message_type) const;
@@ -34,15 +48,25 @@ class NciMessage {
bool simple_status_response_is(uint8_t response) const;
void set_header(uint8_t message_type, uint8_t gid, uint8_t oid);
void set_message(uint8_t message_type, const std::vector<uint8_t> &payload);
void set_message(uint8_t message_type, uint8_t gid, uint8_t oid, const std::vector<uint8_t> &payload);
void set_message(uint8_t message_type, std::span<const uint8_t> payload);
void set_message(uint8_t message_type, uint8_t gid, uint8_t oid, std::span<const uint8_t> payload);
void set_message_type(uint8_t message_type);
void set_gid(uint8_t gid);
void set_oid(uint8_t oid);
void set_payload(const std::vector<uint8_t> &payload);
void set_payload(std::span<const uint8_t> payload);
void set_payload(std::initializer_list<uint8_t> payload) {
this->set_payload(std::span<const uint8_t>(payload.begin(), payload.size()));
}
/// Appends bytes to the payload; bytes that do not fit are dropped
void append(std::span<const uint8_t> data);
void append(std::initializer_list<uint8_t> data) {
this->append(std::span<const uint8_t>(data.begin(), data.size()));
}
/// Sets the packet size to the header plus `size` payload bytes, for a bus driver filling the buffer directly
void set_payload_size(uint8_t size);
protected:
std::vector<uint8_t> nci_message_{0, 0, 0}; // three bytes, MT/PBF/GID, OID, payload length/size
Buffer nci_message_; // MT/PBF/GID, OID, payload length, then the payload
};
} // namespace esphome::nfc
+1 -1
View File
@@ -5,7 +5,7 @@ namespace esphome::nfc {
static const char *const TAG = "nfc.ndef_message";
NdefMessage::NdefMessage(std::vector<uint8_t> &data) {
NdefMessage::NdefMessage(const std::span<const uint8_t> data) {
ESP_LOGV(TAG, "Building NdefMessage with %zu bytes", data.size());
size_t index = 0;
while (index < data.size()) {

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