Merge branch 'dev' into noise-combo-test

This commit is contained in:
J. Nick Koston
2026-09-24 11:07:42 +01:00
committed by GitHub
278 changed files with 7058 additions and 5616 deletions
+116
View File
@@ -0,0 +1,116 @@
---
name: code-review
description: Review guidance for ESPHome pull requests. Use this when reviewing a pull request that changes ESPHome Python, C++, or component code, to check it against the project's coding conventions, embedded-systems memory rules, testing requirements, and breaking-change policy.
---
# Reviewing ESPHome pull requests
ESPHome parses YAML into C++ firmware for memory-constrained microcontrollers
(ESP32, ESP8266, RP2040, LibreTiny). Review changes with that in mind: RAM and
flash are scarce, and code runs unattended for months.
`AGENTS.md` in the repository root is the full contributor guide and the
authority when it disagrees with this summary. The developer documentation at
https://developers.esphome.io explains the component lifecycle and the reasoning
behind these rules. This skill lists the concrete things worth flagging in a
review; read `AGENTS.md` for the detail behind any item.
Only raise findings that the diff actually introduces or changes. Do not ask for
drive-by cleanup of pre-existing code the PR did not touch.
## Memory and embedded constraints (highest value)
Heap allocation after `setup()` is treated as a reliability bug, not a
performance nit, because it fragments a small shared heap. Flag:
- New heap allocation on a hot path or after setup that could be avoided.
- `std::vector` where the size is known at compile time (use `std::array`, or
`StaticVector<T, N>` when a `push_back` API is needed) or fixed at runtime
init (use `FixedVector<T>`).
- Listener / child-entity registration lists stored as `std::vector`; these have
a compile-time-known count and should use `cg.slot_counter()` plus
`StaticVector`.
- `std::vector<uint8_t>` for a byte buffer that never grows: prefer
`std::unique_ptr<uint8_t[]>` or `std::array`.
- `std::map` / `std::set` / `std::unordered_map` for small datasets (1-16
elements): a `std::vector` of a small struct with linear search is lighter.
- `std::deque` anywhere: it allocates 512-byte blocks and should be avoided.
- `std::string` storing a value set once from config: prefer `StringRef` (the
literal already lives in flash).
- `std::string` / `std::to_string` / string-returning helpers on hot paths where
a buffer or view API exists.
## C++ conventions
- Include what you use: a file referencing a symbol must include the header that
declares it, even if it currently arrives transitively. New or changed uses of
a symbol need the matching include.
- Prefix all member access with `this->`.
- Naming: `lower_snake_case` for functions/methods/variables, `UpperCamelCase`
for classes/structs/enums, `UPPER_SNAKE_CASE` for namespace-scope constants,
trailing underscore on protected/private fields.
- `enum class` values must be prefixed with the enum name in `UPPER_SNAKE_CASE`
(e.g. `UARTFlushResult::UART_FLUSH_RESULT_SUCCESS`). Bare names like `SUCCESS`,
`FAIL`, or `OK` collide with SDK macros on some platforms and break the build.
- Prefer `const`/`enum` over `#define`; `#define` is only for conditional
compilation and code-generation sizes.
- Never call `millis()` in a `loop()` body; use
`App.get_loop_component_start_time()`. A rate-limit gate below ~16 ms (the loop
period) does nothing.
- Pick the timing primitive by cadence: gated `loop()` under 250 ms,
`set_interval` at 500 ms and above.
- Do not override a base method to return the value it already returns (e.g.
`get_setup_priority()` returning `setup_priority::DATA`).
- Wrap string literals passed as printf `%s` args in `LOG_STR_LITERAL()`.
- Required, invariant dependencies should be constructor parameters, not setters.
- Callback registration methods must be templated (`template<typename F>`), not
typed as `std::function`, so lightweight forwarders avoid a heap allocation.
- Two-space indent, `using` over `typedef`, wrap at 120 columns.
## Python conventions
- Type-annotate every new function signature (params and return), new dataclass
fields, and new module-level variables. Import `ConfigType` from
`esphome.types`.
- Use the walrus operator to avoid a double lookup, e.g.
`if (blah := config.get(CONF_BLAH)) is not None:`.
- Reuse existing validators from `config_validation.py` (`cv.rename_key`,
`cv.has_exactly_one_key`, etc.) via `cv.All(...)` instead of hand-rolling.
- `esphome/const.py` is frozen: no new `CONF_` constants there. Define them in
the component's own `.py`, or in `esphome/components/const/__init__.py` when
shared. The same constant defined in three or more component files fails CI.
- State that must persist during code generation goes in `CORE.data` namespaced
under the component `DOMAIN` (a `@dataclass`), not module-level mutable globals.
- Prefer callback-based triggers via `build_callback_automation()`; only use a
`Trigger<Ts...>` subclass when the forwarder needs mutable state.
## Testing and coverage
- New and changed lines and branches need test coverage, including defensive
early-returns, error paths, and no-op guards. A mocked-out function is not
covered; exercise the real call path too.
- Component YAML tests live in `tests/components/<component>/`. Never define
buses (uart, i2c, spi, modbus) directly in a test file: pull them from
`tests/test_build_components/common/` through dict-style `packages:` so CI can
group builds. List-style packages or top-level merge keys block grouping.
- Config-only checks use the `validate.*.yaml` prefix; compiled checks use
`test.*.yaml`.
## Breaking changes and public API
- Base classes under `esphome/core/` and documented config options are public
API. Undocumented `public` members of a component are internal.
- A breaking change needs justification, a migration path in the PR description,
and a deprecation window where feasible (`ESPDEPRECATED` in C++,
`cv.rename_key(..., removed_in=...)` in Python). Changing a codegen-injected
lambda signature is not a breaking change.
## Process and PR hygiene
- PR titles start with a `[tag]` prefix: the component name (e.g. `[uart] ...`)
or `[core]` for shared code.
- Prose in docs, comments, and commit messages should be plain English. Keep
inline comments short and only where the code is not self-explanatory; do not
restate what the code says.
- Verify the PR fills out `.github/PULL_REQUEST_TEMPLATE.md` and adds
`CODEOWNERS` entries for a new component.
-3
View File
@@ -31,7 +31,4 @@ RUN \
platformio settings set enable_telemetry No \
&& platformio settings set check_platformio_interval 1000000
COPY script/platformio_install_deps.py platformio.ini ./
RUN ./platformio_install_deps.py platformio.ini --libraries --platforms --tools
WORKDIR /workspaces
+2 -2
View File
@@ -42,7 +42,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
@@ -67,7 +67,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
+2 -4
View File
@@ -12,9 +12,7 @@ on:
- ".github/workflows/ci-docker.yml"
- "requirements*.txt"
- "pyproject.toml"
- "platformio.ini"
- "esphome/idf_component.yml"
- "script/platformio_install_deps.py"
# Core, build pipeline, toolchain, and target-platform changes can change
# how a toolchain is set up or built, so re-run the per-toolchain compile
# smoke test when they change.
@@ -67,7 +65,7 @@ jobs:
with:
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- name: Determine tag and whether to push
id: tag
@@ -159,7 +157,7 @@ jobs:
with:
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- name: Log in to the GitHub container registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
+2 -2
View File
@@ -321,7 +321,7 @@ jobs:
. venv/bin/activate
pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@0b35c9ecc4f0529d0eb674914510c22f85b196b4 # v7.1.0
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
- name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
@@ -350,7 +350,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Report empty upload to Codecov
uses: codecov/codecov-action@0b35c9ecc4f0529d0eb674914510c22f85b196b4 # v7.1.0
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
run_command: empty-upload
force: true
+2 -2
View File
@@ -56,7 +56,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -84,6 +84,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -14,4 +14,4 @@ jobs:
permissions:
issues: write # issues.lock on closed issues
pull-requests: write # issues.lock on closed pull requests
uses: esphome/workflows/.github/workflows/lock.yml@0fdd5e311b7e744069166696072a1a9cbc5fbeb6 # 2026.8.1
uses: esphome/workflows/.github/workflows/lock.yml@cc3e76de337dc59bc1cba8da58d963cd23b873f1 # 2026.9.0
+2 -2
View File
@@ -123,7 +123,7 @@ jobs:
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- name: Log in to docker hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
@@ -202,7 +202,7 @@ jobs:
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
# No GITHUB_TOKEN permissions: the reusable workflow mints an ESPHome
# GitHub App token so the labels, comments and closures come from
# esphome[bot] instead of github-actions[bot].
uses: esphome/workflows/.github/workflows/stale.yml@a1c1485ab46ef41a84a6a9d8abd7fa4b7628fd70 # main
uses: esphome/workflows/.github/workflows/stale.yml@cc3e76de337dc59bc1cba8da58d963cd23b873f1 # main
secrets:
ESPHOME_GITHUB_APP_PRIVATE_KEY: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
with:
+1 -1
View File
@@ -10,7 +10,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.16.7
rev: v0.16.8
hooks:
# Run the linter.
- id: ruff
+27
View File
@@ -443,6 +443,20 @@ file does, and it is the authority when they disagree. The most useful starting
Use `synchronous=True` for actions that run to completion inside `play()` without deferring. Use `synchronous=False` if the action may suspend/defer execution (e.g. `delay`, `wait_until`, `script.wait`) or store trigger arguments for later use.
**Actions that only forward templatable values to their parent need no C++ class.** Register them
with `register_apply_action`; do not write a `TEMPLATABLE_VALUE` class or a builder for this shape.
```python
automation.register_apply_action(
"my_component.set_gains",
schema,
automation.ApplyField(CONF_KP, "set_kp", cg.float_),
automation.ApplyField(CONF_KI, "set_ki", cg.float_),
)
```
The `ApplyField`, `ApplyCall` and `register_apply_action` docstrings in `esphome/automation.py` cover
the rest; `cover.control` and `cover.template.publish` are in-tree examples. `TEMPLATABLE_VALUE` with
`cg.templatable` stays for actions whose `play()` has real logic beyond forwarding values.
* **Conditions:**
```cpp
template<typename... Ts> class MyCondition : public Condition<Ts...> {
@@ -456,6 +470,19 @@ file does, and it is the authority when they disagree. The most useful starting
Register with `automation.register_simple_condition("my_component.is_active", MyCondition, schema)`;
`register_bare_condition`, `register_parented_condition` and the decorator follow the action rules.
**Conditions that only test their parent need no C++ class either.** Register them with
`register_apply_condition`; the expression is applied to the parent, and an `ApplyCall` compares
against config values.
```python
automation.register_apply_condition("my_component.is_active", schema, "is_active()")
automation.register_apply_condition(
"my_component.state_is",
schema,
automation.ApplyCall("state == {}", ((CONF_STATE, cg.bool_),)),
)
```
`cover.is_open`, `rtttl.is_playing` and `component.is_idle` are in-tree examples.
* **Type Hints:** Type-hint all function signatures, including test functions and config validators (e.g. `def validate_x(config: ConfigType) -> ConfigType:`, `def test_x() -> None:`). Import `ConfigType` from `esphome.types`.
* **Configuration Validation:**
+1
View File
@@ -457,6 +457,7 @@ esphome/components/rtl87xx/* @kuba2k2
esphome/components/rtttl/* @glmnet @ximex
esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt
esphome/components/runtime_stats/* @bdraco
esphome/components/rx8025t/* @remcom
esphome/components/rx8130/* @beormund
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
esphome/components/scd4x/* @martgras @sjtrny
+1 -4
View File
@@ -22,16 +22,13 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.9
RUN uv pip install --no-cache-dir esphome-device-builder==1.15.0
RUN \
platformio settings set enable_telemetry No \
&& platformio settings set check_platformio_interval 1000000 \
&& mkdir -p /piolibs
COPY script/platformio_install_deps.py platformio.ini /
RUN /platformio_install_deps.py /platformio.ini --libraries
ARG BUILD_VERSION
LABEL \
+12
View File
@@ -5,3 +5,15 @@ bk72xx:
board: generic-bk7231n-qfn32-tuya
logger:
wifi:
ssid: MySSID
password: password1
ap:
# mqtt and captive_portal together pull in AsyncTCP and ESPAsyncWebServer;
# a stray ESP32 AsyncTCP copy on the library search path breaks this build
captive_portal:
mqtt:
broker: 192.168.178.84
+282 -17
View File
@@ -1,5 +1,8 @@
from collections.abc import Callable
from dataclasses import dataclass, field
import logging
import string
from typing import Any
import esphome.codegen as cg
import esphome.config_validation as cv
@@ -18,15 +21,17 @@ from esphome.const import (
CONF_TYPE_ID,
CONF_UPDATE_INTERVAL,
)
from esphome.core import ID, Lambda
from esphome.core import CORE, ID, EsphomeError, Lambda
from esphome.cpp_generator import (
FlashStringLiteral,
LambdaExpression,
MockObj,
MockObjClass,
TemplateArgsType,
call_lambda,
)
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType
from esphome.types import ConfigType, SafeExpType
from esphome.util import Registry
@@ -57,6 +62,7 @@ def maybe_conf(conf, *validators):
with cv.remove_prepend_path([conf]):
return validator({conf: value})
validate.inner_schema = validator
return validate
@@ -207,6 +213,278 @@ validate_action_list = cv.validate_registry("action", ACTION_REGISTRY)
validate_condition = cv.validate_registry_entry("condition", CONDITION_REGISTRY)
validate_condition_list = cv.validate_registry("condition", CONDITION_REGISTRY)
ApplyAction = cg.esphome_ns.class_("ApplyAction", Action)
ApplyCondition = cg.esphome_ns.class_("ApplyCondition", Condition)
def flash_string(config: ConfigType, value: str) -> str:
"""Default renderer for ``std::string`` constants; copies the literal out of flash on ESP8266."""
if CORE.is_esp8266:
return f"progmem_string({FlashStringLiteral(value)})"
return str(cg.safe_exp(value))
@dataclass(frozen=True)
class ApplyCall:
"""One statement from config keys, e.g. ``"set_range({}, {})"`` with ``((CONF_LOW, cg.float_), ...)``.
Each arg is ``(conf_key, type_)`` or ``(conf_key, type_, const_fn)``. A ``conf_key`` may be a
path into nested sections. A plain ``str`` ``type_`` is raw C++ type text and may use
``{parent}``. ``const_fn(config, value)`` renders a constant's argument text; a lambda bypasses
it. The statement is skipped when none of its keys is set, always emitted when it has no
keys, and a partial set is a config error.
"""
target: str
args: tuple[tuple[Any, ...], ...] = ()
def __post_init__(self) -> None:
fields = [
f for _, f, _, _ in string.Formatter().parse(self.target) if f is not None
]
if any(fields):
raise ValueError(
f"apply target {self.target!r}: only bare {{}} placeholders"
)
if len(fields) != len(self.args):
raise ValueError(
f"apply target {self.target!r} has {len(fields)} "
f"placeholder(s) for {len(self.args)} config key(s)"
)
if any(len(arg) not in (2, 3) for arg in self.args):
raise ValueError(
f"apply target {self.target!r}: each arg is (conf_key, type_[, const_fn])"
)
@property
def members(self) -> list[tuple[Any, Any, Any]]:
"""Each arg as ``(conf_key, type_, const_fn or None)``."""
return [
(arg[0], arg[1], arg[2] if len(arg) == 3 else None) for arg in self.args
]
@dataclass(frozen=True)
class ApplyField:
"""One config key forwarded as ``target(value)``, or as statement ``target`` when it has ``{}``.
Double a literal brace in a template. ``conf_key`` may be a path into nested sections.
``type_`` may be a C++ type string using ``{parent}`` when the type is only known per
instance. ``const_fn(config, value)`` renders a constant's argument text when ``cg.safe_exp``
is not the right spelling (unit conversion belongs in the validator); a lambda bypasses it,
so the target must also take a plain ``type_``. An absent key emits nothing.
"""
conf_key: str | tuple[str, ...]
target: str
type_: SafeExpType
const_fn: Callable[[ConfigType, Any], str] | None = None
def call(self) -> ApplyCall:
target = self.target if "{}" in self.target else f"{self.target}({{}})"
return ApplyCall(target, ((self.conf_key, self.type_, self.const_fn),))
def _config_lookup(config: ConfigType, key: str | tuple[str, ...]) -> Any:
if isinstance(key, str):
return config.get(key)
for part in key:
if (config := config.get(part)) is None:
return None
return config
def _dict_schema(schema: Any) -> Any:
"""The dict-backed cv.Schema inside cv.All and maybe_* wrappers, or None; cv.Any is not inspected."""
if isinstance(schema, dict):
return cv.Schema(schema)
if isinstance(getattr(schema, "schema", None), dict):
return schema
if isinstance(schema, cv.All):
inner = schema.validators
else:
inner = (
getattr(schema, "inner_schema", None),
) # maybe_conf / maybe_simple_value
for candidate in inner:
if candidate is not None and (found := _dict_schema(candidate)) is not None:
return found
return None
def _check_key_in_schema(
name: str, schema: Any, conf_key: str | tuple[str, ...]
) -> None:
"""Reject a key path the schema does not have; a typo would otherwise be a silent no-op.
Only dict-backed schemas, also inside cv.All and maybe_* wrappers, can be checked.
"""
for part in (conf_key,) if isinstance(conf_key, str) else conf_key:
if (schema := _dict_schema(schema)) is None:
return
markers = {
getattr(marker, "schema", marker): marker for marker in schema.schema
}
if part not in markers:
raise ValueError(f"{name}: config key {part!r} is not in the schema")
schema = schema.schema[markers[part]]
async def _apply_parent(config: ConfigType) -> str:
# Global-scope qualified so a trigger arg named like the id cannot shadow it.
return f"::{await cg.get_variable(config[CONF_ID])}"
def _apply_lambda_args(args: TemplateArgsType) -> TemplateArgsType:
# Must match ApplyAction::ApplyFn and ApplyCondition::CheckFn exactly for the function
# pointer conversion.
return [
(cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), arg)
for t, arg in args
]
async def _render_values(
name: str,
target: str,
members: list[tuple[Any, Any, Any]],
values: list[Any],
config: ConfigType,
parent: str,
lambda_args: TemplateArgsType,
compare: bool = False,
) -> list[str]:
"""Render the argument text of one statement; every key must be present.
``compare``: an inlined lambda expression is parenthesized so it binds as a whole
beside an operator.
"""
if any(value is None for value in values):
keys = [key for key, _, _ in members]
raise EsphomeError(f"{name}: {target!r} needs all of {keys}")
exprs: list[str] = []
for (_, type_, const_fn), value in zip(members, values, strict=True):
if isinstance(value, Lambda):
if isinstance(type_, str):
type_ = cg.RawExpression(type_.format(parent=parent))
inner = await cg.process_lambda(value, lambda_args, return_type=type_)
expr = call_lambda(inner)
bare = compare and isinstance(expr, cg.RawExpression)
exprs.append(f"({expr})" if bare else str(expr))
elif const_fn is not None:
exprs.append(const_fn(config, value))
else:
exprs.append(str(cg.safe_exp(value)))
return exprs
def _apply_values(config: ConfigType, members: list[tuple[Any, Any, Any]]) -> list[Any]:
return [_config_lookup(config, key) for key, _, _ in members]
def register_apply_action(
name: str,
schema: cv.Schema,
*fields: ApplyField | ApplyCall,
call: str | None = None,
) -> None:
"""Register an action that only forwards config values to its parent, with no C++ class.
Generates one stateless function for ``ApplyAction<Ts...>``: parent and constants are baked
in, lambdas are called inline with the trigger args. With ``call`` every statement targets
the call object ``auto apply_call = parent->call()``, and ``apply_call.perform()`` is appended.
"""
# An action stores the value, so a std::string constant stays in flash on ESP8266.
statements_spec = [
(
c.target,
[
(key, t, fn or (flash_string if t is cg.std_string else None))
for key, t, fn in c.members
],
)
for c in (f if isinstance(f, ApplyCall) else f.call() for f in fields)
]
for _, members in statements_spec:
for conf_key, _, _ in members:
_check_key_in_schema(name, schema, conf_key)
async def builder(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
parent = await _apply_parent(config)
lambda_args = _apply_lambda_args(args)
receiver = "apply_call." if call else f"{parent}->"
statements: list[str] = []
for target, members in statements_spec:
values = _apply_values(config, members)
if members and all(value is None for value in values):
continue
exprs = await _render_values(
name, target, members, values, config, parent, lambda_args
)
statements.append(f"{receiver}{target.format(*exprs)};")
if call:
statements = [
f"auto apply_call = {parent}->{call}();",
*statements,
"apply_call.perform();",
]
apply_lambda = LambdaExpression(
["\n".join(statements)], lambda_args, capture="", return_type=cg.void
)
return cg.new_Pvariable(action_id, template_arg, apply_lambda)
register_action(name, ApplyAction, schema, synchronous=True)(builder)
def register_apply_condition(
name: str, schema: cv.Schema, check: str | ApplyCall
) -> None:
"""Register a condition that is one expression on its parent, with no C++ class.
``check`` is applied to the parent: ``"is_playing()"`` becomes ``parent->is_playing()``; an
``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...>``.
"""
call = check if isinstance(check, ApplyCall) else ApplyCall(check)
members = call.members
for conf_key, _, _ in members:
_check_key_in_schema(name, schema, conf_key)
async def builder(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
parent = await _apply_parent(config)
lambda_args = _apply_lambda_args(args)
exprs = await _render_values(
name,
call.target,
members,
_apply_values(config, members),
config,
parent,
lambda_args,
compare=True,
)
check_lambda = LambdaExpression(
[f"return {parent}->{call.target.format(*exprs)};"],
lambda_args,
capture="",
return_type=cg.bool_,
)
return cg.new_Pvariable(condition_id, template_arg, check_lambda)
register_condition(name, ApplyCondition, schema)(builder)
def validate_potentially_and_condition(value):
if isinstance(value, list):
@@ -454,28 +732,15 @@ async def for_condition_to_code(
return var
@register_condition(
register_apply_condition(
"component.is_idle",
LambdaCondition,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(cg.Component),
}
),
"is_idle()",
)
async def component_is_idle_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
comp = await cg.get_variable(config[CONF_ID])
lambda_ = await cg.process_lambda(
Lambda(f"return {comp}->is_idle();"), args, return_type=bool
)
return new_lambda_pvariable(
condition_id, lambda_, StatelessLambdaCondition, template_arg
)
@register_action(
File diff suppressed because it is too large Load Diff
@@ -41,10 +41,6 @@ StateAnyForwarder = alarm_control_panel_ns.class_("StateAnyForwarder")
StateEnterForwarder = alarm_control_panel_ns.class_("StateEnterForwarder")
AlarmControlPanelState = alarm_control_panel_ns.enum("AlarmControlPanelState")
ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action)
ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action)
ArmNightAction = alarm_control_panel_ns.class_("ArmNightAction", automation.Action)
DisarmAction = alarm_control_panel_ns.class_("DisarmAction", automation.Action)
PendingAction = alarm_control_panel_ns.class_("PendingAction", automation.Action)
TriggeredAction = alarm_control_panel_ns.class_("TriggeredAction", automation.Action)
ChimeAction = alarm_control_panel_ns.class_("ChimeAction", automation.Action)
@@ -196,64 +192,20 @@ async def new_alarm_control_panel(config, *args):
return var
@automation.register_action(
"alarm_control_panel.arm_away",
ArmAwayAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_arm_away_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if code_config := config.get(CONF_CODE):
templatable_ = await cg.templatable(code_config, args, cg.std_string)
cg.add(var.set_code(templatable_))
return var
@automation.register_action(
"alarm_control_panel.arm_home",
ArmHomeAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_arm_home_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if code_config := config.get(CONF_CODE):
templatable_ = await cg.templatable(code_config, args, cg.std_string)
cg.add(var.set_code(templatable_))
return var
@automation.register_action(
"alarm_control_panel.arm_night",
ArmNightAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_arm_night_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if CONF_CODE in config:
templatable_ = await cg.templatable(config[CONF_CODE], args, cg.std_string)
cg.add(var.set_code(templatable_))
return var
@automation.register_action(
"alarm_control_panel.disarm",
DisarmAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_disarm_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if code_config := config.get(CONF_CODE):
templatable_ = await cg.templatable(code_config, args, cg.std_string)
cg.add(var.set_code(templatable_))
return var
# Mirrors AlarmControlPanel::arm_with_code_: arm first, set the code only when given.
for _name, _arm in (
("alarm_control_panel.arm_away", "arm_away()"),
("alarm_control_panel.arm_home", "arm_home()"),
("alarm_control_panel.arm_night", "arm_night()"),
("alarm_control_panel.disarm", "disarm()"),
):
automation.register_apply_action(
_name,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
automation.ApplyCall(_arm),
automation.ApplyField(CONF_CODE, "set_code", cg.std_string),
call="make_call",
)
@automation.register_action(
@@ -27,54 +27,6 @@ static_assert(std::is_trivially_copyable_v<StateAnyForwarder>);
static_assert(sizeof(StateEnterForwarder<ACP_STATE_TRIGGERED>) <= sizeof(void *));
static_assert(std::is_trivially_copyable_v<StateEnterForwarder<ACP_STATE_TRIGGERED>>);
template<typename... Ts> class ArmAwayAction final : public Action<Ts...> {
public:
explicit ArmAwayAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { this->alarm_control_panel_->arm_away(this->code_.optional_value(x...)); }
protected:
AlarmControlPanel *alarm_control_panel_;
};
template<typename... Ts> class ArmHomeAction final : public Action<Ts...> {
public:
explicit ArmHomeAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { this->alarm_control_panel_->arm_home(this->code_.optional_value(x...)); }
protected:
AlarmControlPanel *alarm_control_panel_;
};
template<typename... Ts> class ArmNightAction final : public Action<Ts...> {
public:
explicit ArmNightAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { this->alarm_control_panel_->arm_night(this->code_.optional_value(x...)); }
protected:
AlarmControlPanel *alarm_control_panel_;
};
template<typename... Ts> class DisarmAction final : public Action<Ts...> {
public:
explicit DisarmAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); }
protected:
AlarmControlPanel *alarm_control_panel_;
};
template<typename... Ts> class PendingAction final : public Action<Ts...> {
public:
explicit PendingAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
+28 -77
View File
@@ -4,8 +4,6 @@ import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import CONF_FREQUENCY, CONF_ID
from esphome.core import ID
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
CODEOWNERS = ["@X-Ryl669"]
@@ -80,30 +78,21 @@ async def to_code(config: ConfigType) -> None:
# Actions
AT581XResetAction = at581x_ns.class_("AT581XResetAction", automation.Action)
AT581XSettingsAction = at581x_ns.class_("AT581XSettingsAction", automation.Action)
@automation.register_action(
automation.register_apply_action(
"at581x.reset",
AT581XResetAction,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(AT581XComponent),
}
),
synchronous=True,
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(AT581XComponent)}),
automation.ApplyCall("reset_hardware_frontend()"),
)
async def at581x_reset_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
def _megahertz(value: float) -> int:
return int(value / 1000000)
def _microamps(value: float) -> int:
return int(value * 1000000)
RADAR_SETTINGS_SCHEMA = cv.Schema(
@@ -111,7 +100,7 @@ RADAR_SETTINGS_SCHEMA = cv.Schema(
cv.Required(CONF_ID): cv.use_id(AT581XComponent),
cv.Optional(CONF_HW_FRONTEND_RESET): cv.templatable(cv.boolean),
cv.Optional(CONF_FREQUENCY, default="5800MHz"): cv.templatable(
cv.All(cv.frequency, cv.one_of(*RADAR_ALLOWED_FREQ))
cv.All(cv.frequency, cv.one_of(*RADAR_ALLOWED_FREQ), _megahertz)
),
cv.Optional(CONF_SENSING_DISTANCE, default=823): cv.templatable(
cv.int_range(min=0, max=1023)
@@ -123,7 +112,7 @@ RADAR_SETTINGS_SCHEMA = cv.Schema(
)
),
cv.Optional(CONF_POWER_CONSUMPTION, default="70uA"): cv.templatable(
cv.All(cv.current, cv.one_of(*RADAR_ALLOWED_CUR_CONSUMPTION))
cv.All(cv.current, cv.one_of(*RADAR_ALLOWED_CUR_CONSUMPTION), _microamps)
),
cv.Optional(CONF_PROTECT_TIME, default="1000ms"): cv.templatable(
cv.All(
@@ -165,60 +154,22 @@ RADAR_SETTINGS_SCHEMA = cv.Schema(
)
@automation.register_action(
# i2c_write_config() must follow the setters: it flushes the staged values.
automation.register_apply_action(
"at581x.settings",
AT581XSettingsAction,
RADAR_SETTINGS_SCHEMA,
synchronous=True,
automation.ApplyField(CONF_FREQUENCY, "set_frequency", cg.int_),
automation.ApplyField(CONF_SENSING_DISTANCE, "set_sensing_distance", cg.int_),
automation.ApplyField(
CONF_POWERON_SELFCHECK_TIME, "set_poweron_selfcheck_time", cg.int_
),
automation.ApplyField(CONF_POWER_CONSUMPTION, "set_power_consumption", cg.int_),
automation.ApplyField(CONF_PROTECT_TIME, "set_protect_time", cg.int_),
automation.ApplyField(CONF_TRIGGER_BASE, "set_trigger_base", cg.int_),
automation.ApplyField(CONF_TRIGGER_KEEP, "set_trigger_keep", cg.int_),
automation.ApplyField(CONF_STAGE_GAIN, "set_stage_gain", cg.int_),
automation.ApplyCall("i2c_write_config()"),
automation.ApplyField(
CONF_HW_FRONTEND_RESET, "reset_hardware_frontend_if", cg.bool_
),
)
async def at581x_settings_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
# Radar configuration
if frontend_reset := config.get(CONF_HW_FRONTEND_RESET):
template_ = await cg.templatable(frontend_reset, args, cg.int8)
cg.add(var.set_hw_frontend_reset(template_))
if freq := config.get(CONF_FREQUENCY):
if not cg.is_template(freq):
freq = int(freq / 1000000)
template_ = await cg.templatable(freq, args, cg.int_)
cg.add(var.set_frequency(template_))
if (sens_dist := config.get(CONF_SENSING_DISTANCE)) is not None:
template_ = await cg.templatable(sens_dist, args, cg.int_)
cg.add(var.set_sensing_distance(template_))
if selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME):
template_ = await cg.templatable(selfcheck, args, cg.int_)
cg.add(var.set_poweron_selfcheck_time(template_))
if protect := config.get(CONF_PROTECT_TIME):
template_ = await cg.templatable(protect, args, cg.int_)
cg.add(var.set_protect_time(template_))
if trig_base := config.get(CONF_TRIGGER_BASE):
template_ = await cg.templatable(trig_base, args, cg.int_)
cg.add(var.set_trigger_base(template_))
if trig_keep := config.get(CONF_TRIGGER_KEEP):
template_ = await cg.templatable(trig_keep, args, cg.int_)
cg.add(var.set_trigger_keep(template_))
if (stage_gain := config.get(CONF_STAGE_GAIN)) is not None:
template_ = await cg.templatable(stage_gain, args, cg.int_)
cg.add(var.set_stage_gain(template_))
if power := config.get(CONF_POWER_CONSUMPTION):
if not cg.is_template(power):
power = int(power * 1000000)
template_ = await cg.templatable(power, args, cg.int_)
cg.add(var.set_power_consumption(template_))
return var
+4
View File
@@ -38,6 +38,10 @@ class AT581XComponent final : public Component, public i2c::I2CDevice {
bool i2c_write_config();
bool reset_hardware_frontend();
void reset_hardware_frontend_if(bool reset) {
if (reset)
this->reset_hardware_frontend();
}
bool i2c_write_reg(uint8_t addr, uint8_t data);
bool i2c_write_reg(uint8_t addr, uint32_t data);
bool i2c_write_reg(uint8_t addr, uint16_t data);
-69
View File
@@ -1,69 +0,0 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
#include "at581x.h"
namespace esphome::at581x {
template<typename... Ts> class AT581XResetAction final : public Action<Ts...>, public Parented<AT581XComponent> {
public:
void play(const Ts &...x) { this->parent_->reset_hardware_frontend(); }
};
template<typename... Ts> class AT581XSettingsAction final : public Action<Ts...>, public Parented<AT581XComponent> {
public:
TEMPLATABLE_VALUE(int8_t, hw_frontend_reset)
TEMPLATABLE_VALUE(int, frequency)
TEMPLATABLE_VALUE(int, sensing_distance)
TEMPLATABLE_VALUE(int, poweron_selfcheck_time)
TEMPLATABLE_VALUE(int, power_consumption)
TEMPLATABLE_VALUE(int, protect_time)
TEMPLATABLE_VALUE(int, trigger_base)
TEMPLATABLE_VALUE(int, trigger_keep)
TEMPLATABLE_VALUE(int, stage_gain)
void play(const Ts &...x) {
if (this->frequency_.has_value()) {
int v = this->frequency_.value(x...);
this->parent_->set_frequency(v);
}
if (this->sensing_distance_.has_value()) {
int v = this->sensing_distance_.value(x...);
this->parent_->set_sensing_distance(v);
}
if (this->poweron_selfcheck_time_.has_value()) {
int v = this->poweron_selfcheck_time_.value(x...);
this->parent_->set_poweron_selfcheck_time(v);
}
if (this->power_consumption_.has_value()) {
int v = this->power_consumption_.value(x...);
this->parent_->set_power_consumption(v);
}
if (this->protect_time_.has_value()) {
int v = this->protect_time_.value(x...);
this->parent_->set_protect_time(v);
}
if (this->trigger_base_.has_value()) {
int v = this->trigger_base_.value(x...);
this->parent_->set_trigger_base(v);
}
if (this->trigger_keep_.has_value()) {
int v = this->trigger_keep_.value(x...);
this->parent_->set_trigger_keep(v);
}
if (this->stage_gain_.has_value()) {
int v = this->stage_gain_.value(x...);
this->parent_->set_stage_gain(v);
}
// This actually perform all the modification on the system
this->parent_->i2c_write_config();
if (this->hw_frontend_reset_.has_value() && this->hw_frontend_reset_.value(x...) == true) {
this->parent_->reset_hardware_frontend();
}
}
};
} // namespace esphome::at581x
@@ -134,9 +134,6 @@ MultiClickTriggerBase = binary_sensor_ns.class_(
MultiClickTrigger = binary_sensor_ns.class_("MultiClickTrigger", MultiClickTriggerBase)
MultiClickTriggerEvent = binary_sensor_ns.struct("MultiClickTriggerEvent")
BinarySensorPublishAction = binary_sensor_ns.class_(
"BinarySensorPublishAction", automation.Action
)
BinarySensorInvalidateAction = binary_sensor_ns.class_(
"BinarySensorInvalidateAction", automation.Action
)
@@ -165,20 +165,6 @@ template<typename... Ts> class BinarySensorCondition final : public Condition<Ts
bool state_;
};
template<typename... Ts> class BinarySensorPublishAction final : public Action<Ts...> {
public:
explicit BinarySensorPublishAction(BinarySensor *sensor) : sensor_(sensor) {}
TEMPLATABLE_VALUE(bool, state)
void play(const Ts &...x) override {
auto val = this->state_.value(x...);
this->sensor_->publish_state(val);
}
protected:
BinarySensor *sensor_;
};
template<typename... Ts> class BinarySensorInvalidateAction final : public Action<Ts...> {
public:
explicit BinarySensorInvalidateAction(BinarySensor *sensor) : sensor_(sensor) {}
@@ -53,7 +53,6 @@ BK72xxBLETracker = bk72xx_ble_tracker_ns.class_(
)
StartScanAction = bk72xx_ble_tracker_ns.class_("StartScanAction", automation.Action)
StopScanAction = bk72xx_ble_tracker_ns.class_("StopScanAction", automation.Action)
ESPBTAdvertiseTrigger = ble_automation.ESPBTAdvertiseTrigger
BLEServiceDataAdvertiseTrigger = ble_automation.BLEServiceDataAdvertiseTrigger
@@ -121,9 +120,8 @@ async def start_scan_action_to_code(
return var
@automation.register_action(
automation.register_apply_action(
"bk72xx_ble_tracker.stop_scan",
StopScanAction,
automation.maybe_simple_id(
cv.Schema(
{
@@ -131,17 +129,8 @@ async def start_scan_action_to_code(
}
)
),
synchronous=True,
automation.ApplyCall("stop_scan()"),
)
async def stop_scan_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: list,
) -> cg.MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
async def to_code(config: ConfigType) -> None:
@@ -38,11 +38,6 @@ template<typename... Ts> class StartScanAction final : public Action<Ts...>, pub
}
};
template<typename... Ts> class StopScanAction final : public Action<Ts...>, public Parented<BK72xxBLETracker> {
public:
void play(const Ts &...x) override { this->parent_->stop_scan(); }
};
} // namespace esphome::bk72xx_ble_tracker
#endif // USE_LIBRETINY
+32 -82
View File
@@ -424,91 +424,41 @@ async def send_packet_action_to_code(config, action_id, template_arg, args):
return var
# Setter action definitions: (setter_name, validator, template_type, enum_map)
_SETTER_ACTIONS = [
(
"set_frequency",
cv.All(cv.frequency, cv.float_range(min=300.0e6, max=928.0e6)),
float,
None,
),
("set_output_power", cv.float_range(min=-30.0, max=11.0), float, None),
("set_modulation_type", cv.enum(MODULATION, upper=False), Modulation, MODULATION),
("set_symbol_rate", cv.float_range(min=600, max=500000), float, None),
(
"set_rx_attenuation",
cv.enum(RX_ATTENUATION, upper=False),
RxAttenuation,
RX_ATTENUATION,
),
("set_dc_blocking_filter", cv.boolean, bool, None),
("set_manchester", cv.boolean, bool, None),
(
"set_filter_bandwidth",
cv.All(cv.frequency, cv.float_range(min=58000, max=812000)),
float,
None,
),
(
"set_fsk_deviation",
cv.All(cv.frequency, cv.float_range(min=1500, max=381000)),
float,
None,
),
("set_msk_deviation", cv.int_range(min=1, max=8), cg.uint8, None),
("set_channel", cv.uint8_t, cg.uint8, None),
(
"set_channel_spacing",
cv.All(cv.frequency, cv.float_range(min=25000, max=405000)),
float,
None,
),
(
"set_if_frequency",
cv.All(cv.frequency, cv.float_range(min=25000, max=788000)),
float,
None,
),
]
_CONFIG_VALIDATORS = {opt.schema: validator for opt, validator in CONFIG_MAP.items()}
# Each key has a set_<key> action validated exactly like the config option.
_SETTER_ACTIONS = (
(CONF_FREQUENCY, cg.float_),
(CONF_OUTPUT_POWER, cg.float_),
(CONF_MODULATION_TYPE, Modulation),
(CONF_SYMBOL_RATE, cg.float_),
(CONF_RX_ATTENUATION, RxAttenuation),
(CONF_DC_BLOCKING_FILTER, cg.bool_),
(CONF_MANCHESTER, cg.bool_),
(CONF_FILTER_BANDWIDTH, cg.float_),
(CONF_FSK_DEVIATION, cg.float_),
(CONF_MSK_DEVIATION, cg.uint8),
(CONF_CHANNEL, cg.uint8),
(CONF_CHANNEL_SPACING, cg.float_),
(CONF_IF_FREQUENCY, cg.float_),
)
def _register_setter_actions():
for setter_name, validator, templ_type, enum_map in _SETTER_ACTIONS:
class_name = (
"".join(word.capitalize() for word in setter_name.split("_")) + "Action"
def _register_setter_actions() -> None:
for conf_key, value_type in _SETTER_ACTIONS:
automation.register_apply_action(
f"cc1101.set_{conf_key}",
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(CC1101Component),
cv.Required(CONF_VALUE): cv.templatable(
_CONFIG_VALIDATORS[conf_key]
),
},
key=CONF_VALUE,
),
automation.ApplyField(CONF_VALUE, f"set_{conf_key}", value_type),
)
action_cls = ns.class_(
class_name, automation.Action, cg.Parented.template(CC1101Component)
)
schema = cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(CC1101Component),
cv.Required(CONF_VALUE): cv.templatable(validator),
},
key=CONF_VALUE,
)
async def _setter_action_to_code(
config,
action_id,
template_arg,
args,
_setter=setter_name,
_type=templ_type,
_map=enum_map,
):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
data = config[CONF_VALUE]
if _map and not cg.is_template(data):
data = _map[data]
templ_ = await cg.templatable(data, args, _type)
cg.add(getattr(var, _setter)(templ_))
return var
automation.register_action(
f"cc1101.{setter_name}", action_cls, schema, synchronous=True
)(_setter_action_to_code)
_register_setter_actions()
-79
View File
@@ -174,83 +174,4 @@ template<typename... Ts> class SendPacketAction final : public Action<Ts...>, pu
size_t data_static_len_{0};
};
template<typename... Ts> class SetSymbolRateAction final : public Action<Ts...>, public Parented<CC1101Component> {
public:
TEMPLATABLE_VALUE(float, symbol_rate)
void play(const Ts &...x) override { this->parent_->set_symbol_rate(this->symbol_rate_.value(x...)); }
};
template<typename... Ts> class SetFrequencyAction final : public Action<Ts...>, public Parented<CC1101Component> {
public:
TEMPLATABLE_VALUE(float, frequency)
void play(const Ts &...x) override { this->parent_->set_frequency(this->frequency_.value(x...)); }
};
template<typename... Ts> class SetOutputPowerAction final : public Action<Ts...>, public Parented<CC1101Component> {
public:
TEMPLATABLE_VALUE(float, output_power)
void play(const Ts &...x) override { this->parent_->set_output_power(this->output_power_.value(x...)); }
};
template<typename... Ts> class SetModulationTypeAction final : public Action<Ts...>, public Parented<CC1101Component> {
public:
TEMPLATABLE_VALUE(Modulation, modulation_type)
void play(const Ts &...x) override { this->parent_->set_modulation_type(this->modulation_type_.value(x...)); }
};
template<typename... Ts> class SetRxAttenuationAction final : public Action<Ts...>, public Parented<CC1101Component> {
public:
TEMPLATABLE_VALUE(RxAttenuation, rx_attenuation)
void play(const Ts &...x) override { this->parent_->set_rx_attenuation(this->rx_attenuation_.value(x...)); }
};
template<typename... Ts>
class SetDcBlockingFilterAction final : public Action<Ts...>, public Parented<CC1101Component> {
public:
TEMPLATABLE_VALUE(bool, dc_blocking_filter)
void play(const Ts &...x) override { this->parent_->set_dc_blocking_filter(this->dc_blocking_filter_.value(x...)); }
};
template<typename... Ts> class SetManchesterAction final : public Action<Ts...>, public Parented<CC1101Component> {
public:
TEMPLATABLE_VALUE(bool, manchester)
void play(const Ts &...x) override { this->parent_->set_manchester(this->manchester_.value(x...)); }
};
template<typename... Ts> class SetFilterBandwidthAction final : public Action<Ts...>, public Parented<CC1101Component> {
public:
TEMPLATABLE_VALUE(float, filter_bandwidth)
void play(const Ts &...x) override { this->parent_->set_filter_bandwidth(this->filter_bandwidth_.value(x...)); }
};
template<typename... Ts> class SetFskDeviationAction final : public Action<Ts...>, public Parented<CC1101Component> {
public:
TEMPLATABLE_VALUE(float, fsk_deviation)
void play(const Ts &...x) override { this->parent_->set_fsk_deviation(this->fsk_deviation_.value(x...)); }
};
template<typename... Ts> class SetMskDeviationAction final : public Action<Ts...>, public Parented<CC1101Component> {
public:
TEMPLATABLE_VALUE(uint8_t, msk_deviation)
void play(const Ts &...x) override { this->parent_->set_msk_deviation(this->msk_deviation_.value(x...)); }
};
template<typename... Ts> class SetChannelAction final : public Action<Ts...>, public Parented<CC1101Component> {
public:
TEMPLATABLE_VALUE(uint8_t, channel)
void play(const Ts &...x) override { this->parent_->set_channel(this->channel_.value(x...)); }
};
template<typename... Ts> class SetChannelSpacingAction final : public Action<Ts...>, public Parented<CC1101Component> {
public:
TEMPLATABLE_VALUE(float, channel_spacing)
void play(const Ts &...x) override { this->parent_->set_channel_spacing(this->channel_spacing_.value(x...)); }
};
template<typename... Ts> class SetIfFrequencyAction final : public Action<Ts...>, public Parented<CC1101Component> {
public:
TEMPLATABLE_VALUE(float, if_frequency)
void play(const Ts &...x) override { this->parent_->set_if_frequency(this->if_frequency_.value(x...)); }
};
} // namespace esphome::cc1101
+34 -77
View File
@@ -50,18 +50,13 @@ from esphome.const import (
CONF_VISUAL,
CONF_WEB_SERVER,
)
from esphome.core import CORE, ID, CoroPriority, Lambda, coroutine_with_priority
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.cpp_generator import (
LambdaExpression,
MockObj,
MockObjClass,
TemplateArgsType,
)
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType, SafeExpType
IS_PLATFORM_COMPONENT = True
@@ -169,7 +164,6 @@ def visual_temperature_step(value: Any) -> ConfigType:
# Actions
ControlAction = climate_ns.class_("ControlAction", automation.Action)
StateTrigger = climate_ns.class_(
"StateTrigger", automation.Trigger.template(Climate.operator("ref"))
)
@@ -500,78 +494,41 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
)
@automation.register_action(
def _literal_with_length(config: ConfigType, value: str) -> str:
# The (const char *, size_t) overload compares bytes in place, so it needs a plain
# literal rather than the default PROGMEM rendering on ESP8266, and skips a strlen.
return f"{cg.safe_exp(value)}, {len(value.encode('utf-8'))}"
automation.register_apply_action(
"climate.control",
ControlAction,
CLIMATE_CONTROL_ACTION_SCHEMA,
synchronous=True,
automation.ApplyField(CONF_MODE, "set_mode", ClimateMode),
automation.ApplyField(CONF_TARGET_TEMPERATURE, "set_target_temperature", cg.float_),
automation.ApplyField(
CONF_TARGET_TEMPERATURE_LOW, "set_target_temperature_low", cg.float_
),
automation.ApplyField(
CONF_TARGET_TEMPERATURE_HIGH, "set_target_temperature_high", cg.float_
),
automation.ApplyField(CONF_TARGET_HUMIDITY, "set_target_humidity", cg.float_),
automation.ApplyField(CONF_FAN_MODE, "set_fan_mode", ClimateFanMode),
automation.ApplyField(
CONF_CUSTOM_FAN_MODE,
"set_fan_mode",
cg.std_string,
const_fn=_literal_with_length,
),
automation.ApplyField(CONF_PRESET, "set_preset", ClimatePreset),
automation.ApplyField(
CONF_CUSTOM_PRESET,
"set_preset",
cg.std_string,
const_fn=_literal_with_length,
),
automation.ApplyField(CONF_SWING_MODE, "set_swing_mode", ClimateSwingMode),
call="make_call",
)
async def climate_control_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID])
# All configured fields are folded into a single stateless lambda whose
# constants live in flash; the action stores only a function pointer.
# For custom_fan_mode/custom_preset the static-string path emits the
# (const char *, size_t) overload of set_fan_mode/set_preset to avoid
# constructing a std::string and calling runtime strlen.
FIELDS = (
(CONF_MODE, "set_mode", ClimateMode),
(CONF_TARGET_TEMPERATURE, "set_target_temperature", cg.float_),
(CONF_TARGET_TEMPERATURE_LOW, "set_target_temperature_low", cg.float_),
(CONF_TARGET_TEMPERATURE_HIGH, "set_target_temperature_high", cg.float_),
(CONF_TARGET_HUMIDITY, "set_target_humidity", cg.float_),
(CONF_FAN_MODE, "set_fan_mode", ClimateFanMode),
(CONF_CUSTOM_FAN_MODE, "set_fan_mode", cg.std_string),
(CONF_PRESET, "set_preset", ClimatePreset),
(CONF_CUSTOM_PRESET, "set_preset", cg.std_string),
(CONF_SWING_MODE, "set_swing_mode", ClimateSwingMode),
)
# Normalize trigger args to `const std::remove_cvref_t<T> &` so the
# apply lambda and any inner field lambdas (generated below via
# `process_lambda`) share one parameter spelling that's well-formed for
# any T (value, ref, or const-ref). Matches ControlAction::ApplyFn.
normalized_args = [
(cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), n)
for t, n in args
]
fwd_args = ", ".join(name for _, name in args)
body_lines: list[str] = []
for conf_key, setter, type_ in FIELDS:
if (value := config.get(conf_key)) is None:
continue
if isinstance(value, Lambda):
inner = await cg.process_lambda(value, normalized_args, return_type=type_)
body_lines.append(f"call.{setter}(({inner})({fwd_args}));")
elif type_ is cg.std_string:
# Static custom strings: emit a flash literal and pass the
# UTF-8 byte length to skip the runtime strlen inside
# set_fan_mode/set_preset.
literal = cg.safe_exp(value)
body_lines.append(
f"call.{setter}({literal}, {len(value.encode('utf-8'))});"
)
else:
body_lines.append(f"call.{setter}({cg.safe_exp(value)});")
apply_args = [
(ClimateCall.operator("ref"), "call"),
*normalized_args,
]
apply_lambda = LambdaExpression(
["\n".join(body_lines)],
apply_args,
capture="",
return_type=cg.void,
)
return cg.new_Pvariable(action_id, template_arg, paren, apply_lambda)
@coroutine_with_priority(CoroPriority.CORE)
-28
View File
@@ -5,34 +5,6 @@
namespace esphome::climate {
// All configured fields are baked into a single stateless lambda whose
// constants live in flash. The action only stores one function pointer
// plus one parent pointer, regardless of how many fields the user set.
// Trigger args are forwarded to the apply function so user lambdas
// (e.g. `target_temperature: !lambda "return x;"`) keep working.
//
// Trigger args are normalized to `const std::remove_cvref_t<Ts> &...` so
// the codegen can emit a matching parameter list for both the apply lambda
// and any inner field lambdas without producing invalid C++ source text
// (e.g. `const T & &` if Ts already carries a reference, or `const const
// T &` if Ts already carries a const). This keeps trigger args no-copy
// regardless of whether the trigger supplies `T`, `T &`, or `const T &`.
template<typename... Ts> class ControlAction final : public Action<Ts...> {
public:
using ApplyFn = void (*)(ClimateCall &, const std::remove_cvref_t<Ts> &...);
ControlAction(Climate *climate, ApplyFn apply) : climate_(climate), apply_(apply) {}
void play(const Ts &...x) override {
auto call = this->climate_->make_call();
this->apply_(call, x...);
call.perform();
}
protected:
Climate *climate_;
ApplyFn apply_;
};
class ControlTrigger final : public Trigger<ClimateCall &> {
public:
ControlTrigger(Climate *climate) {
+10 -2
View File
@@ -14,6 +14,8 @@ from esphome.types import ConfigType, SafeExpType
_LOGGER = logging.getLogger(__name__)
CONF_SUPPORTS_HEAT_COOL = "supports_heat_cool"
DEPENDENCIES = ["remote_transmitter"]
AUTO_LOAD = ["sensor", "remote_base"]
CODEOWNERS = ["@glmnet"]
@@ -37,6 +39,7 @@ def climate_ir_schema(
{
cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
cv.Optional(CONF_SUPPORTS_HEAT_COOL): cv.boolean,
cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor),
}
@@ -61,8 +64,13 @@ def climate_ir_with_receiver_schema(
async def register_climate_ir(var: MockObj, config: ConfigType) -> None:
await cg.register_component(var, config)
await remote_base.register_transmittable(var, config)
cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL]))
cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT]))
supports_cool = config[CONF_SUPPORTS_COOL]
supports_heat = config[CONF_SUPPORTS_HEAT]
cg.add(var.set_supports_cool(supports_cool))
cg.add(var.set_supports_heat(supports_heat))
# The header default is true, so only the false case needs a call.
if not config.get(CONF_SUPPORTS_HEAT_COOL, supports_cool and supports_heat):
cg.add(var.set_supports_heat_cool(False))
if remote_base.CONF_RECEIVER_ID in config:
await remote_base.register_listener(var, config)
if sensor_id := config.get(CONF_SENSOR):
+6 -3
View File
@@ -13,11 +13,13 @@ climate::ClimateTraits ClimateIR::traits() {
if (this->humidity_sensor_ != nullptr) {
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
}
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL});
traits.set_supported_modes({climate::CLIMATE_MODE_OFF});
if (this->supports_cool_)
traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
if (this->supports_heat_)
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
if (this->supports_heat_cool_)
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
if (this->supports_dry_)
traits.add_supported_mode(climate::CLIMATE_MODE_DRY);
if (this->supports_fan_only_)
@@ -94,9 +96,10 @@ void ClimateIR::dump_config() {
" Min. Temperature: %.1f°C\n"
" Max. Temperature: %.1f°C\n"
" Supports HEAT: %s\n"
" Supports COOL: %s",
" Supports COOL: %s\n"
" Supports HEAT_COOL: %s",
this->minimum_temperature_, this->maximum_temperature_, YESNO(this->supports_heat_),
YESNO(this->supports_cool_));
YESNO(this->supports_cool_), YESNO(this->supports_heat_cool_));
}
} // namespace esphome::climate_ir
@@ -41,6 +41,7 @@ class ClimateIR : public Component,
void dump_config() override;
void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
void set_supports_heat_cool(bool supports_heat_cool) { this->supports_heat_cool_ = supports_heat_cool; }
void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void set_humidity_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; }
@@ -60,6 +61,8 @@ class ClimateIR : public Component,
bool supports_cool_{true};
bool supports_heat_{true};
// Default (supports_cool && supports_heat) is resolved during code generation.
bool supports_heat_cool_{true};
bool supports_dry_{false};
bool supports_fan_only_{false};
climate::ClimateFanModeMask fan_modes_{};
+27 -174
View File
@@ -1,9 +1,7 @@
from collections.abc import Callable
from dataclasses import dataclass
import logging
from esphome import automation
from esphome.automation import Condition, maybe_simple_id
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
@@ -38,15 +36,15 @@ from esphome.const import (
DEVICE_CLASS_SHUTTER,
DEVICE_CLASS_WINDOW,
)
from esphome.core import CORE, ID, CoroPriority, Lambda, coroutine_with_priority
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
setup_entity,
)
from esphome.cpp_generator import LambdaExpression, MockObj, MockObjClass
from esphome.types import ConfigType, SafeExpType, TemplateArgsType
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType, SafeExpType
IS_PLATFORM_COMPONENT = True
@@ -70,7 +68,6 @@ _LOGGER = logging.getLogger(__name__)
cover_ns = cg.esphome_ns.namespace("cover")
Cover = cover_ns.class_("Cover", cg.EntityBase)
CoverCall = cover_ns.class_("CoverCall")
COVER_OPEN = cover_ns.COVER_OPEN
COVER_CLOSED = cover_ns.COVER_CLOSED
@@ -90,14 +87,6 @@ COVER_OPERATIONS = {
validate_cover_operation = cv.enum(COVER_OPERATIONS, upper=True)
# Actions
OpenAction = cover_ns.class_("OpenAction", automation.Action)
CloseAction = cover_ns.class_("CloseAction", automation.Action)
StopAction = cover_ns.class_("StopAction", automation.Action)
ToggleAction = cover_ns.class_("ToggleAction", automation.Action)
ControlAction = cover_ns.class_("ControlAction", automation.Action)
CoverPublishAction = cover_ns.class_("CoverPublishAction", automation.Action)
CoverIsOpenCondition = cover_ns.class_("CoverIsOpenCondition", Condition)
CoverIsClosedCondition = cover_ns.class_("CoverIsClosedCondition", Condition)
CoverOpenedTrigger = cover_ns.class_(
"CoverOpenedTrigger", automation.Trigger.template()
)
@@ -256,56 +245,15 @@ COVER_ACTION_SCHEMA = maybe_simple_id(
)
@automation.register_action(
"cover.open", OpenAction, COVER_ACTION_SCHEMA, synchronous=True
)
async def cover_open_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"cover.close", CloseAction, COVER_ACTION_SCHEMA, synchronous=True
)
async def cover_close_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"cover.stop", StopAction, COVER_ACTION_SCHEMA, synchronous=True
)
async def cover_stop_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"cover.toggle", ToggleAction, COVER_ACTION_SCHEMA, synchronous=True
)
async def cover_toggle_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
for _name, _command in (
("cover.open", "set_command_open()"),
("cover.close", "set_command_close()"),
("cover.stop", "set_command_stop()"),
("cover.toggle", "set_command_toggle()"),
):
automation.register_apply_action(
_name, COVER_ACTION_SCHEMA, automation.ApplyCall(_command), call="make_call"
)
COVER_CONTROL_ACTION_SCHEMA = cv.Schema(
@@ -319,125 +267,30 @@ COVER_CONTROL_ACTION_SCHEMA = cv.Schema(
)
@dataclass(frozen=True)
class ApplyField:
"""One field in a folded-lambda action.
`conf_key` is the YAML key looked up in `config`. When present, the
helper emits `statement_fn(target, value_expr)` into the lambda body.
`target` is whatever the statement function needs to identify the
field (typically a setter name like `"set_position"` or a struct
member like `"position"`). `type_` is the C++ return type for
`cg.process_lambda` when the value is a user lambda.
"""
conf_key: str
target: str
type_: object
async def build_apply_lambda_action(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
fields: tuple[ApplyField, ...],
prefix_args: list[tuple[object, str]],
statement_fn: Callable[[str, str], str],
) -> MockObj:
"""Fold configured fields into a single stateless apply lambda action.
Used by both `cover.control` and `cover.template.publish` (and shared
with the template/cover platform). Constants are emitted as flash
immediates; user lambdas are invoked inline so trigger args still flow.
Trigger arg types are normalized to `const std::remove_cvref_t<T> &`
to match the ApplyFn signature for any T (value, ref, or const-ref).
"""
paren = await cg.get_variable(config[CONF_ID])
# Normalize trigger args to `const std::remove_cvref_t<T> &` so the
# apply lambda and any inner field lambdas (generated below via
# `process_lambda`) share one parameter spelling that's well-formed for
# any T.
normalized_args = [
(cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), n)
for t, n in args
]
fwd_args = ", ".join(name for _, name in args)
body_lines: list[str] = []
for field in fields:
if (value := config.get(field.conf_key)) is None:
continue
if isinstance(value, Lambda):
inner = await cg.process_lambda(
value, normalized_args, return_type=field.type_
)
value_expr = f"({inner})({fwd_args})"
else:
value_expr = str(cg.safe_exp(value))
body_lines.append(statement_fn(field.target, value_expr))
apply_args = [
*prefix_args,
*normalized_args,
]
apply_lambda = LambdaExpression(
["\n".join(body_lines)],
apply_args,
capture="",
return_type=cg.void,
)
return cg.new_Pvariable(action_id, template_arg, paren, apply_lambda)
# CONF_STATE and CONF_POSITION are cv.Exclusive in the schema, so at most
# one is present and both dispatch to set_position.
_COVER_CONTROL_FIELDS: tuple[ApplyField, ...] = (
ApplyField(CONF_STOP, "set_stop", cg.bool_),
ApplyField(CONF_STATE, "set_position", cg.float_),
ApplyField(CONF_POSITION, "set_position", cg.float_),
ApplyField(CONF_TILT, "set_tilt", cg.float_),
automation.register_apply_action(
"cover.control",
COVER_CONTROL_ACTION_SCHEMA,
automation.ApplyField(CONF_STOP, "set_stop", cg.bool_),
automation.ApplyField(CONF_STATE, "set_position", cg.float_),
automation.ApplyField(CONF_POSITION, "set_position", cg.float_),
automation.ApplyField(CONF_TILT, "set_tilt", cg.float_),
call="make_call",
)
@automation.register_action(
"cover.control", ControlAction, COVER_CONTROL_ACTION_SCHEMA, synchronous=True
)
async def cover_control_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
return await build_apply_lambda_action(
config=config,
action_id=action_id,
template_arg=template_arg,
args=args,
fields=_COVER_CONTROL_FIELDS,
prefix_args=[(CoverCall.operator("ref"), "call")],
statement_fn=lambda setter, expr: f"call.{setter}({expr});",
)
COVER_CONDITION_SCHEMA = cv.maybe_simple_value(
{cv.Required(CONF_ID): cv.use_id(Cover)}, key=CONF_ID
)
async def cover_condition_to_code(
config: ConfigType, condition_id: ID, template_arg: MockObj, args: TemplateArgsType
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren)
automation.register_condition(
"cover.is_open", CoverIsOpenCondition, COVER_CONDITION_SCHEMA
)(cover_condition_to_code)
automation.register_condition(
"cover.is_closed", CoverIsClosedCondition, COVER_CONDITION_SCHEMA
)(cover_condition_to_code)
automation.register_apply_condition(
"cover.is_open", COVER_CONDITION_SCHEMA, f"position == {COVER_OPEN}"
)
automation.register_apply_condition(
"cover.is_closed", COVER_CONDITION_SCHEMA, f"position == {COVER_CLOSED}"
)
@coroutine_with_priority(CoroPriority.CORE)
-97
View File
@@ -6,103 +6,6 @@
namespace esphome::cover {
template<typename... Ts> class OpenAction final : public Action<Ts...> {
public:
explicit OpenAction(Cover *cover) : cover_(cover) {}
void play(const Ts &...x) override { this->cover_->make_call().set_command_open().perform(); }
protected:
Cover *cover_;
};
template<typename... Ts> class CloseAction final : public Action<Ts...> {
public:
explicit CloseAction(Cover *cover) : cover_(cover) {}
void play(const Ts &...x) override { this->cover_->make_call().set_command_close().perform(); }
protected:
Cover *cover_;
};
template<typename... Ts> class StopAction final : public Action<Ts...> {
public:
explicit StopAction(Cover *cover) : cover_(cover) {}
void play(const Ts &...x) override { this->cover_->make_call().set_command_stop().perform(); }
protected:
Cover *cover_;
};
template<typename... Ts> class ToggleAction final : public Action<Ts...> {
public:
explicit ToggleAction(Cover *cover) : cover_(cover) {}
void play(const Ts &...x) override { this->cover_->make_call().set_command_toggle().perform(); }
protected:
Cover *cover_;
};
// All configured fields are baked into a single stateless lambda whose
// constants live in flash. Each action stores only one function pointer
// plus one parent pointer, regardless of how many fields the user set.
// Trigger args are forwarded to the apply function so user lambdas
// (e.g. `position: !lambda "return x;"`) keep working.
//
// Trigger args are normalized to `const std::remove_cvref_t<Ts> &...` so
// the codegen can emit a matching parameter list for both the apply lambda
// and any inner field lambdas without producing invalid C++ source text
// (e.g. `const T & &` if Ts already carries a reference, or `const const
// T &` if Ts already carries a const). This keeps trigger args no-copy
// regardless of whether the trigger supplies `T`, `T &`, or `const T &`.
template<typename... Ts> class ControlAction final : public Action<Ts...> {
public:
using ApplyFn = void (*)(CoverCall &, const std::remove_cvref_t<Ts> &...);
ControlAction(Cover *cover, ApplyFn apply) : cover_(cover), apply_(apply) {}
void play(const Ts &...x) override {
auto call = this->cover_->make_call();
this->apply_(call, x...);
call.perform();
}
protected:
Cover *cover_;
ApplyFn apply_;
};
template<typename... Ts> class CoverPublishAction final : public Action<Ts...> {
public:
using ApplyFn = void (*)(Cover *, const std::remove_cvref_t<Ts> &...);
CoverPublishAction(Cover *cover, ApplyFn apply) : cover_(cover), apply_(apply) {}
void play(const Ts &...x) override {
this->apply_(this->cover_, x...);
this->cover_->publish_state();
}
protected:
Cover *cover_;
ApplyFn apply_;
};
template<bool OPEN, typename... Ts> class CoverPositionCondition final : public Condition<Ts...> {
public:
CoverPositionCondition(Cover *cover) : cover_(cover) {}
bool check(const Ts &...x) override { return this->cover_->position == (OPEN ? COVER_OPEN : COVER_CLOSED); }
protected:
Cover *cover_;
};
template<typename... Ts> using CoverIsOpenCondition = CoverPositionCondition<true, Ts...>;
template<typename... Ts> using CoverIsClosedCondition = CoverPositionCondition<false, Ts...>;
template<bool OPEN> class CoverPositionTrigger final : public Trigger<> {
public:
CoverPositionTrigger(Cover *a_cover) : cover_(a_cover) {
+60 -117
View File
@@ -1,3 +1,5 @@
from collections.abc import Callable
from esphome import automation
import esphome.codegen as cg
from esphome.components import mqtt, time, web_server
@@ -21,13 +23,13 @@ from esphome.const import (
CONF_WEB_SERVER,
CONF_YEAR,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType, SafeExpType
CODEOWNERS = ["@rfdarter", "@jesserockz"]
@@ -40,11 +42,6 @@ DateEntity = datetime_ns.class_("DateEntity", DateTimeBase)
TimeEntity = datetime_ns.class_("TimeEntity", DateTimeBase)
DateTimeEntity = datetime_ns.class_("DateTimeEntity", DateTimeBase)
# Actions
DateSetAction = datetime_ns.class_("DateSetAction", automation.Action)
TimeSetAction = datetime_ns.class_("TimeSetAction", automation.Action)
DateTimeSetAction = datetime_ns.class_("DateTimeSetAction", automation.Action)
DateTimeStateTrigger = datetime_ns.class_(
"DateTimeStateTrigger", automation.Trigger.template(cg.ESPTime)
)
@@ -181,118 +178,64 @@ async def to_code(config: ConfigType) -> None:
cg.add_global(datetime_ns.using)
@automation.register_action(
"datetime.date.set",
DateSetAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(DateEntity),
cv.Required(CONF_DATE): cv.Any(
cv.returning_lambda, cv.date_time(date=True, time=False)
),
}
),
synchronous=True,
)
async def datetime_date_set_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
action_var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(action_var, config[CONF_ID])
date_config = config[CONF_DATE]
if cg.is_template(date_config):
template_ = await cg.templatable(date_config, args, cg.ESPTime)
cg.add(action_var.set_date(template_))
else:
date_struct = cg.StructInitializer(
cg.ESPTime,
("day_of_month", date_config[CONF_DAY]),
("month", date_config[CONF_MONTH]),
("year", date_config[CONF_YEAR]),
def _esptime_initializer(
members: tuple[tuple[str, str], ...],
) -> Callable[[ConfigType, ConfigType], str]:
def const_fn(config: ConfigType, value: ConfigType) -> str:
return str(
cg.StructInitializer(
cg.ESPTime, *((member, value[key]) for member, key in members)
)
)
template_ = await cg.templatable(date_struct, args, cg.ESPTime)
cg.add(action_var.set_date(template_))
return action_var
return const_fn
@automation.register_action(
"datetime.time.set",
TimeSetAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(TimeEntity),
cv.Required(CONF_TIME): cv.Any(
cv.returning_lambda, cv.date_time(date=False, time=True)
),
}
# ESPTime member order, so the designated initializer compiles.
_TIME_MEMBERS = (("second", CONF_SECOND), ("minute", CONF_MINUTE), ("hour", CONF_HOUR))
_DATE_MEMBERS = (("day_of_month", CONF_DAY), ("month", CONF_MONTH), ("year", CONF_YEAR))
for _name, _entity, _key, _target, _date, _time, _members in (
(
"datetime.date.set",
DateEntity,
CONF_DATE,
"set_date",
True,
False,
_DATE_MEMBERS,
),
synchronous=True,
)
async def datetime_time_set_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
action_var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(action_var, config[CONF_ID])
time_config = config[CONF_TIME]
if cg.is_template(time_config):
template_ = await cg.templatable(time_config, args, cg.ESPTime)
cg.add(action_var.set_time(template_))
else:
time_struct = cg.StructInitializer(
cg.ESPTime,
("second", time_config[CONF_SECOND]),
("minute", time_config[CONF_MINUTE]),
("hour", time_config[CONF_HOUR]),
)
template_ = await cg.templatable(time_struct, args, cg.ESPTime)
cg.add(action_var.set_time(template_))
return action_var
@automation.register_action(
"datetime.datetime.set",
DateTimeSetAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(DateTimeEntity),
cv.Required(CONF_DATETIME): cv.Any(
cv.returning_lambda, cv.date_time(date=True, time=True)
),
},
(
"datetime.time.set",
TimeEntity,
CONF_TIME,
"set_time",
False,
True,
_TIME_MEMBERS,
),
synchronous=True,
)
async def datetime_datetime_set_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
action_var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(action_var, config[CONF_ID])
datetime_config = config[CONF_DATETIME]
if cg.is_template(datetime_config):
template_ = await cg.templatable(datetime_config, args, cg.ESPTime)
cg.add(action_var.set_datetime(template_))
else:
datetime_struct = cg.StructInitializer(
cg.ESPTime,
("second", datetime_config[CONF_SECOND]),
("minute", datetime_config[CONF_MINUTE]),
("hour", datetime_config[CONF_HOUR]),
("day_of_month", datetime_config[CONF_DAY]),
("month", datetime_config[CONF_MONTH]),
("year", datetime_config[CONF_YEAR]),
)
template_ = await cg.templatable(datetime_struct, args, cg.ESPTime)
cg.add(action_var.set_datetime(template_))
return action_var
(
"datetime.datetime.set",
DateTimeEntity,
CONF_DATETIME,
"set_datetime",
True,
True,
_TIME_MEMBERS + _DATE_MEMBERS,
),
):
automation.register_apply_action(
_name,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(_entity),
cv.Required(_key): cv.Any(
cv.returning_lambda, cv.date_time(date=_date, time=_time)
),
}
),
automation.ApplyField(
_key, _target, cg.ESPTime, const_fn=_esptime_initializer(_members)
),
call="make_call",
)
-14
View File
@@ -98,20 +98,6 @@ class DateCall {
inline DateCall DateEntity::make_call() { return DateCall(this); }
template<typename... Ts> class DateSetAction final : public Action<Ts...>, public Parented<DateEntity> {
public:
TEMPLATABLE_VALUE(ESPTime, date)
void play(const Ts &...x) override {
auto call = this->parent_->make_call();
if (this->date_.has_value()) {
call.set_date(this->date_.value(x...));
}
call.perform();
}
};
} // namespace esphome::datetime
#endif // USE_DATETIME_DATE
@@ -123,20 +123,6 @@ class DateTimeCall {
inline DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); }
template<typename... Ts> class DateTimeSetAction final : public Action<Ts...>, public Parented<DateTimeEntity> {
public:
TEMPLATABLE_VALUE(ESPTime, datetime)
void play(const Ts &...x) override {
auto call = this->parent_->make_call();
if (this->datetime_.has_value()) {
call.set_datetime(this->datetime_.value(x...));
}
call.perform();
}
};
#ifdef USE_TIME
class OnDateTimeTrigger final : public Trigger<>, public Component, public Parented<DateTimeEntity> {
public:
-14
View File
@@ -100,20 +100,6 @@ class TimeCall {
inline TimeCall TimeEntity::make_call() { return TimeCall(this); }
template<typename... Ts> class TimeSetAction final : public Action<Ts...>, public Parented<TimeEntity> {
public:
TEMPLATABLE_VALUE(ESPTime, time)
void play(const Ts &...x) override {
auto call = this->parent_->make_call();
if (this->time_.has_value()) {
call.set_time(this->time_.value(x...));
}
call.perform();
}
};
#ifdef USE_TIME
class OnTimeTrigger final : public Trigger<>, public Component, public Parented<TimeEntity> {
public:
+29
View File
@@ -1,3 +1,5 @@
from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.components import uart
@@ -16,6 +18,7 @@ DFPlayerIsPlayingCondition = dfplayer_ns.class_(
MULTI_CONF = True
CONF_FOLDER = "folder"
CONF_ENABLE = "enable"
CONF_EQ_PRESET = "eq_preset"
CONF_ON_FINISHED_PLAYBACK = "on_finished_playback"
@@ -375,6 +378,32 @@ async def dfplayer_random_to_code(config, action_id, template_arg, args):
return var
def _default_enable(value: Any) -> Any:
"""Fill in ``enable: true`` for a bare action or a mapping that only picks the player.
Done before ``maybe_simple_value`` so neither form is wrapped as the ``enable`` value.
"""
if value is None or isinstance(value, dict):
return {CONF_ENABLE: True, **(value or {})}
return value
automation.register_apply_action(
"dfplayer.set_current_track_repeat",
cv.All(
_default_enable,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(DFPlayer),
cv.Optional(CONF_ENABLE, default=True): cv.templatable(cv.boolean),
},
key=CONF_ENABLE,
),
),
automation.ApplyField(CONF_ENABLE, "set_current_track_repeat", cg.bool_),
)
@automation.register_condition(
"dfplayer.is_playing",
DFPlayerIsPlayingCondition,
+7
View File
@@ -102,6 +102,13 @@ void DFPlayer::random() {
this->send_cmd_(0x18);
}
void DFPlayer::set_current_track_repeat(bool enable) {
uint16_t arg = enable ? 0x00 : 0x01;
ESP_LOGD(TAG, "Setting current track repeat to %s",
enable ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"));
this->send_cmd_(0x19, arg);
}
void DFPlayer::play_folder(uint16_t folder, uint16_t file) {
ESP_LOGD(TAG, "Playing file %d in folder %d", file, folder);
if (folder < 100 && file < 256) {
+1
View File
@@ -46,6 +46,7 @@ class DFPlayer final : public uart::UARTDevice, public Component {
void pause();
void stop();
void random();
void set_current_track_repeat(bool enable);
bool is_playing() { return is_playing_; }
void dump_config() override;
+13 -30
View File
@@ -39,12 +39,6 @@ DisplayPage = display_ns.class_("DisplayPage")
DisplayPagePtr = DisplayPage.operator("ptr")
DisplayRef = Display.operator("ref")
DisplayPageShowAction = display_ns.class_("DisplayPageShowAction", automation.Action)
DisplayPageShowNextAction = display_ns.class_(
"DisplayPageShowNextAction", automation.Action
)
DisplayPageShowPrevAction = display_ns.class_(
"DisplayPageShowPrevAction", automation.Action
)
DisplayIsDisplayingPageCondition = display_ns.class_(
"DisplayIsDisplayingPageCondition", automation.Condition
)
@@ -288,34 +282,23 @@ async def display_page_show_to_code(config, action_id, template_arg, args):
return var
@automation.register_action(
# The id is the display itself, so it cannot be a lambda; a plain use_id rejects one clearly.
DISPLAY_PAGE_CYCLE_ACTION_SCHEMA = maybe_simple_id(
{
cv.GenerateID(CONF_ID): cv.use_id(Display),
}
)
automation.register_apply_action(
"display.page.show_next",
DisplayPageShowNextAction,
maybe_simple_id(
{
cv.GenerateID(CONF_ID): cv.templatable(cv.use_id(Display)),
}
),
synchronous=True,
DISPLAY_PAGE_CYCLE_ACTION_SCHEMA,
automation.ApplyCall("show_next_page()"),
)
async def display_page_show_next_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
automation.register_apply_action(
"display.page.show_previous",
DisplayPageShowPrevAction,
maybe_simple_id(
{
cv.GenerateID(CONF_ID): cv.templatable(cv.use_id(Display)),
}
),
synchronous=True,
DISPLAY_PAGE_CYCLE_ACTION_SCHEMA,
automation.ApplyCall("show_prev_page()"),
)
async def display_page_show_previous_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_condition(
-18
View File
@@ -848,24 +848,6 @@ template<typename... Ts> class DisplayPageShowAction final : public Action<Ts...
}
};
template<typename... Ts> class DisplayPageShowNextAction final : public Action<Ts...> {
public:
DisplayPageShowNextAction(Display *buffer) : buffer_(buffer) {}
void play(const Ts &...x) override { this->buffer_->show_next_page(); }
Display *buffer_;
};
template<typename... Ts> class DisplayPageShowPrevAction final : public Action<Ts...> {
public:
DisplayPageShowPrevAction(Display *buffer) : buffer_(buffer) {}
void play(const Ts &...x) override { this->buffer_->show_prev_page(); }
Display *buffer_;
};
template<typename... Ts> class DisplayIsDisplayingPageCondition final : public Condition<Ts...> {
public:
DisplayIsDisplayingPageCondition(Display *parent) : parent_(parent) {}
+14 -6
View File
@@ -424,6 +424,7 @@ ARDUINO_DISABLED_LIBRARIES: frozenset[str] = frozenset(
"Hash",
"HTTPClient",
"HTTPUpdate",
"HTTPUpdateServer",
"Insights",
"LittleFS",
"Matter",
@@ -931,14 +932,15 @@ def _is_framework_url(source: str) -> bool:
# The default/recommended arduino framework version
# - https://github.com/espressif/arduino-esp32/releases
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(3, 3, 11),
"latest": cv.Version(3, 3, 11),
"dev": cv.Version(3, 3, 11),
"recommended": cv.Version(3, 3, 12),
"latest": cv.Version(3, 3, 12),
"dev": cv.Version(3, 3, 12),
}
ARDUINO_PLATFORM_VERSION_LOOKUP = {
cv.Version(
4, 0, 0, "alpha1"
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
cv.Version(3, 3, 12): cv.Version(55, 3, 312),
cv.Version(3, 3, 11): cv.Version(55, 3, 311),
cv.Version(3, 3, 10): cv.Version(55, 3, 39),
cv.Version(3, 3, 9): cv.Version(55, 3, 39),
@@ -963,6 +965,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
# See: https://github.com/pioarduino/esp-idf/releases
ARDUINO_IDF_VERSION_LOOKUP = {
cv.Version(4, 0, 0, "alpha1"): cv.Version(6, 0, 1),
cv.Version(3, 3, 12): cv.Version(5, 5, 5),
cv.Version(3, 3, 11): cv.Version(5, 5, 5),
cv.Version(3, 3, 10): cv.Version(5, 5, 5),
cv.Version(3, 3, 9): cv.Version(5, 5, 4),
@@ -998,7 +1001,7 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(
6, 0, 0
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
cv.Version(5, 5, 5): cv.Version(55, 3, 311),
cv.Version(5, 5, 5): cv.Version(55, 3, 312),
cv.Version(5, 5, 4): cv.Version(55, 3, 39),
cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37),
cv.Version(5, 5, 3): cv.Version(55, 3, 37),
@@ -1019,8 +1022,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
# The platform-espressif32 version
# - https://github.com/pioarduino/platform-espressif32/releases
PLATFORM_VERSION_LOOKUP = {
"recommended": cv.Version(55, 3, 311),
"latest": cv.Version(55, 3, 311),
"recommended": cv.Version(55, 3, 312),
"latest": cv.Version(55, 3, 312),
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
}
@@ -2792,6 +2795,11 @@ async def to_code(config):
config.get(CONF_ENGINEERING_SAMPLE, False),
)
# ESP32-C2 defaults to the ROM's newlib "nano" printf, which does not
# understand %zu or %lld and crashes on any %s that follows one.
if variant == VARIANT_ESP32C2:
add_idf_sdkconfig_option("CONFIG_LIBC_NEWLIB_NANO_FORMAT", False)
# Set minimum chip revision for ESP32 variant
# Setting this to 3.0 or higher reduces flash size by excluding workaround code,
# and for PSRAM users saves significant IRAM by keeping C library functions in ROM.
@@ -39,8 +39,7 @@ from esphome.const import (
CONF_SERVICE_UUID,
CONF_TRIGGER_ID,
)
from esphome.core import CORE, ID, CoroPriority, TimePeriod, coroutine_with_priority
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority
from esphome.enum import StrEnum
from esphome.types import ConfigType
@@ -116,12 +115,6 @@ BLEEndOfScanTrigger = esp32_ble_tracker_ns.class_(
"BLEEndOfScanTrigger", automation.Trigger.template()
)
# Actions
ESP32BLEStartScanAction = esp32_ble_tracker_ns.class_(
"ESP32BLEStartScanAction", automation.Action
)
ESP32BLEStopScanAction = esp32_ble_tracker_ns.class_(
"ESP32BLEStopScanAction", automation.Action
)
def validate_max_connections_deprecated(config: ConfigType) -> ConfigType:
@@ -469,23 +462,12 @@ ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema(
)
@automation.register_action(
automation.register_apply_action(
"esp32_ble_tracker.start_scan",
ESP32BLEStartScanAction,
ESP32_BLE_START_SCAN_ACTION_SCHEMA,
synchronous=True,
automation.ApplyField(CONF_CONTINUOUS, "set_scan_continuous", cg.bool_),
automation.ApplyCall("start_scan_if_idle()"),
)
async def esp32_ble_tracker_start_scan_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_CONTINUOUS], args, cg.bool_)
cg.add(var.set_continuous(template_))
return var
ESP32_BLE_STOP_SCAN_ACTION_SCHEMA = automation.maybe_simple_id(
@@ -497,21 +479,11 @@ ESP32_BLE_STOP_SCAN_ACTION_SCHEMA = automation.maybe_simple_id(
)
@automation.register_action(
automation.register_apply_action(
"esp32_ble_tracker.stop_scan",
ESP32BLEStopScanAction,
ESP32_BLE_STOP_SCAN_ACTION_SCHEMA,
synchronous=True,
automation.ApplyCall("stop_scan()"),
)
async def esp32_ble_tracker_stop_scan_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
async def register_ble_device(
@@ -94,30 +94,6 @@ class BLEEndOfScanTrigger final : public Trigger<>, public ESPBTDeviceListener {
void on_scan_end() override { this->trigger(); }
};
template<typename... Ts> class ESP32BLEStartScanAction final : public Action<Ts...> {
public:
ESP32BLEStartScanAction(ESP32BLETracker *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(bool, continuous)
void play(const Ts &...x) override {
this->parent_->set_scan_continuous(this->continuous_.value(x...));
// Only call start_scan() if scanner is IDLE
// For other states (STARTING, RUNNING, STOPPING, FAILED), the normal state
// machine flow will eventually transition back to IDLE, at which point
// loop() will see scan_continuous_ and restart scanning if it is true.
if (this->parent_->get_scanner_state() == ScannerState::IDLE) {
this->parent_->start_scan();
}
}
protected:
ESP32BLETracker *parent_;
};
template<typename... Ts> class ESP32BLEStopScanAction final : public Action<Ts...>, public Parented<ESP32BLETracker> {
public:
void play(const Ts &...x) override { this->parent_->stop_scan(); }
};
} // namespace esphome::esp32_ble_tracker
#endif
@@ -218,6 +218,12 @@ class ESP32BLETracker final : public Component,
#endif
void start_scan();
// For the start_scan action: in any other state the state machine returns to IDLE on its own
// and loop() restarts scanning when scan_continuous_ is set, so only an idle scanner starts here.
void start_scan_if_idle() {
if (this->scanner_state_ == ScannerState::IDLE)
this->start_scan();
}
void stop_scan();
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param);
+1 -1
View File
@@ -313,7 +313,7 @@ async def to_code(config: ConfigType) -> None:
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.6.3")
esp32.add_idf_component(name="espressif/wifi_remote_over_eppp", ref="0.3.3")
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.5")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.12")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.13")
esp32.add_extra_script(
"post",
"esp32_hosted.py",
+5 -1
View File
@@ -37,6 +37,7 @@ from esphome.types import ConfigType
from .boards import BOARDS, board_ld_script
from .const import (
BUILD_FLASH_MODES,
CONF_EARLY_PIN_INIT,
CONF_ENABLE_SERIAL,
CONF_ENABLE_SERIAL1,
@@ -44,6 +45,7 @@ from .const import (
KEY_BOARD,
KEY_ESP8266,
KEY_PIN_INITIAL_STATES,
KEY_SCANF_FLOAT,
KEY_SERIAL1_REQUIRED,
KEY_SERIAL_REQUIRED,
KEY_WAVEFORM_REQUIRED,
@@ -233,7 +235,6 @@ ARDUINO_FRAMEWORK_SCHEMA = cv.All(
)
BUILD_FLASH_MODES = ["qio", "qout", "dio", "dout"]
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -315,6 +316,9 @@ async def to_code(config: ConfigType) -> None:
"enabling scanf float support (~8KB flash)"
)
# The native generator reads the same decision (KEY_SCANF_FLOAT)
CORE.data[KEY_ESP8266][KEY_SCANF_FLOAT] = bool(enable_scanf_float)
extra_scripts = [
"pre:ccache.py",
"pre:testing_mode.py",
+3 -1
View File
@@ -16,7 +16,6 @@ KEY_WAVEFORM_REQUIRED = "waveform_required"
KEY_SERIAL_REQUIRED = "serial_required"
KEY_SERIAL1_REQUIRED = "serial1_required"
# Set for the native (non-PlatformIO) toolchain's build generator
KEY_FLASH_MODE = "flash_mode"
KEY_SCANF_FLOAT = "scanf_float"
# Per-board flash-layout override consumed by board_ld_script()
KEY_LDSCRIPT = "ldscript"
@@ -73,3 +72,6 @@ def enable_serial1() -> None:
enable_serial1()
"""
CORE.data.setdefault(KEY_ESP8266, {})[KEY_SERIAL1_REQUIRED] = True
BUILD_FLASH_MODES = ("qio", "qout", "dio", "dout")
@@ -4,7 +4,6 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/automation.h"
#include "esphome/components/output/float_output.h"
namespace esphome::esp8266_pwm {
@@ -37,19 +36,6 @@ class ESP8266PWM final : public output::FloatOutput, public Component {
float last_output_{0.0};
};
template<typename... Ts> class SetFrequencyAction final : public Action<Ts...> {
public:
SetFrequencyAction(ESP8266PWM *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(float, frequency);
void play(const Ts &...x) {
float freq = this->frequency_.value(x...);
this->parent_->update_frequency(freq);
}
ESP8266PWM *parent_;
};
} // namespace esphome::esp8266_pwm
#endif
+2 -17
View File
@@ -4,8 +4,6 @@ from esphome.components import output
from esphome.components.esp8266.const import require_waveform
import esphome.config_validation as cv
from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_NUMBER, CONF_PIN
from esphome.core import ID
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
DEPENDENCIES = ["esp8266"]
@@ -19,7 +17,6 @@ def valid_pwm_pin(value: ConfigType) -> ConfigType:
esp8266_pwm_ns = cg.esphome_ns.namespace("esp8266_pwm")
ESP8266PWM = esp8266_pwm_ns.class_("ESP8266PWM", output.FloatOutput, cg.Component)
SetFrequencyAction = esp8266_pwm_ns.class_("SetFrequencyAction", automation.Action)
validate_frequency = cv.All(cv.frequency, cv.float_range(min=1.0e-6))
# Schema default that also matches the C++ initializer in esp8266_pwm.h; codegen
@@ -57,25 +54,13 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_frequency(frequency))
@automation.register_action(
automation.register_apply_action(
"output.esp8266_pwm.set_frequency",
SetFrequencyAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(ESP8266PWM),
cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency),
}
),
synchronous=True,
automation.ApplyField(CONF_FREQUENCY, "update_frequency", cg.float_),
)
async def esp8266_set_frequency_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_FREQUENCY], args, cg.float_)
cg.add(var.set_frequency(template_))
return var
+25 -47
View File
@@ -39,9 +39,6 @@ ESPNowRecvInfo = espnow_ns.class_("ESPNowRecvInfo")
ESPNowRecvInfoConstRef = ESPNowRecvInfo.operator("const").operator("ref")
SendAction = espnow_ns.class_("SendAction", automation.Action)
SetChannelAction = espnow_ns.class_("SetChannelAction", automation.Action)
AddPeerAction = espnow_ns.class_("AddPeerAction", automation.Action)
DeletePeerAction = espnow_ns.class_("DeletePeerAction", automation.Action)
ESPNowHandlerTrigger = automation.Trigger.template(
ESPNowRecvInfoConstRef,
@@ -232,12 +229,16 @@ def _validate_raw_data(value: Any) -> str | list:
)
def _mac_bytes(address: core.MACAddress) -> list[HexInt]:
return [HexInt(p) for p in address.parts]
async def register_peer(
var: MockObj, config: ConfigType, args: TemplateArgsType
) -> None:
peer = config[CONF_ADDRESS]
if isinstance(peer, core.MACAddress):
peer = [HexInt(p) for p in peer.parts]
peer = _mac_bytes(peer)
template_ = await cg.templatable(peer, args, peer_address_t, peer_address_t)
cg.add(var.set_address(template_))
@@ -323,40 +324,28 @@ async def send_action(
return var
@automation.register_action(
"espnow.peer.add",
AddPeerAction,
cv.maybe_simple_value(
PEER_SCHEMA,
key=CONF_ADDRESS,
),
synchronous=True,
)
@automation.register_action(
"espnow.peer.delete",
DeletePeerAction,
cv.maybe_simple_value(
PEER_SCHEMA,
key=CONF_ADDRESS,
),
synchronous=True,
)
async def peer_action(
config: ConfigType,
action_id: core.ID,
template_arg: cg.TemplateArguments,
args: list[tuple],
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
await register_peer(var, config, args)
return var
def _peer_address(config: ConfigType, value: core.MACAddress) -> str:
return str(cg.safe_exp(_mac_bytes(value)))
@automation.register_action(
for _name, _method in (
("espnow.peer.add", "add_peer_from_action"),
("espnow.peer.delete", "del_peer_from_action"),
):
automation.register_apply_action(
_name,
cv.maybe_simple_value(
PEER_SCHEMA,
key=CONF_ADDRESS,
),
automation.ApplyField(
CONF_ADDRESS, _method, peer_address_t, const_fn=_peer_address
),
)
automation.register_apply_action(
"espnow.set_channel",
SetChannelAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(ESPNowComponent),
@@ -364,16 +353,5 @@ async def peer_action(
},
key=CONF_CHANNEL,
),
synchronous=True,
automation.ApplyField(CONF_CHANNEL, "set_channel_from_action", cg.uint8),
)
async def channel_action(
config: ConfigType,
action_id: core.ID,
template_arg: cg.TemplateArguments,
args: list[tuple],
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_CHANNEL], args, cg.uint8)
cg.add(var.set_channel(template_))
return var
-33
View File
@@ -86,39 +86,6 @@ template<typename... Ts> class SendAction final : public Action<Ts...>, public P
} flags_{0};
};
template<typename... Ts> class AddPeerAction final : public Action<Ts...>, public Parented<ESPNowComponent> {
TEMPLATABLE_VALUE(peer_address_t, address);
protected:
void play(const Ts &...x) override {
peer_address_t address = this->address_.value(x...);
this->parent_->add_peer(address.data());
}
};
template<typename... Ts> class DeletePeerAction final : public Action<Ts...>, public Parented<ESPNowComponent> {
TEMPLATABLE_VALUE(peer_address_t, address);
protected:
void play(const Ts &...x) override {
peer_address_t address = this->address_.value(x...);
this->parent_->del_peer(address.data());
}
};
template<typename... Ts> class SetChannelAction final : public Action<Ts...>, public Parented<ESPNowComponent> {
TEMPLATABLE_VALUE(uint8_t, channel)
protected:
void play(const Ts &...x) override {
if (this->parent_->is_wifi_enabled()) {
return;
}
this->parent_->set_wifi_channel(this->channel_.value(x...));
this->parent_->apply_wifi_channel();
}
};
class OnReceiveTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint16_t>,
public ESPNowReceivedPacketHandler {
public:
@@ -116,9 +116,18 @@ class ESPNowComponent final : public Component {
esp_err_t add_peer(const uint8_t *peer);
// Remove a peer with the esp_now api and remove from the internal list if exists
esp_err_t del_peer(const uint8_t *peer);
// Action entry points; distinct names because add_peer(peer_address_t) only fills the boot-time list
esp_err_t add_peer_from_action(const peer_address_t &address) { return this->add_peer(address.data()); }
esp_err_t del_peer_from_action(const peer_address_t &address) { return this->del_peer(address.data()); }
void set_wifi_channel(uint8_t channel) { this->wifi_channel_ = channel; }
void apply_wifi_channel();
void set_channel_from_action(uint8_t channel) {
if (this->is_wifi_enabled())
return;
this->set_wifi_channel(channel);
this->apply_wifi_channel();
}
uint8_t get_wifi_channel();
void set_auto_add_peer(bool value) { this->auto_add_peer_ = value; }
+6 -17
View File
@@ -16,14 +16,14 @@ from esphome.const import (
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_MOTION,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
setup_entity,
)
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType
CODEOWNERS = ["@nohat"]
@@ -40,8 +40,6 @@ event_ns = cg.esphome_ns.namespace("event")
Event = event_ns.class_("Event", cg.EntityBase)
EventPtr = Event.operator("ptr")
TriggerEventAction = event_ns.class_("TriggerEventAction", automation.Action)
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
_EVENT_SCHEMA = (
@@ -135,20 +133,11 @@ TRIGGER_EVENT_SCHEMA = cv.Schema(
)
@automation.register_action(
"event.trigger", TriggerEventAction, TRIGGER_EVENT_SCHEMA, synchronous=True
automation.register_apply_action(
"event.trigger",
TRIGGER_EVENT_SCHEMA,
automation.ApplyField(CONF_EVENT_TYPE, "trigger", cg.std_string),
)
async def event_fire_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
templ = await cg.templatable(config[CONF_EVENT_TYPE], args, cg.std_string)
cg.add(var.set_event_type(templ))
return var
@coroutine_with_priority(CoroPriority.CORE)
-7
View File
@@ -6,13 +6,6 @@
namespace esphome::event {
template<typename... Ts> class TriggerEventAction final : public Action<Ts...>, public Parented<Event> {
public:
TEMPLATABLE_VALUE(std::string, event_type)
void play(const Ts &...x) override { this->parent_->trigger(this->event_type_.value(x...)); }
};
class EventTrigger final : public Trigger<StringRef> {
public:
EventTrigger(Event *event) {
+6 -49
View File
@@ -31,19 +31,17 @@ from esphome.const import (
CONF_TRIGGER_ID,
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.cpp_generator import LambdaExpression
IS_PLATFORM_COMPONENT = True
fan_ns = cg.esphome_ns.namespace("fan")
Fan = fan_ns.class_("Fan", cg.EntityBase)
FanCall = fan_ns.class_("FanCall")
FanDirection = fan_ns.enum("FanDirection", is_class=True)
FAN_DIRECTION_ENUM = {
@@ -63,7 +61,6 @@ RESTORE_MODES = {
}
# Actions
TurnOnAction = fan_ns.class_("TurnOnAction", automation.Action)
TurnOffAction = fan_ns.class_("TurnOffAction", automation.Action)
ToggleAction = fan_ns.class_("ToggleAction", automation.Action)
CycleSpeedAction = fan_ns.class_("CycleSpeedAction", automation.Action)
@@ -332,9 +329,8 @@ async def fan_turn_off_to_code(config, action_id, template_arg, args):
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
automation.register_apply_action(
"fan.turn_on",
TurnOnAction,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(Fan),
@@ -345,50 +341,11 @@ async def fan_turn_off_to_code(config, action_id, template_arg, args):
),
}
),
synchronous=True,
automation.ApplyField(CONF_OSCILLATING, "set_oscillating", cg.bool_),
automation.ApplyField(CONF_SPEED, "set_speed", cg.int_),
automation.ApplyField(CONF_DIRECTION, "set_direction", FanDirection),
call="turn_on",
)
async def fan_turn_on_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
# All configured fields are folded into a single stateless lambda whose
# constants live in flash; the action stores only a function pointer.
FIELDS = (
(CONF_OSCILLATING, "set_oscillating", cg.bool_),
(CONF_SPEED, "set_speed", cg.int_),
(CONF_DIRECTION, "set_direction", FanDirection),
)
# Normalize trigger args to `const std::remove_cvref_t<T> &` so the
# apply lambda and any inner field lambdas (generated below via
# `process_lambda`) share one parameter spelling that's well-formed for
# any T (value, ref, or const-ref). Matches TurnOnAction::ApplyFn.
normalized_args = [
(cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), n)
for t, n in args
]
fwd_args = ", ".join(name for _, name in args)
body_lines: list[str] = []
for conf_key, setter, type_ in FIELDS:
if (value := config.get(conf_key)) is None:
continue
if isinstance(value, Lambda):
inner = await cg.process_lambda(value, normalized_args, return_type=type_)
body_lines.append(f"call.{setter}(({inner})({fwd_args}));")
else:
body_lines.append(f"call.{setter}({cg.safe_exp(value)});")
apply_args = [
(FanCall.operator("ref"), "call"),
*normalized_args,
]
apply_lambda = LambdaExpression(
["\n".join(body_lines)],
apply_args,
capture="",
return_type=cg.void,
)
return cg.new_Pvariable(action_id, template_arg, paren, apply_lambda)
@automation.register_action(
-27
View File
@@ -6,33 +6,6 @@
namespace esphome::fan {
// All configured fields are baked into a single stateless lambda whose
// constants live in flash. The action only stores one function pointer
// plus one parent pointer, regardless of how many fields the user set.
// Trigger args are forwarded to the apply function so user lambdas
// (e.g. `speed: !lambda "return x;"`) keep working.
//
// Trigger args are normalized to `const std::remove_cvref_t<Ts> &...` so
// the codegen can emit a matching parameter list for both the apply lambda
// and any inner field lambdas without producing invalid C++ source text
// (e.g. `const T & &` if Ts already carries a reference, or `const const
// T &` if Ts already carries a const). This keeps trigger args no-copy
// regardless of whether the trigger supplies `T`, `T &`, or `const T &`.
template<typename... Ts> class TurnOnAction final : public Action<Ts...> {
public:
using ApplyFn = void (*)(FanCall &, const std::remove_cvref_t<Ts> &...);
TurnOnAction(Fan *state, ApplyFn apply) : state_(state), apply_(apply) {}
void play(const Ts &...x) override {
auto call = this->state_->turn_on();
this->apply_(call, x...);
call.perform();
}
Fan *state_;
ApplyFn apply_;
};
template<typename... Ts> class TurnOffAction final : public Action<Ts...> {
public:
explicit TurnOffAction(Fan *state) : state_(state) {}
@@ -2,87 +2,78 @@
namespace esphome::fujitsu_general {
// bytes' bits are reversed for fujitsu, so nibbles are ordered 1, 0, 3, 2, 5, 4, etc...
#define SET_NIBBLE(message, nibble, value) \
((message)[(nibble) / 2] |= ((value) &0b00001111) << (((nibble) % 2) ? 0 : 4))
#define GET_NIBBLE(message, nibble) (((message)[(nibble) / 2] >> (((nibble) % 2) ? 0 : 4)) & 0b00001111)
static const char *const TAG = "fujitsu_general.climate";
// Common header
const uint8_t FUJITSU_GENERAL_COMMON_LENGTH = 6;
const uint8_t FUJITSU_GENERAL_COMMON_BYTE0 = 0x14;
const uint8_t FUJITSU_GENERAL_COMMON_BYTE1 = 0x63;
const uint8_t FUJITSU_GENERAL_COMMON_BYTE2 = 0x00;
const uint8_t FUJITSU_GENERAL_COMMON_BYTE3 = 0x10;
const uint8_t FUJITSU_GENERAL_COMMON_BYTE4 = 0x10;
const uint8_t FUJITSU_GENERAL_MESSAGE_TYPE_BYTE = 5;
constexpr uint8_t FUJITSU_GENERAL_COMMON_LENGTH = 6;
constexpr uint8_t FUJITSU_GENERAL_COMMON_BYTE0 = 0x14;
constexpr uint8_t FUJITSU_GENERAL_COMMON_BYTE1 = 0x63;
constexpr uint8_t FUJITSU_GENERAL_COMMON_BYTE2 = 0x00;
constexpr uint8_t FUJITSU_GENERAL_COMMON_BYTE3 = 0x10;
constexpr uint8_t FUJITSU_GENERAL_COMMON_BYTE4 = 0x10;
constexpr uint8_t FUJITSU_GENERAL_MESSAGE_TYPE_BYTE = 5;
// State message - temp & fan etc.
const uint8_t FUJITSU_GENERAL_STATE_MESSAGE_LENGTH = 16;
const uint8_t FUJITSU_GENERAL_MESSAGE_TYPE_STATE = 0xFE;
constexpr uint8_t FUJITSU_GENERAL_STATE_MESSAGE_LENGTH = 16;
constexpr uint8_t FUJITSU_GENERAL_MESSAGE_TYPE_STATE = 0xFE;
// Util messages - off & eco etc.
const uint8_t FUJITSU_GENERAL_UTIL_MESSAGE_LENGTH = 7;
const uint8_t FUJITSU_GENERAL_MESSAGE_TYPE_OFF = 0x02;
const uint8_t FUJITSU_GENERAL_MESSAGE_TYPE_ECONOMY = 0x09;
const uint8_t FUJITSU_GENERAL_MESSAGE_TYPE_NUDGE = 0x6C;
constexpr uint8_t FUJITSU_GENERAL_UTIL_MESSAGE_LENGTH = 7;
constexpr uint8_t FUJITSU_GENERAL_MESSAGE_TYPE_OFF = 0x02;
constexpr uint8_t FUJITSU_GENERAL_MESSAGE_TYPE_ECONOMY = 0x09;
constexpr uint8_t FUJITSU_GENERAL_MESSAGE_TYPE_NUDGE = 0x6C;
// State header
const uint8_t FUJITSU_GENERAL_STATE_HEADER_BYTE0 = 0x09;
const uint8_t FUJITSU_GENERAL_STATE_HEADER_BYTE1 = 0x30;
constexpr uint8_t FUJITSU_GENERAL_STATE_HEADER_BYTE0 = 0x09;
constexpr uint8_t FUJITSU_GENERAL_STATE_HEADER_BYTE1 = 0x30;
// State footer
const uint8_t FUJITSU_GENERAL_STATE_FOOTER_BYTE0 = 0x20;
// Temperature
const uint8_t FUJITSU_GENERAL_TEMPERATURE_NIBBLE = 16;
constexpr uint8_t FUJITSU_GENERAL_STATE_FOOTER_BYTE0 = 0x20;
// Power on
const uint8_t FUJITSU_GENERAL_POWER_ON_NIBBLE = 17;
const uint8_t FUJITSU_GENERAL_POWER_OFF = 0x00;
const uint8_t FUJITSU_GENERAL_POWER_ON = 0x01;
constexpr uint8_t FUJITSU_GENERAL_POWER_OFF = 0x00;
constexpr uint8_t FUJITSU_GENERAL_POWER_ON = 0x01;
// Mode
const uint8_t FUJITSU_GENERAL_MODE_NIBBLE = 19;
const uint8_t FUJITSU_GENERAL_MODE_AUTO = 0x00;
const uint8_t FUJITSU_GENERAL_MODE_COOL = 0x01;
const uint8_t FUJITSU_GENERAL_MODE_DRY = 0x02;
const uint8_t FUJITSU_GENERAL_MODE_FAN = 0x03;
const uint8_t FUJITSU_GENERAL_MODE_HEAT = 0x04;
// const uint8_t FUJITSU_GENERAL_MODE_10C = 0x0B;
// Bit 3 is the clean flag, which is also 10 degree heat on the ARRAH2E and ARREW4E remotes.
constexpr uint8_t FUJITSU_GENERAL_MODE_MASK = 0b0111;
constexpr uint8_t FUJITSU_GENERAL_CLEAN_BIT = 0b1000;
constexpr uint8_t FUJITSU_GENERAL_MODE_AUTO = 0x00;
constexpr uint8_t FUJITSU_GENERAL_MODE_COOL = 0x01;
constexpr uint8_t FUJITSU_GENERAL_MODE_DRY = 0x02;
constexpr uint8_t FUJITSU_GENERAL_MODE_FAN = 0x03;
constexpr uint8_t FUJITSU_GENERAL_MODE_HEAT = 0x04;
// Swing
const uint8_t FUJITSU_GENERAL_SWING_NIBBLE = 20;
const uint8_t FUJITSU_GENERAL_SWING_NONE = 0x00;
const uint8_t FUJITSU_GENERAL_SWING_VERTICAL = 0x01;
const uint8_t FUJITSU_GENERAL_SWING_HORIZONTAL = 0x02;
const uint8_t FUJITSU_GENERAL_SWING_BOTH = 0x03;
constexpr uint8_t FUJITSU_GENERAL_SWING_MASK = 0b0011;
constexpr uint8_t FUJITSU_GENERAL_SWING_NONE = 0x00;
constexpr uint8_t FUJITSU_GENERAL_SWING_VERTICAL = 0x01;
constexpr uint8_t FUJITSU_GENERAL_SWING_HORIZONTAL = 0x02;
constexpr uint8_t FUJITSU_GENERAL_SWING_BOTH = 0x03;
// Fan
const uint8_t FUJITSU_GENERAL_FAN_NIBBLE = 21;
const uint8_t FUJITSU_GENERAL_FAN_AUTO = 0x00;
const uint8_t FUJITSU_GENERAL_FAN_HIGH = 0x01;
const uint8_t FUJITSU_GENERAL_FAN_MEDIUM = 0x02;
const uint8_t FUJITSU_GENERAL_FAN_LOW = 0x03;
const uint8_t FUJITSU_GENERAL_FAN_SILENT = 0x04;
constexpr uint8_t FUJITSU_GENERAL_FAN_MASK = 0b0111;
constexpr uint8_t FUJITSU_GENERAL_FAN_AUTO = 0x00;
constexpr uint8_t FUJITSU_GENERAL_FAN_HIGH = 0x01;
constexpr uint8_t FUJITSU_GENERAL_FAN_MEDIUM = 0x02;
constexpr uint8_t FUJITSU_GENERAL_FAN_LOW = 0x03;
constexpr uint8_t FUJITSU_GENERAL_FAN_SILENT = 0x04;
// TODO Outdoor Unit Low Noise
// const uint8_t FUJITSU_GENERAL_OUTDOOR_UNIT_LOW_NOISE_BYTE14 = 0xA0;
// const uint8_t FUJITSU_GENERAL_STATE_BYTE14 = 0x20;
const uint16_t FUJITSU_GENERAL_HEADER_MARK = 3300;
const uint16_t FUJITSU_GENERAL_HEADER_SPACE = 1600;
constexpr uint16_t FUJITSU_GENERAL_HEADER_MARK = 3300;
constexpr uint16_t FUJITSU_GENERAL_HEADER_SPACE = 1600;
const uint16_t FUJITSU_GENERAL_BIT_MARK = 420;
const uint16_t FUJITSU_GENERAL_ONE_SPACE = 1200;
const uint16_t FUJITSU_GENERAL_ZERO_SPACE = 420;
constexpr uint16_t FUJITSU_GENERAL_BIT_MARK = 420;
constexpr uint16_t FUJITSU_GENERAL_ONE_SPACE = 1200;
constexpr uint16_t FUJITSU_GENERAL_ZERO_SPACE = 420;
const uint16_t FUJITSU_GENERAL_TRL_MARK = 420;
const uint16_t FUJITSU_GENERAL_TRL_SPACE = 8000;
constexpr uint16_t FUJITSU_GENERAL_TRL_MARK = 420;
constexpr uint16_t FUJITSU_GENERAL_TRL_SPACE = 8000;
const uint32_t FUJITSU_GENERAL_CARRIER_FREQUENCY = 38000;
constexpr uint32_t FUJITSU_GENERAL_CARRIER_FREQUENCY = 38000;
void FujitsuGeneralClimate::transmit_state() {
if (this->mode == climate::CLIMATE_MODE_OFF) {
@@ -111,68 +102,67 @@ void FujitsuGeneralClimate::transmit_state() {
uint8_t temperature_clamped =
(uint8_t) roundf(clamp<float>(this->target_temperature, FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX));
uint8_t temperature_offset = temperature_clamped - FUJITSU_GENERAL_TEMP_MIN;
SET_NIBBLE(remote_state, FUJITSU_GENERAL_TEMPERATURE_NIBBLE, temperature_offset);
set_nibble(remote_state, FUJITSU_GENERAL_TEMPERATURE_NIBBLE, temperature_offset);
// Set power on
if (!this->power_) {
SET_NIBBLE(remote_state, FUJITSU_GENERAL_POWER_ON_NIBBLE, FUJITSU_GENERAL_POWER_ON);
set_nibble(remote_state, FUJITSU_GENERAL_POWER_ON_NIBBLE, FUJITSU_GENERAL_POWER_ON);
}
// Set mode
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_COOL);
set_nibble(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_COOL);
break;
case climate::CLIMATE_MODE_HEAT:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_HEAT);
set_nibble(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_HEAT);
break;
case climate::CLIMATE_MODE_DRY:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_DRY);
set_nibble(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_DRY);
break;
case climate::CLIMATE_MODE_FAN_ONLY:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_FAN);
set_nibble(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_FAN);
break;
case climate::CLIMATE_MODE_HEAT_COOL:
default:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_AUTO);
set_nibble(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_AUTO);
break;
// TODO: CLIMATE_MODE_10C is missing from esphome
}
// Set fan
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) {
case climate::CLIMATE_FAN_HIGH:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_HIGH);
set_nibble(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_HIGH);
break;
case climate::CLIMATE_FAN_MEDIUM:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_MEDIUM);
set_nibble(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_MEDIUM);
break;
case climate::CLIMATE_FAN_LOW:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_LOW);
set_nibble(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_LOW);
break;
case climate::CLIMATE_FAN_QUIET:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_SILENT);
set_nibble(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_SILENT);
break;
case climate::CLIMATE_FAN_AUTO:
default:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_AUTO);
set_nibble(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_AUTO);
break;
}
// Set swing
switch (this->swing_mode) {
case climate::CLIMATE_SWING_VERTICAL:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_SWING_NIBBLE, FUJITSU_GENERAL_SWING_VERTICAL);
set_nibble(remote_state, FUJITSU_GENERAL_SWING_NIBBLE, FUJITSU_GENERAL_SWING_VERTICAL);
break;
case climate::CLIMATE_SWING_HORIZONTAL:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_SWING_NIBBLE, FUJITSU_GENERAL_SWING_HORIZONTAL);
set_nibble(remote_state, FUJITSU_GENERAL_SWING_NIBBLE, FUJITSU_GENERAL_SWING_HORIZONTAL);
break;
case climate::CLIMATE_SWING_BOTH:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_SWING_NIBBLE, FUJITSU_GENERAL_SWING_BOTH);
set_nibble(remote_state, FUJITSU_GENERAL_SWING_NIBBLE, FUJITSU_GENERAL_SWING_BOTH);
break;
case climate::CLIMATE_SWING_OFF:
default:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_SWING_NIBBLE, FUJITSU_GENERAL_SWING_NONE);
set_nibble(remote_state, FUJITSU_GENERAL_SWING_NIBBLE, FUJITSU_GENERAL_SWING_NONE);
break;
}
@@ -243,6 +233,52 @@ uint8_t FujitsuGeneralClimate::checksum_state_(uint8_t const *message) {
uint8_t FujitsuGeneralClimate::checksum_util_(uint8_t const *message) { return 255 - message[5]; }
// These decoders use if chains rather than switches: on ESP8266 the compiler turns a dense switch
// into a lookup table in .rodata, which lives in RAM there.
climate::ClimateMode decode_mode(uint8_t mode_field, climate::ClimateMode current_mode) {
const uint8_t mode = mode_field & FUJITSU_GENERAL_MODE_MASK;
if (mode == FUJITSU_GENERAL_MODE_COOL)
return climate::CLIMATE_MODE_COOL;
if (mode == FUJITSU_GENERAL_MODE_HEAT)
return climate::CLIMATE_MODE_HEAT;
if (mode == FUJITSU_GENERAL_MODE_DRY)
return climate::CLIMATE_MODE_DRY;
if (mode == FUJITSU_GENERAL_MODE_FAN)
return climate::CLIMATE_MODE_FAN_ONLY;
if (mode == FUJITSU_GENERAL_MODE_AUTO)
return climate::CLIMATE_MODE_HEAT_COOL;
// A state frame means the unit is on, so never keep OFF.
ESP_LOGW(TAG, "Received unassigned mode %X, keeping the current mode", mode);
return current_mode == climate::CLIMATE_MODE_OFF ? climate::CLIMATE_MODE_HEAT_COOL : current_mode;
}
optional<climate::ClimateFanMode> decode_fan_mode(uint8_t fan_field, optional<climate::ClimateFanMode> current_mode) {
const uint8_t fan = fan_field & FUJITSU_GENERAL_FAN_MASK;
if (fan == FUJITSU_GENERAL_FAN_HIGH)
return climate::CLIMATE_FAN_HIGH;
if (fan == FUJITSU_GENERAL_FAN_MEDIUM)
return climate::CLIMATE_FAN_MEDIUM;
if (fan == FUJITSU_GENERAL_FAN_LOW)
return climate::CLIMATE_FAN_LOW;
if (fan == FUJITSU_GENERAL_FAN_SILENT)
return climate::CLIMATE_FAN_QUIET;
if (fan == FUJITSU_GENERAL_FAN_AUTO)
return climate::CLIMATE_FAN_AUTO;
ESP_LOGW(TAG, "Received unassigned fan speed %X, keeping the current fan mode", fan);
return current_mode;
}
climate::ClimateSwingMode decode_swing_mode(uint8_t swing_field) {
const uint8_t swing = swing_field & FUJITSU_GENERAL_SWING_MASK;
if (swing == FUJITSU_GENERAL_SWING_VERTICAL)
return climate::CLIMATE_SWING_VERTICAL;
if (swing == FUJITSU_GENERAL_SWING_HORIZONTAL)
return climate::CLIMATE_SWING_HORIZONTAL;
if (swing == FUJITSU_GENERAL_SWING_BOTH)
return climate::CLIMATE_SWING_BOTH;
return climate::CLIMATE_SWING_OFF;
}
bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) {
ESP_LOGV(TAG, "Received IR message");
@@ -316,70 +352,25 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) {
}
if (recv_message_type == FUJITSU_GENERAL_MESSAGE_TYPE_STATE) {
const uint8_t recv_tempertature = GET_NIBBLE(recv_message, FUJITSU_GENERAL_TEMPERATURE_NIBBLE);
const uint8_t recv_tempertature = get_nibble(recv_message, FUJITSU_GENERAL_TEMPERATURE_NIBBLE);
const uint8_t offset_temperature = recv_tempertature + FUJITSU_GENERAL_TEMP_MIN;
this->target_temperature = offset_temperature;
ESP_LOGV(TAG, "Received temperature %d", offset_temperature);
const uint8_t recv_mode = GET_NIBBLE(recv_message, FUJITSU_GENERAL_MODE_NIBBLE);
const uint8_t recv_mode = get_nibble(recv_message, FUJITSU_GENERAL_MODE_NIBBLE);
ESP_LOGV(TAG, "Received mode %X", recv_mode);
switch (recv_mode) {
case FUJITSU_GENERAL_MODE_COOL:
this->mode = climate::CLIMATE_MODE_COOL;
break;
case FUJITSU_GENERAL_MODE_HEAT:
this->mode = climate::CLIMATE_MODE_HEAT;
break;
case FUJITSU_GENERAL_MODE_DRY:
this->mode = climate::CLIMATE_MODE_DRY;
break;
case FUJITSU_GENERAL_MODE_FAN:
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
break;
case FUJITSU_GENERAL_MODE_AUTO:
default:
// TODO: CLIMATE_MODE_10C is missing from esphome
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
break;
if ((recv_mode & FUJITSU_GENERAL_CLEAN_BIT) != 0) {
ESP_LOGW(TAG, "Received a frame with the clean / 10 degree heat bit set, which is not supported");
}
this->mode = decode_mode(recv_mode, this->mode);
const uint8_t recv_fan_mode = GET_NIBBLE(recv_message, FUJITSU_GENERAL_FAN_NIBBLE);
const uint8_t recv_fan_mode = get_nibble(recv_message, FUJITSU_GENERAL_FAN_NIBBLE);
ESP_LOGV(TAG, "Received fan mode %X", recv_fan_mode);
switch (recv_fan_mode) {
case FUJITSU_GENERAL_FAN_SILENT:
this->fan_mode = climate::CLIMATE_FAN_QUIET;
break;
case FUJITSU_GENERAL_FAN_LOW:
this->fan_mode = climate::CLIMATE_FAN_LOW;
break;
case FUJITSU_GENERAL_FAN_MEDIUM:
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
break;
case FUJITSU_GENERAL_FAN_HIGH:
this->fan_mode = climate::CLIMATE_FAN_HIGH;
break;
case FUJITSU_GENERAL_FAN_AUTO:
default:
this->fan_mode = climate::CLIMATE_FAN_AUTO;
break;
}
this->fan_mode = decode_fan_mode(recv_fan_mode, this->fan_mode);
const uint8_t recv_swing_mode = GET_NIBBLE(recv_message, FUJITSU_GENERAL_SWING_NIBBLE);
const uint8_t recv_swing_mode = get_nibble(recv_message, FUJITSU_GENERAL_SWING_NIBBLE);
ESP_LOGV(TAG, "Received swing mode %X", recv_swing_mode);
switch (recv_swing_mode) {
case FUJITSU_GENERAL_SWING_VERTICAL:
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
break;
case FUJITSU_GENERAL_SWING_HORIZONTAL:
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
break;
case FUJITSU_GENERAL_SWING_BOTH:
this->swing_mode = climate::CLIMATE_SWING_BOTH;
break;
case FUJITSU_GENERAL_SWING_NONE:
default:
this->swing_mode = climate::CLIMATE_SWING_OFF;
}
this->swing_mode = decode_swing_mode(recv_swing_mode);
this->power_ = true;
}
@@ -7,8 +7,8 @@
namespace esphome::fujitsu_general {
const uint8_t FUJITSU_GENERAL_TEMP_MIN = 16; // Celsius // TODO 16 for heating, 18 for cooling, unsupported in ESPH
const uint8_t FUJITSU_GENERAL_TEMP_MAX = 30; // Celsius
constexpr uint8_t FUJITSU_GENERAL_TEMP_MIN = 16; // Celsius // TODO 16 for heating, 18 for cooling, unsupported in ESPH
constexpr uint8_t FUJITSU_GENERAL_TEMP_MAX = 30; // Celsius
// clang-format off
/**
@@ -43,9 +43,36 @@ const uint8_t FUJITSU_GENERAL_TEMP_MAX = 30; // Celsius
* heat 30 swing vert 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00101000 00000000 00000000 00000000 00000100 00011101
* heat 30 noswing 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00100000 00000000 00000000 00000000 00000100 00010011
* ```
*
* The column markers show which bits varied in these captures, not field widths.
*/
// clang-format on
// Bits are reversed within each byte, so an odd nibble index is the low half of its byte.
constexpr uint8_t get_nibble(const uint8_t *message, uint8_t nibble) {
return (message[nibble / 2] >> ((nibble % 2) ? 0 : 4)) & 0b00001111;
}
/// Write a nibble into a zero-initialised frame.
constexpr void set_nibble(uint8_t *message, uint8_t nibble, uint8_t value) {
message[nibble / 2] |= (value & 0b00001111) << ((nibble % 2) ? 0 : 4);
}
// Nibble indices of the state frame fields.
constexpr uint8_t FUJITSU_GENERAL_TEMPERATURE_NIBBLE = 16;
constexpr uint8_t FUJITSU_GENERAL_POWER_ON_NIBBLE = 17;
constexpr uint8_t FUJITSU_GENERAL_MODE_NIBBLE = 19;
constexpr uint8_t FUJITSU_GENERAL_SWING_NIBBLE = 20;
constexpr uint8_t FUJITSU_GENERAL_FAN_NIBBLE = 21;
/// Unassigned values keep the current mode, except that OFF becomes HEAT_COOL.
climate::ClimateMode decode_mode(uint8_t mode_field, climate::ClimateMode current_mode);
/// Unassigned values keep the current fan mode.
optional<climate::ClimateFanMode> decode_fan_mode(uint8_t fan_field, optional<climate::ClimateFanMode> current_mode);
climate::ClimateSwingMode decode_swing_mode(uint8_t swing_field);
class FujitsuGeneralClimate final : public climate_ir::ClimateIR {
public:
FujitsuGeneralClimate()
+9 -25
View File
@@ -8,8 +8,7 @@ from esphome.const import (
CONF_TYPE,
CONF_VALUE,
)
from esphome.core import ID, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.core import CoroPriority, coroutine_with_priority
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
@@ -21,7 +20,6 @@ RestoringGlobalsComponent = globals_ns.class_(
RestoringGlobalStringComponent = globals_ns.class_(
"RestoringGlobalStringComponent", cg.PollingComponent
)
GlobalVarSetAction = globals_ns.class_("GlobalVarSetAction", automation.Action)
CONF_MAX_RESTORE_DATA_LENGTH = "max_restore_data_length"
@@ -94,33 +92,19 @@ async def to_code(config: ConfigType) -> None:
cg.add(glob.set_name_hash(hash_))
@automation.register_action(
# A lambda returns the global's own type so `return 42;` reduces to a cast; the value
# itself is raw C++ text.
_VALUE_TYPE = "std::remove_reference_t<decltype({parent}->value())>"
automation.register_apply_action(
"globals.set",
GlobalVarSetAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(GlobalsComponent),
cv.Required(CONF_VALUE): cv.templatable(cv.string_strict),
}
),
synchronous=True,
automation.ApplyField(
CONF_VALUE, "value() = {}", _VALUE_TYPE, const_fn=lambda _, value: value
),
)
async def globals_set_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
template_arg = cg.TemplateArguments(full_id.type, *template_arg)
var = cg.new_Pvariable(action_id, template_arg, paren)
# Use the global's value_type alias as the lambda return type so
# TemplatableFn stores a direct function pointer instead of going through
# the deprecated converting trampoline when the value expression deduces
# to a different type (e.g. int literal assigned to a float global).
value_type = cg.RawExpression(f"{full_id.type}::value_type")
templ = await cg.templatable(
config[CONF_VALUE], args, value_type, to_exp=cg.RawExpression
)
cg.add(var.set_value(templ))
return var
+3 -15
View File
@@ -1,8 +1,10 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include <array>
#include <cstring>
namespace esphome::globals {
@@ -127,20 +129,6 @@ template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public P
ESPPreferenceObject rtc_;
};
template<class C, typename... Ts> class GlobalVarSetAction final : public Action<Ts...> {
public:
explicit GlobalVarSetAction(C *parent) : parent_(parent) {}
using T = typename C::value_type;
TEMPLATABLE_VALUE(T, value);
void play(const Ts &...x) override { this->parent_->value() = this->value_.value(x...); }
protected:
C *parent_;
};
template<typename T> T &id(GlobalsComponent<T> *value) { return value->value(); }
template<typename T> T &id(RestoringGlobalsComponent<T> *value) { return value->value(); }
template<typename T, uint8_t SZ> T &id(RestoringGlobalStringComponent<T, SZ> *value) { return value->value(); }
+11 -22
View File
@@ -5,17 +5,6 @@ namespace esphome::gree {
static const char *const TAG = "gree.climate";
climate::ClimateTraits GreeClimate::traits() {
auto t = climate_ir::ClimateIR::traits();
// ClimateIR unconditionally includes HEAT_COOL in the base mode set; remove it when heat is not supported.
if (!this->supports_heat_) {
auto modes = t.get_supported_modes();
modes.erase(climate::CLIMATE_MODE_HEAT_COOL);
t.set_supported_modes(modes);
}
return t;
}
void GreeClimate::set_model(Model model) {
if (model == GREE_YAN) {
// YAN only has a vertical vane; the horizontal swing IR bytes are not defined for this model.
@@ -42,23 +31,24 @@ void GreeClimate::set_mode_bit(uint8_t bit_mask, bool enabled) {
void GreeClimate::transmit_state() {
uint8_t remote_state[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00};
const Model model = this->model_;
remote_state[0] = this->fan_speed_() | this->operation_mode_();
remote_state[1] = this->temperature_();
if (this->model_ == GREE_YAN) {
if (model == GREE_YAN) {
remote_state[2] = 0x20; // bits 0..3 always 0000, bits 4..7 TURBO, LIGHT, HEALTH, X-FAN
remote_state[3] = 0x50; // bits 4..7 always 0101
remote_state[4] = this->vertical_swing_();
}
if (this->model_ == GREE_YX1FF || this->model_ == GREE_YAG) {
if (model == GREE_YX1FF || model == GREE_YAG) {
remote_state[2] = 0x60;
remote_state[3] = 0x50;
remote_state[4] = this->vertical_swing_();
}
if (this->model_ == GREE_YAG) {
if (model == GREE_YAG) {
remote_state[5] = 0x40;
if (this->vertical_swing_() == GREE_VDIR_SWING || this->horizontal_swing_() == GREE_HDIR_SWING) {
@@ -66,11 +56,11 @@ void GreeClimate::transmit_state() {
}
}
if (this->model_ == GREE_YAC || this->model_ == GREE_YAG) {
if (model == GREE_YAC || model == GREE_YAG) {
remote_state[4] |= (this->horizontal_swing_() << 4);
}
if (this->model_ == GREE_YAA || this->model_ == GREE_YAC || this->model_ == GREE_YAC1FB9) {
if (model == GREE_YAA || model == GREE_YAC || model == GREE_YAC1FB9) {
remote_state[2] = 0x20; // bits 0..3 always 0000, bits 4..7 TURBO, LIGHT, HEALTH, X-FAN
remote_state[3] = 0x50; // bits 4..7 always 0101
remote_state[6] = 0x20; // YAA1FB, FAA1FB1, YB1F2 bits 4..7 always 0010
@@ -82,14 +72,13 @@ void GreeClimate::transmit_state() {
}
}
if (this->model_ == GREE_YAN || this->model_ == GREE_YAA || this->model_ == GREE_YAC ||
this->model_ == GREE_YAC1FB9) {
if (model == GREE_YAN || model == GREE_YAA || model == GREE_YAC || model == GREE_YAC1FB9) {
// Merge the mode bits into remote_state[2]
// Clear the mode bits (bits 4-7) and OR in the current mode_bits_
remote_state[2] = (remote_state[2] & 0x0F) | this->mode_bits_;
}
if (this->model_ == GREE_YX1FF) {
if (model == GREE_YX1FF) {
if (this->fan_speed_() == GREE_FAN_TURBO) {
remote_state[2] |= GREE_FAN_TURBO_BIT;
}
@@ -100,7 +89,7 @@ void GreeClimate::transmit_state() {
}
// Calculate the checksum
if (this->model_ == GREE_YAN || this->model_ == GREE_YX1FF) {
if (model == GREE_YAN || model == GREE_YX1FF) {
remote_state[7] = ((remote_state[0] << 4) + (remote_state[1] << 4) + 0xC0);
} else {
remote_state[7] =
@@ -115,7 +104,7 @@ void GreeClimate::transmit_state() {
data->set_carrier_frequency(GREE_IR_FREQUENCY);
data->mark(GREE_HEADER_MARK);
if (this->model_ == GREE_YAC1FB9) {
if (model == GREE_YAC1FB9) {
data->space(GREE_YAC1FB9_HEADER_SPACE);
} else {
data->space(GREE_HEADER_SPACE);
@@ -137,7 +126,7 @@ void GreeClimate::transmit_state() {
data->space(GREE_ZERO_SPACE);
data->mark(GREE_BIT_MARK);
if (this->model_ == GREE_YAC1FB9) {
if (model == GREE_YAC1FB9) {
data->space(GREE_YAC1FB9_MESSAGE_SPACE);
} else {
data->space(GREE_MESSAGE_SPACE);
+1 -2
View File
@@ -77,7 +77,7 @@ static constexpr uint8_t GREE_PRESET_SLEEP = 0x01;
static constexpr uint8_t GREE_PRESET_SLEEP_BIT = 0x80;
// Model codes
enum Model { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9, GREE_YX1FF, GREE_YAG };
enum Model : uint8_t { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9, GREE_YX1FF, GREE_YAG };
class GreeClimate final : public climate_ir::ClimateIR {
public:
@@ -94,7 +94,6 @@ class GreeClimate final : public climate_ir::ClimateIR {
protected:
// Transmit via IR the state of this climate controller.
void transmit_state() override;
climate::ClimateTraits traits() override;
uint8_t operation_mode_();
uint8_t fan_speed_();
+13 -50
View File
@@ -27,11 +27,6 @@ HlkFm22xComponent = hlk_fm22x_ns.class_(
"HlkFm22xComponent", cg.PollingComponent, uart.UARTDevice
)
EnrollmentAction = hlk_fm22x_ns.class_("EnrollmentAction", automation.Action)
DeleteAction = hlk_fm22x_ns.class_("DeleteAction", automation.Action)
DeleteAllAction = hlk_fm22x_ns.class_("DeleteAllAction", automation.Action)
ScanAction = hlk_fm22x_ns.class_("ScanAction", automation.Action)
ResetAction = hlk_fm22x_ns.class_("ResetAction", automation.Action)
CONFIG_SCHEMA = cv.All(
cv.Schema(
@@ -101,9 +96,8 @@ async def to_code(config):
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
@automation.register_action(
automation.register_apply_action(
"hlk_fm22x.enroll",
EnrollmentAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
@@ -112,22 +106,14 @@ async def to_code(config):
},
key=CONF_NAME,
),
synchronous=True,
automation.ApplyCall(
"enroll_face({}, static_cast<hlk_fm22x::HlkFm22xFaceDirection>({}))",
((CONF_NAME, cg.std_string), (CONF_DIRECTION, cg.uint8)),
),
)
async def hlk_fm22x_enroll_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_NAME], args, cg.std_string)
cg.add(var.set_name(template_))
template_ = await cg.templatable(config[CONF_DIRECTION], args, cg.uint8)
cg.add(var.set_direction(template_))
return var
@automation.register_action(
automation.register_apply_action(
"hlk_fm22x.delete",
DeleteAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
@@ -135,60 +121,37 @@ async def hlk_fm22x_enroll_to_code(config, action_id, template_arg, args):
},
key=CONF_FACE_ID,
),
synchronous=True,
automation.ApplyField(CONF_FACE_ID, "delete_face", cg.int16),
)
async def hlk_fm22x_delete_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_FACE_ID], args, cg.int16)
cg.add(var.set_face_id(template_))
return var
@automation.register_action(
automation.register_apply_action(
"hlk_fm22x.delete_all",
DeleteAllAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
}
),
synchronous=True,
automation.ApplyCall("delete_all_faces()"),
)
async def hlk_fm22x_delete_all_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
@automation.register_action(
automation.register_apply_action(
"hlk_fm22x.scan",
ScanAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
}
),
synchronous=True,
automation.ApplyCall("scan_face()"),
)
async def hlk_fm22x_scan_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
@automation.register_action(
automation.register_apply_action(
"hlk_fm22x.reset",
ResetAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
}
),
synchronous=True,
automation.ApplyCall("reset()"),
)
async def hlk_fm22x_reset_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
-37
View File
@@ -141,41 +141,4 @@ class HlkFm22xComponent final : public PollingComponent, public uart::UARTDevice
CallbackManager<void(uint8_t)> enrollment_failed_callback_;
};
template<typename... Ts> class EnrollmentAction final : public Action<Ts...>, public Parented<HlkFm22xComponent> {
public:
TEMPLATABLE_VALUE(std::string, name)
TEMPLATABLE_VALUE(uint8_t, direction)
void play(const Ts &...x) override {
auto name = this->name_.value(x...);
auto direction = (HlkFm22xFaceDirection) this->direction_.value(x...);
this->parent_->enroll_face(name, direction);
}
};
template<typename... Ts> class DeleteAction final : public Action<Ts...>, public Parented<HlkFm22xComponent> {
public:
TEMPLATABLE_VALUE(int16_t, face_id)
void play(const Ts &...x) override {
auto face_id = this->face_id_.value(x...);
this->parent_->delete_face(face_id);
}
};
template<typename... Ts> class DeleteAllAction final : public Action<Ts...>, public Parented<HlkFm22xComponent> {
public:
void play(const Ts &...x) override { this->parent_->delete_all_faces(); }
};
template<typename... Ts> class ScanAction final : public Action<Ts...>, public Parented<HlkFm22xComponent> {
public:
void play(const Ts &...x) override { this->parent_->scan_face(); }
};
template<typename... Ts> class ResetAction final : public Action<Ts...>, public Parented<HlkFm22xComponent> {
public:
void play(const Ts &...x) override { this->parent_->reset(); }
};
} // namespace esphome::hlk_fm22x
-15
View File
@@ -2,7 +2,6 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/automation.h"
#include "esphome/components/output/float_output.h"
#include <cstdint>
@@ -43,20 +42,6 @@ class LEDCOutput final : public output::FloatOutput, public Component {
bool initialized_ = false;
};
template<typename... Ts> class SetFrequencyAction final : public Action<Ts...> {
public:
SetFrequencyAction(LEDCOutput *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(float, frequency);
void play(const Ts &...x) {
float freq = this->frequency_.value(x...);
this->parent_->update_frequency(freq);
}
protected:
LEDCOutput *parent_;
};
} // namespace esphome::ledc
#endif
+2 -17
View File
@@ -12,8 +12,6 @@ from esphome.const import (
CONF_PHASE_ANGLE,
CONF_PIN,
)
from esphome.core import ID
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
DEPENDENCIES = ["esp32"]
@@ -45,7 +43,6 @@ def validate_frequency(value: Any) -> float:
ledc_ns = cg.esphome_ns.namespace("ledc")
LEDCOutput = ledc_ns.class_("LEDCOutput", output.FloatOutput, cg.Component)
SetFrequencyAction = ledc_ns.class_("SetFrequencyAction", automation.Action)
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
@@ -77,25 +74,13 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_phase_angle(config[CONF_PHASE_ANGLE]))
@automation.register_action(
automation.register_apply_action(
"output.ledc.set_frequency",
SetFrequencyAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(LEDCOutput),
cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency),
}
),
synchronous=True,
automation.ApplyField(CONF_FREQUENCY, "update_frequency", cg.float_),
)
async def ledc_set_frequency_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_FREQUENCY], args, cg.float_)
cg.add(var.set_frequency(template_))
return var
+6
View File
@@ -513,6 +513,8 @@ async def component_to_code(config):
# it for project source files only. GCC uses the last -O flag.
build_src_flags += " -Os"
cg.add_platformio_option("build_src_flags", build_src_flags)
# Must run before the platform's builder scripts are loaded; see the script.
cg.add_platformio_option("extra_scripts", ["pre:scons_dont_inherit.py"])
cg.add_platformio_option("extra_scripts", ["pre:ccache.py"])
# IRAM_ATTR is a no-op on BK72xx (SDK masks FIQ+IRQ around flash ops).
# On other families, patch_linker.py routes .sram.text into the right
@@ -618,4 +620,8 @@ def copy_files() -> None:
patch_linker_file,
CORE.relative_build_path("patch_linker.py"),
)
copy_file_if_changed(
script_dir / "scons_dont_inherit.py.script",
CORE.relative_build_path("scons_dont_inherit.py"),
)
copy_ccache_script()
@@ -0,0 +1,16 @@
# pylint: disable=E0602
Import("env") # noqa
import builtins
import functools
import sys
# SCons 4.11 compiles SConscript files without dont_inherit=True, so its own
# `from __future__ import annotations` leaks into every builder script and
# turns their annotations into strings. That breaks LibreTiny's Library
# dataclass, whose InitVar is then no longer recognised. Runs before the
# platform's builder scripts are loaded. The module comes from sys.modules
# because SCons.Script also exports a function named SConscript.
sconscript = sys.modules["SCons.Script.SConscript"]
if sconscript.__dict__.get("compile") is None:
sconscript.compile = functools.partial(builtins.compile, dont_inherit=True)
@@ -2,7 +2,6 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/automation.h"
#include "esphome/components/output/float_output.h"
#ifdef USE_LIBRETINY
@@ -34,20 +33,6 @@ class LibreTinyPWM final : public output::FloatOutput, public Component {
bool initialized_ = false;
};
template<typename... Ts> class SetFrequencyAction final : public Action<Ts...> {
public:
SetFrequencyAction(LibreTinyPWM *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(float, frequency);
void play(const Ts &...x) {
float freq = this->frequency_.value(x...);
this->parent_->update_frequency(freq);
}
protected:
LibreTinyPWM *parent_;
};
} // namespace esphome::libretiny_pwm
#endif
+2 -17
View File
@@ -3,15 +3,12 @@ import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_PIN
from esphome.core import ID
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
DEPENDENCIES = ["libretiny"]
libretinypwm_ns = cg.esphome_ns.namespace("libretiny_pwm")
LibreTinyPWM = libretinypwm_ns.class_("LibreTinyPWM", output.FloatOutput, cg.Component)
SetFrequencyAction = libretinypwm_ns.class_("SetFrequencyAction", automation.Action)
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
@@ -32,25 +29,13 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_frequency(config[CONF_FREQUENCY]))
@automation.register_action(
automation.register_apply_action(
"output.libretiny_pwm.set_frequency",
SetFrequencyAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(LibreTinyPWM),
cv.Required(CONF_FREQUENCY): cv.templatable(cv.int_),
}
),
synchronous=True,
automation.ApplyField(CONF_FREQUENCY, "update_frequency", cg.float_),
)
async def libretiny_pwm_set_frequency_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_FREQUENCY], args, cg.float_)
cg.add(var.set_frequency(template_))
return var
-28
View File
@@ -31,34 +31,6 @@ template<bool HasTransitionLength, typename... Ts> class ToggleAction final : pu
transition_length_{};
};
// All configured fields are baked into a single stateless lambda whose
// constants live in flash. The action only stores one function pointer
// plus one parent pointer, regardless of how many fields the user set.
// Trigger args are forwarded to the apply function so user lambdas
// (e.g. `brightness: !lambda "return x;"`) keep working.
//
// Trigger args are normalized to `const std::remove_cvref_t<Ts> &...` so
// the codegen can emit a matching parameter list for both the apply lambda
// and any inner field lambdas without producing invalid C++ source text
// (e.g. `const T & &` if Ts already carries a reference, or `const const
// T &` if Ts already carries a const). This keeps trigger args no-copy
// regardless of whether the trigger supplies `T`, `T &`, or `const T &`.
template<typename... Ts> class LightControlAction final : public Action<Ts...> {
public:
using ApplyFn = void (*)(LightState *, LightCall &, const std::remove_cvref_t<Ts> &...);
LightControlAction(LightState *parent, ApplyFn apply) : parent_(parent), apply_(apply) {}
void play(const Ts &...x) override {
auto call = this->parent_->make_call();
this->apply_(this->parent_, call, x...);
call.perform();
}
protected:
LightState *parent_;
ApplyFn apply_;
};
template<bool HasTransitionLength, typename... Ts> class DimRelativeAction final : public Action<Ts...> {
public:
explicit DimRelativeAction(LightState *parent) : parent_(parent) {}
+42 -83
View File
@@ -27,7 +27,7 @@ from esphome.const import (
CONF_WHITE,
)
from esphome.core import CORE, ID, EsphomeError, Lambda
from esphome.cpp_generator import LambdaExpression, MockObj, TemplateArgsType
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
from .types import (
@@ -37,8 +37,6 @@ from .types import (
AddressableSet,
ColorMode,
DimRelativeAction,
LightCall,
LightControlAction,
LightEffectCycleAction,
LightIsOffCondition,
LightIsOnCondition,
@@ -147,7 +145,7 @@ LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id(
)
def _resolve_effect_index(config: ConfigType) -> int:
def _resolve_effect_index(config: ConfigType, original_name: str) -> int:
"""Resolve a static effect name to its 1-based index at codegen time.
Effect index 0 means "None" (no effect). Effects are 1-indexed matching
@@ -155,7 +153,6 @@ def _resolve_effect_index(config: ConfigType) -> int:
"""
from . import available_effects_str, find_effect_index
original_name = config[CONF_EFFECT]
if original_name.lower() == "none":
return 0
light_id = config[CONF_ID]
@@ -173,87 +170,49 @@ def _resolve_effect_index(config: ConfigType) -> int:
)
@automation.register_action(
"light.turn_off", LightControlAction, LIGHT_TURN_OFF_ACTION_SCHEMA, synchronous=True
def _effect_index(config: ConfigType, value: str) -> str:
# Resolved at codegen time; the cast picks set_effect(uint32_t) over the optional overload.
return f"static_cast<uint32_t>({_resolve_effect_index(config, value)})"
_LIGHT_CONTROL_FIELDS = (
automation.ApplyField(CONF_COLOR_MODE, "set_color_mode", ColorMode),
automation.ApplyField(CONF_STATE, "set_state", cg.bool_),
automation.ApplyField(CONF_TRANSITION_LENGTH, "set_transition_length", cg.uint32),
automation.ApplyField(CONF_FLASH_LENGTH, "set_flash_length", cg.uint32),
automation.ApplyField(CONF_BRIGHTNESS, "set_brightness", cg.float_),
automation.ApplyField(CONF_COLOR_BRIGHTNESS, "set_color_brightness", cg.float_),
automation.ApplyField(CONF_RED, "set_red", cg.float_),
automation.ApplyField(CONF_GREEN, "set_green", cg.float_),
automation.ApplyField(CONF_BLUE, "set_blue", cg.float_),
automation.ApplyField(CONF_WHITE, "set_white", cg.float_),
automation.ApplyField(CONF_COLOR_TEMPERATURE, "set_color_temperature", cg.float_),
automation.ApplyField(CONF_COLD_WHITE, "set_cold_white", cg.float_),
automation.ApplyField(CONF_WARM_WHITE, "set_warm_white", cg.float_),
automation.ApplyField(
CONF_EFFECT, "set_effect", cg.std_string, const_fn=_effect_index
),
)
@automation.register_action(
"light.turn_on", LightControlAction, LIGHT_TURN_ON_ACTION_SCHEMA, synchronous=True
automation.register_apply_action(
"light.turn_off",
LIGHT_TURN_OFF_ACTION_SCHEMA,
automation.ApplyField(CONF_STATE, "set_state", cg.bool_),
automation.ApplyField(CONF_TRANSITION_LENGTH, "set_transition_length", cg.uint32),
call="make_call",
)
@automation.register_action(
"light.control", LightControlAction, LIGHT_CONTROL_ACTION_SCHEMA, synchronous=True
automation.register_apply_action(
"light.turn_on",
LIGHT_TURN_ON_ACTION_SCHEMA,
*_LIGHT_CONTROL_FIELDS,
call="make_call",
)
automation.register_apply_action(
"light.control",
LIGHT_CONTROL_ACTION_SCHEMA,
*_LIGHT_CONTROL_FIELDS,
call="make_call",
)
async def light_control_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
# All configured fields are folded into a single stateless lambda whose
# constants live in flash; the action stores only a function pointer.
FIELDS = (
(CONF_COLOR_MODE, "set_color_mode", ColorMode),
(CONF_STATE, "set_state", cg.bool_),
(CONF_TRANSITION_LENGTH, "set_transition_length", cg.uint32),
(CONF_FLASH_LENGTH, "set_flash_length", cg.uint32),
(CONF_BRIGHTNESS, "set_brightness", cg.float_),
(CONF_COLOR_BRIGHTNESS, "set_color_brightness", cg.float_),
(CONF_RED, "set_red", cg.float_),
(CONF_GREEN, "set_green", cg.float_),
(CONF_BLUE, "set_blue", cg.float_),
(CONF_WHITE, "set_white", cg.float_),
(CONF_COLOR_TEMPERATURE, "set_color_temperature", cg.float_),
(CONF_COLD_WHITE, "set_cold_white", cg.float_),
(CONF_WARM_WHITE, "set_warm_white", cg.float_),
)
# Normalize trigger args to `const std::remove_cvref_t<T> &` so the
# apply lambda and any inner field lambdas (generated below via
# `process_lambda`) share one parameter spelling that's well-formed for
# any T (value, ref, or const-ref). Matches LightControlAction::ApplyFn.
normalized_args = [
(cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), n)
for t, n in args
]
fwd_args = ", ".join(name for _, name in args)
body_lines: list[str] = []
for conf_key, setter, type_ in FIELDS:
if conf_key not in config:
continue
value = config[conf_key]
if isinstance(value, Lambda):
inner = await cg.process_lambda(value, normalized_args, return_type=type_)
body_lines.append(f"call.{setter}(({inner})({fwd_args}));")
else:
body_lines.append(f"call.{setter}({cg.safe_exp(value)});")
if CONF_EFFECT in config:
if isinstance(config[CONF_EFFECT], Lambda):
inner_lambda = await cg.process_lambda(
config[CONF_EFFECT], normalized_args, return_type=cg.std_string
)
body_lines.append(
f"{{ auto __effect_s = ({inner_lambda})({fwd_args});\n"
f"call.set_effect(parent->get_effect_index("
f"__effect_s.c_str(), __effect_s.size())); }}"
)
else:
# Cast disambiguates between set_effect(uint32_t) and
# set_effect(optional<uint32_t>) when the literal is an int.
body_lines.append(
f"call.set_effect(static_cast<uint32_t>({_resolve_effect_index(config)}));"
)
apply_args = [
(LightState.operator("ptr"), "parent"),
(LightCall.operator("ref"), "call"),
*normalized_args,
]
apply_lambda = LambdaExpression(
["\n".join(body_lines)],
apply_args,
capture="",
return_type=cg.void,
)
return cg.new_Pvariable(action_id, template_arg, paren, apply_lambda)
def _record_effect_cycle_ref(config: ConfigType) -> ConfigType:
-2
View File
@@ -14,7 +14,6 @@ Color = cg.esphome_ns.class_("Color")
LightColorValues = light_ns.class_("LightColorValues")
LightStateRTCState = light_ns.struct("LightStateRTCState")
LightCall = light_ns.class_("LightCall")
# Addressable strips
ChannelColors = light_ns.struct("ChannelColors")
@@ -42,7 +41,6 @@ LIMIT_MODES = {
# Actions
ToggleAction = light_ns.class_("ToggleAction", automation.Action)
LightControlAction = light_ns.class_("LightControlAction", automation.Action)
LightEffectCycleAction = light_ns.class_("LightEffectCycleAction", automation.Action)
DimRelativeAction = light_ns.class_("DimRelativeAction", automation.Action)
AddressableSet = light_ns.class_("AddressableSet", automation.Action)
@@ -37,7 +37,6 @@ LN882HBLETracker = ln882h_ble_tracker_ns.class_(
)
StartScanAction = ln882h_ble_tracker_ns.class_("StartScanAction", automation.Action)
StopScanAction = ln882h_ble_tracker_ns.class_("StopScanAction", automation.Action)
ESPBTAdvertiseTrigger = ble_automation.ESPBTAdvertiseTrigger
BLEServiceDataAdvertiseTrigger = ble_automation.BLEServiceDataAdvertiseTrigger
@@ -103,9 +102,8 @@ async def start_scan_action_to_code(
return var
@automation.register_action(
automation.register_apply_action(
"ln882h_ble_tracker.stop_scan",
StopScanAction,
automation.maybe_simple_id(
cv.Schema(
{
@@ -113,17 +111,8 @@ async def start_scan_action_to_code(
}
)
),
synchronous=True,
automation.ApplyCall("stop_scan()"),
)
async def stop_scan_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: list,
) -> cg.MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
async def to_code(config: ConfigType) -> None:
@@ -37,11 +37,6 @@ template<typename... Ts> class StartScanAction final : public Action<Ts...>, pub
}
};
template<typename... Ts> class StopScanAction final : public Action<Ts...>, public Parented<LN882HBLETracker> {
public:
void play(const Ts &...x) override { this->parent_->stop_scan(); }
};
} // namespace esphome::ln882h_ble_tracker
#endif // USE_LIBRETINY
+2 -2
View File
@@ -292,8 +292,8 @@ BASE_PROPS = {
"LV_TEXT_DECOR_", "NONE", "UNDERLINE", "STRIKETHROUGH"
).several_of,
"text_font": lv_font,
"text_letter_space": lvalid.lv_positive_int,
"text_line_space": lvalid.lv_positive_int,
"text_letter_space": lvalid.lv_int,
"text_line_space": lvalid.lv_int,
"text_opa": lvalid.opacity,
"text_outline_stroke_color": lvalid.lv_color,
"text_outline_stroke_opa": lvalid.opacity,
@@ -118,6 +118,9 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
}
#endif
#endif
#ifdef USE_OTA_SIGNED_VERIFICATION
txt_count++; // ota_signed
#endif
#ifdef ESPHOME_PROJECT_NAME
txt_count += 2; // project_name and project_version
#endif
@@ -186,6 +189,13 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
#endif
#endif
#ifdef USE_OTA_SIGNED_VERIFICATION
// Signals that an unsigned OTA image is rejected; serial flash is unaffected.
MDNS_STATIC_CONST_CHAR(TXT_OTA_SIGNED, "ota_signed");
MDNS_STATIC_CONST_CHAR(VALUE_TRUE, "1");
txt_records.push_back({MDNS_STR(TXT_OTA_SIGNED), MDNS_STR(VALUE_TRUE)});
#endif
#ifdef ESPHOME_PROJECT_NAME
MDNS_STATIC_CONST_CHAR(TXT_PROJECT_NAME, "project_name");
MDNS_STATIC_CONST_CHAR(TXT_PROJECT_VERSION, "project_version");
+36 -60
View File
@@ -152,7 +152,7 @@ CONF_ON_PAUSE = "on_pause"
CONF_ON_ANNOUNCEMENT = "on_announcement"
CONF_MEDIA_URL = "media_url"
# Command actions that all share the same schema and codegen handler
# Command actions that all share the same schema and only differ in the command sent
_COMMAND_ACTIONS = [
"play",
"pause",
@@ -201,16 +201,7 @@ _STATE_CONDITIONS = [
"muted",
]
# Special action classes with custom schemas/handlers
PlayMediaAction = media_player_ns.class_(
"PlayMediaAction", automation.Action, cg.Parented.template(MediaPlayer)
)
EnqueueMediaAction = media_player_ns.class_(
"EnqueueMediaAction", automation.Action, cg.Parented.template(MediaPlayer)
)
VolumeSetAction = media_player_ns.class_(
"VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer)
)
MediaPlayerCommand = media_player_ns.enum("MediaPlayerCommand", is_class=True)
_CALLBACK_AUTOMATIONS = (
@@ -329,59 +320,50 @@ _MEDIA_URL_ACTION_SCHEMA = cv.maybe_simple_value(
)
async def _media_action_handler(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
media_url = await cg.templatable(config[CONF_MEDIA_URL], args, cg.std_string)
announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_)
cg.add(var.set_media_url(media_url))
cg.add(var.set_announcement(announcement))
return var
_ANNOUNCEMENT_FIELD = automation.ApplyField(
CONF_ANNOUNCEMENT, "set_announcement", cg.bool_
)
_MEDIA_URL_FIELD = automation.ApplyField(CONF_MEDIA_URL, "set_media_url", cg.std_string)
automation.register_action(
def _set_command(command_name: str) -> automation.ApplyCall:
command = getattr(
MediaPlayerCommand, f"MEDIA_PLAYER_COMMAND_{command_name.upper()}"
)
return automation.ApplyCall(f"set_command({command})")
automation.register_apply_action(
"media_player.play_media",
PlayMediaAction,
_MEDIA_URL_ACTION_SCHEMA,
synchronous=True,
)(_media_action_handler)
_MEDIA_URL_FIELD,
_ANNOUNCEMENT_FIELD,
call="make_call",
)
automation.register_action(
automation.register_apply_action(
"media_player.enqueue",
EnqueueMediaAction,
_MEDIA_URL_ACTION_SCHEMA,
synchronous=True,
)(_media_action_handler)
_set_command("enqueue"),
_MEDIA_URL_FIELD,
_ANNOUNCEMENT_FIELD,
call="make_call",
)
for _action_name in _COMMAND_ACTIONS:
automation.register_apply_action(
f"media_player.{_action_name}",
MEDIA_PLAYER_ACTION_SCHEMA,
_set_command(_action_name),
_ANNOUNCEMENT_FIELD,
call="make_call",
)
def _snake_to_camel(name):
return "".join(word.capitalize() for word in name.split("_"))
def _register_command_actions():
async def handler(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_)
cg.add(var.set_announcement(announcement))
return var
for action_name in _COMMAND_ACTIONS:
class_name = f"{_snake_to_camel(action_name)}Action"
action_class = media_player_ns.class_(
class_name, automation.Action, cg.Parented.template(MediaPlayer)
)
automation.register_action(
f"media_player.{action_name}",
action_class,
MEDIA_PLAYER_ACTION_SCHEMA,
synchronous=True,
)(handler)
_register_command_actions()
def _register_state_conditions():
async def handler(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -401,9 +383,8 @@ def _register_state_conditions():
_register_state_conditions()
@automation.register_action(
automation.register_apply_action(
"media_player.volume_set",
VolumeSetAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(MediaPlayer),
@@ -411,14 +392,9 @@ _register_state_conditions()
},
key=CONF_VOLUME,
),
synchronous=True,
automation.ApplyField(CONF_VOLUME, "set_volume", cg.float_),
call="make_call",
)
async def media_player_volume_set_action(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
volume = await cg.templatable(config[CONF_VOLUME], args, cg.float_)
cg.add(var.set_volume(volume))
return var
@coroutine_with_priority(CoroPriority.CORE)
@@ -5,76 +5,6 @@
namespace esphome::media_player {
template<MediaPlayerCommand Command, typename... Ts>
class MediaPlayerCommandAction final : public Action<Ts...>, public Parented<MediaPlayer> {
public:
TEMPLATABLE_VALUE(bool, announcement);
void play(const Ts &...x) override {
this->parent_->make_call().set_command(Command).set_announcement(this->announcement_.value(x...)).perform();
}
};
template<typename... Ts>
using PlayAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_PLAY, Ts...>;
template<typename... Ts>
using PauseAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_PAUSE, Ts...>;
template<typename... Ts>
using StopAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP, Ts...>;
template<typename... Ts>
using ToggleAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_TOGGLE, Ts...>;
template<typename... Ts>
using VolumeUpAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_VOLUME_UP, Ts...>;
template<typename... Ts>
using VolumeDownAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_VOLUME_DOWN, Ts...>;
template<typename... Ts>
using TurnOnAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_TURN_ON, Ts...>;
template<typename... Ts>
using TurnOffAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_TURN_OFF, Ts...>;
template<typename... Ts>
using NextAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_NEXT, Ts...>;
template<typename... Ts>
using PreviousAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_PREVIOUS, Ts...>;
template<typename... Ts>
using MuteAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_MUTE, Ts...>;
template<typename... Ts>
using UnmuteAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_UNMUTE, Ts...>;
template<typename... Ts>
using RepeatOffAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_OFF, Ts...>;
template<typename... Ts>
using RepeatOneAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ONE, Ts...>;
template<typename... Ts>
using RepeatAllAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ALL, Ts...>;
template<typename... Ts>
using ShuffleAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_SHUFFLE, Ts...>;
template<typename... Ts>
using UnshuffleAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_UNSHUFFLE, Ts...>;
template<typename... Ts>
using GroupJoinAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_GROUP_JOIN, Ts...>;
template<typename... Ts>
using ClearPlaylistAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST, Ts...>;
template<MediaPlayerCommand Command, typename... Ts>
class MediaPlayerMediaAction final : public Action<Ts...>, public Parented<MediaPlayer> {
TEMPLATABLE_VALUE(std::string, media_url)
TEMPLATABLE_VALUE(bool, announcement)
void play(const Ts &...x) override {
auto call = this->parent_->make_call();
if constexpr (Command != MediaPlayerCommand::MEDIA_PLAYER_COMMAND_PLAY)
call.set_command(Command);
call.set_media_url(this->media_url_.value(x...)).set_announcement(this->announcement_.value(x...)).perform();
}
};
template<typename... Ts>
using PlayMediaAction = MediaPlayerMediaAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_PLAY, Ts...>;
template<typename... Ts>
using EnqueueMediaAction = MediaPlayerMediaAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_ENQUEUE, Ts...>;
template<typename... Ts> class VolumeSetAction final : public Action<Ts...>, public Parented<MediaPlayer> {
TEMPLATABLE_VALUE(float, volume)
void play(const Ts &...x) override { this->parent_->make_call().set_volume(this->volume_.value(x...)).perform(); }
};
/// Callback forwarder that triggers an Automation<> on any state change.
/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_.
struct StateAnyForwarder {
@@ -53,46 +53,6 @@ const uint8_t MITSUBISHI_BYTE04 = 0x00;
const uint8_t MITSUBISHI_BYTE13 = 0x00;
const uint8_t MITSUBISHI_BYTE16 = 0x00;
climate::ClimateTraits MitsubishiClimate::traits() {
auto traits = climate::ClimateTraits();
if (this->sensor_ != nullptr) {
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
}
traits.set_visual_min_temperature(MITSUBISHI_TEMP_MIN);
traits.set_visual_max_temperature(MITSUBISHI_TEMP_MAX);
traits.set_visual_temperature_step(1.0f);
traits.set_supported_modes({climate::CLIMATE_MODE_OFF});
if (this->supports_cool_)
traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
if (this->supports_heat_)
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
if (this->supports_cool_ && this->supports_heat_)
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
if (this->supports_dry_)
traits.add_supported_mode(climate::CLIMATE_MODE_DRY);
if (this->supports_fan_only_)
traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY);
// Default to only 3 levels in ESPHome even if most unit supports 4. The 3rd level is not used.
traits.set_supported_fan_modes(
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH});
if (this->fan_mode_ == MITSUBISHI_FAN_Q4L)
traits.add_supported_fan_mode(climate::CLIMATE_FAN_QUIET);
if (/*this->fan_mode_ == MITSUBISHI_FAN_5L ||*/ this->fan_mode_ >= MITSUBISHI_FAN_4L)
traits.add_supported_fan_mode(climate::CLIMATE_FAN_MIDDLE); // Shouldn't be used for this but it helps
traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH,
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL});
traits.set_supported_presets({climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO,
climate::CLIMATE_PRESET_BOOST, climate::CLIMATE_PRESET_SLEEP});
return traits;
}
void MitsubishiClimate::transmit_state() {
// Byte 0-4: Constant: 0x23, 0xCB, 0x26, 0x01, 0x00
// Byte 5: On=0x20, Off: 0x00
+16 -9
View File
@@ -11,7 +11,7 @@ const uint8_t MITSUBISHI_TEMP_MIN = 16; // Celsius
const uint8_t MITSUBISHI_TEMP_MAX = 31; // Celsius
// Fan mode
enum SetFanMode {
enum SetFanMode : uint8_t {
MITSUBISHI_FAN_3L = 0, // 3 levels + auto
MITSUBISHI_FAN_4L, // 4 levels + auto
MITSUBISHI_FAN_Q4L, // Quiet + 4 levels + auto
@@ -19,7 +19,7 @@ enum SetFanMode {
};
// Enum to represent horizontal directios
enum HorizontalDirection {
enum HorizontalDirection : uint8_t {
HORIZONTAL_DIRECTION_LEFT = 0x10,
HORIZONTAL_DIRECTION_MIDDLE_LEFT = 0x20,
HORIZONTAL_DIRECTION_MIDDLE = 0x30,
@@ -29,7 +29,7 @@ enum HorizontalDirection {
};
// Enum to represent vertical directions
enum VerticalDirection {
enum VerticalDirection : uint8_t {
VERTICAL_DIRECTION_AUTO = 0x00,
VERTICAL_DIRECTION_UP = 0x08,
VERTICAL_DIRECTION_MIDDLE_UP = 0x10,
@@ -38,12 +38,14 @@ enum VerticalDirection {
VERTICAL_DIRECTION_DOWN = 0x28,
};
// Fan modes offered by every set_fan_mode option.
static constexpr climate::ClimateFanModeMask MITSUBISHI_BASE_FAN_MODES{
climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH};
class MitsubishiClimate final : public climate_ir::ClimateIR {
public:
MitsubishiClimate()
: climate_ir::ClimateIR(MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MIDDLE,
climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_QUIET},
: climate_ir::ClimateIR(MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX, 1.0f, true, true, MITSUBISHI_BASE_FAN_MODES,
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL,
climate::CLIMATE_SWING_HORIZONTAL},
{climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST,
@@ -54,7 +56,14 @@ class MitsubishiClimate final : public climate_ir::ClimateIR {
void set_supports_fan_only(bool supports_fan_only) { this->supports_fan_only_ = supports_fan_only; }
void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
void set_fan_mode(SetFanMode fan_mode) { this->fan_mode_ = fan_mode; }
void set_fan_mode(SetFanMode fan_mode) {
this->fan_mode_ = fan_mode;
this->fan_modes_ = MITSUBISHI_BASE_FAN_MODES;
if (fan_mode == MITSUBISHI_FAN_Q4L)
this->fan_modes_.insert(climate::CLIMATE_FAN_QUIET);
if (fan_mode >= MITSUBISHI_FAN_4L)
this->fan_modes_.insert(climate::CLIMATE_FAN_MIDDLE); // Shouldn't be used for this but it helps
}
void set_horizontal_default(HorizontalDirection horizontal_direction) {
this->default_horizontal_direction_ = horizontal_direction;
@@ -74,8 +83,6 @@ class MitsubishiClimate final : public climate_ir::ClimateIR {
HorizontalDirection default_horizontal_direction_;
VerticalDirection default_vertical_direction_;
climate::ClimateTraits traits() override;
};
} // namespace esphome::mitsubishi
@@ -10,8 +10,8 @@ from esphome.const import (
CONF_UPDATE_INTERVAL,
CONF_USE_FAHRENHEIT,
)
from esphome.core import ID, Lambda
from esphome.cpp_generator import LambdaExpression, MockObj
from esphome.core import ID
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType, TemplateArgsType
CODEOWNERS = ["@crnjan"]
@@ -32,7 +32,6 @@ MitsubishiCN105Component = mitsubishi_ns.class_(
)
VaneState = mitsubishi_ns.struct("VaneState")
VaneCall = mitsubishi_ns.class_("VaneCall")
VerticalVaneMode = mitsubishi_ns.enum("VerticalVaneMode")
# The insertion order must match VALUES in
@@ -59,10 +58,6 @@ ClearRemoteTemperatureAction = mitsubishi_ns.class_(
cg.Parented.template(MitsubishiCN105Component),
)
VaneControlAction = mitsubishi_ns.class_(
"VaneControlAction",
automation.Action,
)
CONFIG_SCHEMA = (
cv.Schema(
@@ -117,8 +112,9 @@ async def to_code(config: ConfigType) -> None:
)
)
cg.add(var.set_use_fahrenheit(config[CONF_USE_FAHRENHEIT]))
# User lambdas may use unqualified enum names.
cg.add_global(mitsubishi_ns.using)
if on_state := config.get(CONF_VANE, {}).get(CONF_ON_STATE):
cg.add_global(mitsubishi_ns.using)
for conf in on_state:
await automation.build_callback_automation(
var,
@@ -183,14 +179,6 @@ async def clear_temperature_action_to_code(
return var
VANE_CONTROL_FIELDS = (
(
(CONF_VERTICAL, CONF_DIRECTION),
"vertical.set_direction",
VerticalVaneMode,
),
)
VANE_CONTROL_ACTION_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(MitsubishiCN105Component),
@@ -205,46 +193,11 @@ VANE_CONTROL_ACTION_SCHEMA = cv.Schema(
)
@automation.register_action(
automation.register_apply_action(
f"{DOMAIN}.vane.control",
VaneControlAction,
VANE_CONTROL_ACTION_SCHEMA,
synchronous=True,
automation.ApplyField(
(CONF_VERTICAL, CONF_DIRECTION), "vertical.set_direction", VerticalVaneMode
),
call="make_vane_call",
)
async def vane_control_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
cg.add_global(mitsubishi_ns.using)
parent = await cg.get_variable(config[CONF_ID])
normalized_args = [
(cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), name)
for t, name in args
]
forwarded_args = ", ".join(name for _, name in args)
body_lines: list[str] = []
for path, setter, type_ in VANE_CONTROL_FIELDS:
if (section := config.get(path[0])) is None:
continue
if (value := section.get(path[1])) is None:
continue
if isinstance(value, Lambda):
inner = await cg.process_lambda(
value,
normalized_args,
return_type=type_,
)
body_lines.append(f"call.{setter}(({inner})({forwarded_args}));")
else:
body_lines.append(f"call.{setter}({cg.safe_exp(value)});")
apply_lambda = LambdaExpression(
["\n".join(body_lines)],
[(VaneCall.operator("ref"), "call"), *normalized_args],
capture="",
return_type=cg.void,
)
return cg.new_Pvariable(action_id, template_arg, parent, apply_lambda)
@@ -4,8 +4,6 @@
#include "esphome/core/automation.h"
#include <type_traits>
namespace esphome::mitsubishi_cn105 {
template<typename... Ts>
@@ -22,21 +20,4 @@ class ClearRemoteTemperatureAction final : public Action<Ts...>, public Parented
void play(const Ts &...x) override { this->parent_->clear_remote_temperature(); }
};
template<typename... Ts> class VaneControlAction final : public Action<Ts...> {
public:
using ApplyFn = void (*)(VaneCall &, const std::remove_cvref_t<Ts> &...);
VaneControlAction(MitsubishiCN105Component *parent, ApplyFn apply) : parent_(parent), apply_(apply) {}
void play(const Ts &...x) override {
auto call = this->parent_->make_vane_call();
this->apply_(call, x...);
call.perform();
}
protected:
MitsubishiCN105Component *parent_;
ApplyFn apply_;
};
} // namespace esphome::mitsubishi_cn105
+12
View File
@@ -203,6 +203,12 @@ void ModbusClientHub::parse_modbus_frames() {
void ModbusServerHub::parse_modbus_frames() {
while (!this->rx_buffer_.empty()) {
if (this->deferred_payload_len_ != 0) {
// Another frame arrived before the deferred reply went out, so the client has moved on.
this->cancel_timeout("deferred_send");
ESP_LOGD(TAG, "Dropped deferred reply to %" PRIu8 ": a new frame arrived first", this->deferred_payload_[0]);
this->deferred_payload_len_ = 0;
}
size_t size = this->rx_buffer_.size();
ESP_LOGVV(TAG, "Parsing frames buffer size = %" PRIu32, size);
bool retry_as_client = false;
@@ -1213,6 +1219,7 @@ void ModbusServerHub::send_raw_(const uint8_t *payload, uint16_t len) {
this->set_timeout("deferred_send", (this->tx_delay_remaining() + US_PER_MS - 1) / US_PER_MS, [this]() {
ModbusFrame frame(this->deferred_payload_[0], this->deferred_payload_.data() + 1,
this->deferred_payload_len_ - 1);
this->deferred_payload_len_ = 0;
if (!this->send_frame_(frame)) {
ESP_LOGE(TAG, "Deferred server reply dropped: transmission still blocked");
}
@@ -1234,6 +1241,11 @@ void Modbus::clear_rx_buffer_(const LogString *reason, bool warn, size_t bytes_t
if (warn) {
ESP_LOGW(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "us after last send", bytes, LOG_STR_ARG(reason),
micros() - this->last_send_);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
ESP_LOGV(TAG, " discarded: %s%s", format_hex_pretty_to(hex_buf, this->rx_buffer_.data(), bytes),
bytes > MODBUS_MAX_LOG_BYTES ? LOG_STR_LITERAL(" ...") : LOG_STR_LITERAL(""));
#endif
} else {
ESP_LOGV(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "us after last send", bytes, LOG_STR_ARG(reason),
micros() - this->last_send_);
-9
View File
@@ -147,15 +147,6 @@ struct IPAddress {
}
return (ntohl(this->ip_addr_.u_addr.ip4.s_addr) & 0xF0000000UL) == 0xE0000000UL;
}
// Remove before 2026.8.0
ESPDEPRECATED(
"str() is deprecated: use 'char buf[IP_ADDRESS_BUFFER_SIZE]; ip.str_to(buf);' instead. Removed in 2026.8.0",
"2026.2.0")
std::string str() const {
char buf[IP_ADDRESS_BUFFER_SIZE];
this->str_to(buf);
return buf;
}
char *str_to(char *buf) const {
if (this->ip_addr_.type == IPADDR_TYPE_V6) {
inet_ntop(AF_INET6, &this->ip_addr_.u_addr.ip6, buf, IP_ADDRESS_BUFFER_SIZE);
-96
View File
@@ -1,96 +0,0 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/string_ref.h"
#include "nextion.h"
namespace esphome::nextion {
template<typename... Ts> class NextionSetBrightnessAction final : public Action<Ts...> {
public:
explicit NextionSetBrightnessAction(Nextion *component) : component_(component) {}
TEMPLATABLE_VALUE(float, brightness)
void play(const Ts &...x) override {
this->component_->set_brightness(this->brightness_.value(x...));
this->component_->set_backlight_brightness(this->brightness_.value(x...));
}
void set_brightness(std::function<void(Ts..., float)> brightness) { this->brightness_ = brightness; }
protected:
Nextion *component_;
};
template<typename... Ts> class NextionPublishFloatAction final : public Action<Ts...> {
public:
explicit NextionPublishFloatAction(NextionComponent *component) : component_(component) {}
TEMPLATABLE_VALUE(float, state)
TEMPLATABLE_VALUE(bool, publish_state)
TEMPLATABLE_VALUE(bool, send_to_nextion)
void play(const Ts &...x) override {
this->component_->set_state(this->state_.value(x...), this->publish_state_.value(x...),
this->send_to_nextion_.value(x...));
}
void set_state(std::function<void(Ts..., float)> state) { this->state_ = state; }
void set_publish_state(std::function<void(Ts..., bool)> publish_state) { this->publish_state_ = publish_state; }
void set_send_to_nextion(std::function<void(Ts..., bool)> send_to_nextion) {
this->send_to_nextion_ = send_to_nextion;
}
protected:
NextionComponent *component_;
};
template<typename... Ts> class NextionPublishTextAction final : public Action<Ts...> {
public:
explicit NextionPublishTextAction(NextionComponent *component) : component_(component) {}
TEMPLATABLE_VALUE(const char *, state)
TEMPLATABLE_VALUE(bool, publish_state)
TEMPLATABLE_VALUE(bool, send_to_nextion)
void play(const Ts &...x) override {
this->component_->set_state(this->state_.value(x...), this->publish_state_.value(x...),
this->send_to_nextion_.value(x...));
}
void set_state(std::function<void(Ts..., const char *)> state) { this->state_ = state; }
void set_publish_state(std::function<void(Ts..., bool)> publish_state) { this->publish_state_ = publish_state; }
void set_send_to_nextion(std::function<void(Ts..., bool)> send_to_nextion) {
this->send_to_nextion_ = send_to_nextion;
}
protected:
NextionComponent *component_;
};
template<typename... Ts> class NextionPublishBoolAction final : public Action<Ts...> {
public:
explicit NextionPublishBoolAction(NextionComponent *component) : component_(component) {}
TEMPLATABLE_VALUE(bool, state)
TEMPLATABLE_VALUE(bool, publish_state)
TEMPLATABLE_VALUE(bool, send_to_nextion)
void play(const Ts &...x) override {
this->component_->set_state(this->state_.value(x...), this->publish_state_.value(x...),
this->send_to_nextion_.value(x...));
}
void set_state(std::function<void(Ts..., bool)> state) { this->state_ = state; }
void set_publish_state(std::function<void(Ts..., bool)> publish_state) { this->publish_state_ = publish_state; }
void set_send_to_nextion(std::function<void(Ts..., bool)> send_to_nextion) {
this->send_to_nextion_ = send_to_nextion;
}
protected:
NextionComponent *component_;
};
} // namespace esphome::nextion
@@ -18,10 +18,6 @@ NextionBinarySensor = nextion_ns.class_(
"NextionBinarySensor", binary_sensor.BinarySensor, cg.PollingComponent
)
NextionPublishBoolAction = nextion_ns.class_(
"NextionPublishBoolAction", automation.Action
)
CONFIG_SCHEMA = cv.All(
binary_sensor.binary_sensor_schema(NextionBinarySensor)
.extend(
@@ -57,9 +53,8 @@ async def to_code(config):
cg.add(hub.register_binarysensor_component(var))
@automation.register_action(
automation.register_apply_action(
"binary_sensor.nextion.publish",
NextionPublishBoolAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(NextionBinarySensor),
@@ -70,19 +65,12 @@ async def to_code(config):
),
}
),
synchronous=True,
automation.ApplyCall(
"set_state({}, {}, {})",
(
(CONF_STATE, cg.bool_),
(CONF_PUBLISH_STATE, cg.bool_),
(CONF_SEND_TO_NEXTION, cg.bool_),
),
),
)
async def sensor_nextion_publish_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_)
cg.add(var.set_state(template_))
template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, cg.bool_)
cg.add(var.set_publish_state(template_))
template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, cg.bool_)
cg.add(var.set_send_to_nextion(template_))
return var
+3 -16
View File
@@ -56,11 +56,6 @@ def AUTO_LOAD() -> list[str]:
return base
NextionSetBrightnessAction = nextion_ns.class_(
"NextionSetBrightnessAction", automation.Action
)
def _deprecated_dump_device_info(value):
_LOGGER.warning(
"'dump_device_info' is deprecated and will be removed in ESPHome 2026.11.0. "
@@ -160,9 +155,8 @@ CONFIG_SCHEMA = cv.All(
)
@automation.register_action(
automation.register_apply_action(
"display.nextion.set_brightness",
NextionSetBrightnessAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(Nextion),
@@ -170,16 +164,9 @@ CONFIG_SCHEMA = cv.All(
},
key=CONF_BRIGHTNESS,
),
synchronous=True,
automation.ApplyField(CONF_BRIGHTNESS, "set_brightness", cg.float_),
automation.ApplyField(CONF_BRIGHTNESS, "set_backlight_brightness", cg.float_),
)
async def nextion_set_brightness_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, cg.float_)
cg.add(var.set_brightness(template_))
return var
_CALLBACK_AUTOMATIONS = (
+9 -21
View File
@@ -21,10 +21,6 @@ CODEOWNERS = ["@senexcrenshaw"]
NextionSensor = nextion_ns.class_("NextionSensor", sensor.Sensor, cg.PollingComponent)
NextionPublishFloatAction = nextion_ns.class_(
"NextionPublishFloatAction", automation.Action
)
def CheckWaveID(value):
value = cv.int_(value)
@@ -97,9 +93,8 @@ async def to_code(config):
cg.add(var.set_wave_max_length(config[CONF_WAVE_MAX_LENGTH]))
@automation.register_action(
automation.register_apply_action(
"sensor.nextion.publish",
NextionPublishFloatAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(NextionSensor),
@@ -110,19 +105,12 @@ async def to_code(config):
),
}
),
synchronous=True,
automation.ApplyCall(
"set_state({}, {}, {})",
(
(CONF_STATE, cg.float_),
(CONF_PUBLISH_STATE, cg.bool_),
(CONF_SEND_TO_NEXTION, cg.bool_),
),
),
)
async def sensor_nextion_publish_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_STATE], args, cg.float_)
cg.add(var.set_state(template_))
template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, cg.bool_)
cg.add(var.set_publish_state(template_))
template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, cg.bool_)
cg.add(var.set_send_to_nextion(template_))
return var
+9 -21
View File
@@ -16,10 +16,6 @@ CODEOWNERS = ["@senexcrenshaw"]
NextionSwitch = nextion_ns.class_("NextionSwitch", switch.Switch, cg.PollingComponent)
NextionPublishBoolAction = nextion_ns.class_(
"NextionPublishBoolAction", automation.Action
)
CONFIG_SCHEMA = cv.All(
switch.switch_schema(NextionSwitch)
.extend(CONFIG_SWITCH_COMPONENT_SCHEMA)
@@ -39,9 +35,8 @@ async def to_code(config):
await setup_component_core_(var, config, ".val")
@automation.register_action(
automation.register_apply_action(
"switch.nextion.publish",
NextionPublishBoolAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(NextionSwitch),
@@ -52,19 +47,12 @@ async def to_code(config):
),
}
),
synchronous=True,
automation.ApplyCall(
"set_state({}, {}, {})",
(
(CONF_STATE, cg.bool_),
(CONF_PUBLISH_STATE, cg.bool_),
(CONF_SEND_TO_NEXTION, cg.bool_),
),
),
)
async def sensor_nextion_publish_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_)
cg.add(var.set_state(template_))
template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, cg.bool_)
cg.add(var.set_publish_state(template_))
template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, cg.bool_)
cg.add(var.set_send_to_nextion(template_))
return var
@@ -13,10 +13,6 @@ NextionTextSensor = nextion_ns.class_(
"NextionTextSensor", text_sensor.TextSensor, cg.PollingComponent
)
NextionPublishTextAction = nextion_ns.class_(
"NextionPublishTextAction", automation.Action
)
CONFIG_SCHEMA = (
text_sensor.text_sensor_schema(NextionTextSensor)
.extend(CONFIG_TEXT_COMPONENT_SCHEMA)
@@ -35,9 +31,8 @@ async def to_code(config):
await setup_component_core_(var, config, ".txt")
@automation.register_action(
automation.register_apply_action(
"text_sensor.nextion.publish",
NextionPublishTextAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(NextionTextSensor),
@@ -48,19 +43,12 @@ async def to_code(config):
),
}
),
synchronous=True,
automation.ApplyCall(
"set_state({}, {}, {})",
(
(CONF_STATE, cg.std_string),
(CONF_PUBLISH_STATE, cg.bool_),
(CONF_SEND_TO_NEXTION, cg.bool_),
),
),
)
async def sensor_nextion_publish_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_STATE], args, cg.const_char_ptr)
cg.add(var.set_state(template_))
template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, cg.bool_)
cg.add(var.set_publish_state(template_))
template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, cg.bool_)
cg.add(var.set_send_to_nextion(template_))
return var
+4 -16
View File
@@ -3,6 +3,7 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <algorithm>
#include <cstring>
@@ -10,10 +11,6 @@
#include <noise/protocol.h>
#include <sodium.h>
#ifdef USE_ESP8266
#include <pgmspace.h>
#endif
namespace esphome::noise {
static const char *const TAG = "noise";
@@ -109,22 +106,13 @@ size_t format_reject_payload(uint8_t *buf, size_t capacity, const LogString *rea
return 0;
}
buf[0] = HANDSHAKE_STATUS_REJECT;
#ifdef USE_STORE_LOG_STR_IN_FLASH
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
reason_len = std::min(reason_len, capacity - 1);
if (reason_len > 0) {
memcpy_P(buf + 1, reinterpret_cast<PGM_P>(reason), reason_len);
}
#else
// The reason may live in PROGMEM on ESP8266; the progmem helpers read RAM and flash alike
const char *reason_str = LOG_STR_ARG(reason);
size_t reason_len = strlen(reason_str);
reason_len = std::min(reason_len, capacity - 1);
size_t reason_len = std::min(ESPHOME_strlen_P(reason_str), capacity - 1);
if (reason_len > 0) {
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string
std::memcpy(buf + 1, reason_str, reason_len);
progmem_memcpy(buf + 1, reason_str, reason_len);
}
#endif
return reason_len + 1;
}
+8
View File
@@ -428,6 +428,10 @@ async def to_code(config: ConfigType) -> None:
)
zephyr_add_prj_conf("REBOOT", True)
# some boards enable USB by default.
# disable it to prevent extra current consumption.
zephyr_add_prj_conf("USB_DEVICE_STACK", False, False)
@coroutine_with_priority(CoroPriority.DIAGNOSTICS)
async def _dfu_to_code(dfu_config):
@@ -436,6 +440,10 @@ async def _dfu_to_code(dfu_config):
if CONF_RESET_PIN in dfu_config:
pin = await cg.gpio_pin_expression(dfu_config[CONF_RESET_PIN])
cg.add(var.set_reset_pin(pin))
# DFU uses cdc rate callback to enter bootloader which was disabled explicitly to save power.
zephyr_add_prj_conf("USB_DEVICE_STACK", True)
zephyr_add_prj_conf("USB_CDC_ACM", True)
zephyr_add_prj_conf("CDC_ACM_DTE_RATE_CALLBACK_SUPPORT", True)
await cg.register_component(var, dfu_config)
+27 -92
View File
@@ -163,10 +163,6 @@ ValueRangeTrigger = number_ns.class_(
"ValueRangeTrigger", automation.Trigger.template(cg.float_), cg.Component
)
# Actions
NumberSetAction = number_ns.class_("NumberSetAction", automation.Action)
NumberOperationAction = number_ns.class_("NumberOperationAction", automation.Action)
# Conditions
NumberInRangeCondition = number_ns.class_(
"NumberInRangeCondition", automation.Condition
@@ -366,85 +362,40 @@ OPERATION_BASE_SCHEMA = cv.Schema(
)
@automation.register_action(
automation.register_apply_action(
"number.set",
NumberSetAction,
OPERATION_BASE_SCHEMA.extend(
{
cv.Required(CONF_VALUE): cv.templatable(cv.float_),
}
),
synchronous=True,
automation.ApplyField(CONF_VALUE, "set_value", cg.float_),
call="make_call",
)
async def number_set_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_VALUE], args, cg.float_)
cg.add(var.set_value(template_))
return var
# The operation is fixed by the action name; CONF_MODE only stays accepted in the config.
for _name, _mode, _cycle in (
("number.increment", "INCREMENT", True),
("number.decrement", "DECREMENT", True),
("number.to_min", "TO_MIN", False),
("number.to_max", "TO_MAX", False),
):
_schema = {cv.Optional(CONF_MODE, default=_mode): cv.one_of(_mode, upper=True)}
_fields = [
automation.ApplyCall(f"with_operation({NUMBER_OPERATION_OPTIONS[_mode]})")
]
if _cycle:
_schema[cv.Optional(CONF_CYCLE, default=True)] = cv.boolean
_fields.append(automation.ApplyField(CONF_CYCLE, "with_cycle", cg.bool_))
automation.register_apply_action(
_name,
automation.maybe_simple_id(OPERATION_BASE_SCHEMA.extend(_schema)),
*_fields,
call="make_call",
)
@automation.register_action(
"number.increment",
NumberOperationAction,
automation.maybe_simple_id(
OPERATION_BASE_SCHEMA.extend(
{
cv.Optional(CONF_MODE, default="INCREMENT"): cv.one_of(
"INCREMENT", upper=True
),
cv.Optional(CONF_CYCLE, default=True): cv.boolean,
}
)
),
synchronous=True,
)
@automation.register_action(
"number.decrement",
NumberOperationAction,
automation.maybe_simple_id(
OPERATION_BASE_SCHEMA.extend(
{
cv.Optional(CONF_MODE, default="DECREMENT"): cv.one_of(
"DECREMENT", upper=True
),
cv.Optional(CONF_CYCLE, default=True): cv.boolean,
}
)
),
synchronous=True,
)
@automation.register_action(
"number.to_min",
NumberOperationAction,
automation.maybe_simple_id(
OPERATION_BASE_SCHEMA.extend(
{
cv.Optional(CONF_MODE, default="TO_MIN"): cv.one_of(
"TO_MIN", upper=True
),
}
)
),
synchronous=True,
)
@automation.register_action(
"number.to_max",
NumberOperationAction,
automation.maybe_simple_id(
OPERATION_BASE_SCHEMA.extend(
{
cv.Optional(CONF_MODE, default="TO_MAX"): cv.one_of(
"TO_MAX", upper=True
),
}
)
),
synchronous=True,
)
@automation.register_action(
automation.register_apply_action(
"number.operation",
NumberOperationAction,
OPERATION_BASE_SCHEMA.extend(
{
cv.Required(CONF_OPERATION): cv.templatable(
@@ -453,23 +404,7 @@ async def number_set_to_code(config, action_id, template_arg, args):
cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean),
}
),
synchronous=True,
automation.ApplyField(CONF_OPERATION, "with_operation", NumberOperation),
automation.ApplyField(CONF_CYCLE, "with_cycle", cg.bool_),
call="make_call",
)
async def number_to_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if (operation := config.get(CONF_OPERATION)) is not None:
to_ = await cg.templatable(operation, args, NumberOperation)
cg.add(var.set_operation(to_))
if (cycle := config.get(CONF_CYCLE)) is not None:
template_ = await cg.templatable(cycle, args, cg.bool_)
cg.add(var.set_cycle(template_))
if (mode := config.get(CONF_MODE)) is not None:
template_ = await cg.templatable(
NUMBER_OPERATION_OPTIONS[mode], args, NumberOperation
)
cg.add(var.set_operation(template_))
if (cycle := config.get(CONF_CYCLE)) is not None:
template_ = await cg.templatable(cycle, args, cg.bool_)
cg.add(var.set_cycle(template_))
return var
-34
View File
@@ -13,40 +13,6 @@ class NumberStateTrigger final : public Trigger<float> {
}
};
template<typename... Ts> class NumberSetAction final : public Action<Ts...> {
public:
NumberSetAction(Number *number) : number_(number) {}
TEMPLATABLE_VALUE(float, value)
void play(const Ts &...x) override {
auto call = this->number_->make_call();
call.set_value(this->value_.value(x...));
call.perform();
}
protected:
Number *number_;
};
template<typename... Ts> class NumberOperationAction final : public Action<Ts...> {
public:
explicit NumberOperationAction(Number *number) : number_(number) {}
TEMPLATABLE_VALUE(NumberOperation, operation)
TEMPLATABLE_VALUE(bool, cycle)
void play(const Ts &...x) override {
auto call = this->number_->make_call();
call.with_operation(this->operation_.value(x...));
if (this->cycle_.has_value()) {
call.with_cycle(this->cycle_.value(x...));
}
call.perform();
}
protected:
Number *number_;
};
class ValueRangeTrigger final : public Trigger<float>, public Component {
public:
explicit ValueRangeTrigger(Number *parent) : parent_(parent) {}
+45 -68
View File
@@ -1,3 +1,5 @@
from dataclasses import dataclass
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
@@ -12,12 +14,26 @@ from esphome.const import (
CONF_POWER_SUPPLY,
)
from esphome.core import CORE
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
IS_PLATFORM_COMPONENT = True
DOMAIN = "output"
CONF_ZERO_MEANS_ZERO = "zero_means_zero"
@dataclass
class OutputData:
power_scaling: bool = False
def _get_data() -> OutputData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = OutputData()
return CORE.data[DOMAIN]
BINARY_OUTPUT_SCHEMA = cv.Schema(
{
cv.Optional(CONF_POWER_SUPPLY): cv.use_id(power_supply.PowerSupply),
@@ -39,13 +55,6 @@ BinaryOutputPtr = BinaryOutput.operator("ptr")
FloatOutput = output_ns.class_("FloatOutput", BinaryOutput)
FloatOutputPtr = FloatOutput.operator("ptr")
# Actions
TurnOffAction = output_ns.class_("TurnOffAction", automation.Action)
TurnOnAction = output_ns.class_("TurnOnAction", automation.Action)
SetLevelAction = output_ns.class_("SetLevelAction", automation.Action)
SetMinPowerAction = output_ns.class_("SetMinPowerAction", automation.Action)
SetMaxPowerAction = output_ns.class_("SetMaxPowerAction", automation.Action)
async def setup_output_platform_(obj, config):
if CONF_INVERTED in config:
@@ -85,81 +94,49 @@ BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id(
)
@automation.register_action(
"output.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA, synchronous=True
)
async def output_turn_on_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
def _enable_power_scaling(config: ConfigType) -> ConfigType:
# set_min_power/set_max_power only exist with the define; to_code emits it from this fact.
_get_data().power_scaling = True
return config
@automation.register_action(
"output.turn_off", TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA, synchronous=True
)
async def output_turn_off_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
for _name, _call in (
("output.turn_on", "turn_on()"),
("output.turn_off", "turn_off()"),
):
automation.register_apply_action(
_name, BINARY_OUTPUT_ACTION_SCHEMA, automation.ApplyCall(_call)
)
@automation.register_action(
automation.register_apply_action(
"output.set_level",
SetLevelAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(FloatOutput),
cv.Required(CONF_LEVEL): cv.templatable(cv.percentage),
}
),
synchronous=True,
automation.ApplyField(CONF_LEVEL, "set_level", cg.float_),
)
async def output_set_level_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_LEVEL], args, cg.float_)
cg.add(var.set_level(template_))
return var
@automation.register_action(
"output.set_min_power",
SetMinPowerAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(FloatOutput),
cv.Required(CONF_MIN_POWER): cv.templatable(cv.percentage),
}
),
synchronous=True,
)
async def output_set_min_power_to_code(config, action_id, template_arg, args):
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_MIN_POWER], args, cg.float_)
cg.add(var.set_min_power(template_))
return var
@automation.register_action(
"output.set_max_power",
SetMaxPowerAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(FloatOutput),
cv.Required(CONF_MAX_POWER): cv.templatable(cv.percentage),
}
),
synchronous=True,
)
async def output_set_max_power_to_code(config, action_id, template_arg, args):
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_MAX_POWER], args, cg.float_)
cg.add(var.set_max_power(template_))
return var
for _name, _key, _target in (
("output.set_min_power", CONF_MIN_POWER, "set_min_power"),
("output.set_max_power", CONF_MAX_POWER, "set_max_power"),
):
automation.register_apply_action(
_name,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(FloatOutput),
cv.Required(_key): cv.templatable(cv.percentage),
}
).add_extra(_enable_power_scaling),
automation.ApplyField(_key, _target, cg.float_),
)
async def to_code(config):
cg.add_define("USE_OUTPUT")
if _get_data().power_scaling:
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
cg.add_global(output_ns.using)
-69
View File
@@ -1,69 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/defines.h"
#include "esphome/components/output/binary_output.h"
#include "esphome/components/output/float_output.h"
namespace esphome::output {
template<typename... Ts> class TurnOffAction final : public Action<Ts...> {
public:
TurnOffAction(BinaryOutput *output) : output_(output) {}
void play(const Ts &...x) override { this->output_->turn_off(); }
protected:
BinaryOutput *output_;
};
template<typename... Ts> class TurnOnAction final : public Action<Ts...> {
public:
TurnOnAction(BinaryOutput *output) : output_(output) {}
void play(const Ts &...x) override { this->output_->turn_on(); }
protected:
BinaryOutput *output_;
};
template<typename... Ts> class SetLevelAction final : public Action<Ts...> {
public:
SetLevelAction(FloatOutput *output) : output_(output) {}
TEMPLATABLE_VALUE(float, level)
void play(const Ts &...x) override { this->output_->set_level(this->level_.value(x...)); }
protected:
FloatOutput *output_;
};
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
template<typename... Ts> class SetMinPowerAction final : public Action<Ts...> {
public:
SetMinPowerAction(FloatOutput *output) : output_(output) {}
TEMPLATABLE_VALUE(float, min_power)
void play(const Ts &...x) override { this->output_->set_min_power(this->min_power_.value(x...)); }
protected:
FloatOutput *output_;
};
template<typename... Ts> class SetMaxPowerAction final : public Action<Ts...> {
public:
SetMaxPowerAction(FloatOutput *output) : output_(output) {}
TEMPLATABLE_VALUE(float, max_power)
void play(const Ts &...x) override { this->output_->set_max_power(this->max_power_.value(x...)); }
protected:
FloatOutput *output_;
};
#endif // USE_OUTPUT_FLOAT_POWER_SCALING
} // namespace esphome::output

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