mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 18:22:48 +00:00
Compare commits
146
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18fcd02712 | ||
|
|
4a2f17bc8b | ||
|
|
253793c4df | ||
|
|
3ef0acd4b2 | ||
|
|
73ca04579c | ||
|
|
4ccc5fa37e | ||
|
|
f70880ce96 | ||
|
|
057bf88e4a | ||
|
|
16757a84e6 | ||
|
|
a9111a8993 | ||
|
|
211fd3ce54 | ||
|
|
9df7e825d3 | ||
|
|
e63428ddce | ||
|
|
f8e8295088 | ||
|
|
9f5ac44db1 | ||
|
|
6f71b9102b | ||
|
|
04da1827e3 | ||
|
|
d2fefe2299 | ||
|
|
f1f83cda07 | ||
|
|
7dc98c683b | ||
|
|
071fc08e32 | ||
|
|
c27cbb132d | ||
|
|
7e308bb83d | ||
|
|
68235f34c5 | ||
|
|
b59518f6ab | ||
|
|
da148bedfe | ||
|
|
b0c9a7d5bd | ||
|
|
22177680c4 | ||
|
|
e92b1e27ca | ||
|
|
fed5dd6a9d | ||
|
|
cbd985826c | ||
|
|
2ce2b7b25c | ||
|
|
acc9324fa8 | ||
|
|
cbb4c49011 | ||
|
|
afc5c29569 | ||
|
|
993c96875c | ||
|
|
75360a5d52 | ||
|
|
8b11d3da39 | ||
|
|
5790181f44 | ||
|
|
4780936867 | ||
|
|
22959ea1cf | ||
|
|
e9aecb5fe8 | ||
|
|
6768cb818b | ||
|
|
1861d8282f | ||
|
|
c39ffd7099 | ||
|
|
1671bb6dd3 | ||
|
|
0ceefd5f91 | ||
|
|
4477e35d66 | ||
|
|
e98c4bd375 | ||
|
|
eae8ff7f91 | ||
|
|
f24e1f31f2 | ||
|
|
4cbced407d | ||
|
|
64127632f5 | ||
|
|
cd5ff787f4 | ||
|
|
698eb71862 | ||
|
|
e1126a152e | ||
|
|
807556171b | ||
|
|
f4300d9cee | ||
|
|
e3951f2ab4 | ||
|
|
438307f548 | ||
|
|
b7b1507daf | ||
|
|
1be1f67516 | ||
|
|
21dc6af22e | ||
|
|
20703f32bb | ||
|
|
2ddc6a0741 | ||
|
|
c1d82168d8 | ||
|
|
281c54bcb8 | ||
|
|
a47271b1f1 | ||
|
|
22efbd9778 | ||
|
|
6a97172867 | ||
|
|
c275365258 | ||
|
|
4efeee245c | ||
|
|
bd91d3ff23 | ||
|
|
2a26d5ab62 | ||
|
|
f58fe2e3ea | ||
|
|
07c8cfdde3 | ||
|
|
802f2b9f24 | ||
|
|
7df946b665 | ||
|
|
aaed14d018 | ||
|
|
62b501049f | ||
|
|
606525ed18 | ||
|
|
bd1efeae94 | ||
|
|
0110215e31 | ||
|
|
170bb02fa3 | ||
|
|
4f6af3cfab | ||
|
|
8f14adaa0c | ||
|
|
761dd9dd9c | ||
|
|
2e809c3626 | ||
|
|
bbd8472b66 | ||
|
|
01f9416a79 | ||
|
|
4e2d1cd7fc | ||
|
|
5e8f620362 | ||
|
|
9d87fcd3b7 | ||
|
|
fa7ccc934e | ||
|
|
4a7fe83a17 | ||
|
|
c33ccc7043 | ||
|
|
4f95ffaa1f | ||
|
|
487ef61d0a | ||
|
|
ce6a66959a | ||
|
|
70f8ec73c9 | ||
|
|
d78d640abd | ||
|
|
cf3552b46e | ||
|
|
f6e97b7ada | ||
|
|
35607e99a7 | ||
|
|
b36c6bd7ca | ||
|
|
9c5479287e | ||
|
|
93df1e1259 | ||
|
|
26abc1325e | ||
|
|
3da7533159 | ||
|
|
1a8a661b56 | ||
|
|
3b570d84ae | ||
|
|
547624edf9 | ||
|
|
14baf03aad | ||
|
|
418ae73870 | ||
|
|
e7ff903284 | ||
|
|
56f7280d55 | ||
|
|
01fc7511ae | ||
|
|
8c3ebd3414 | ||
|
|
ca46954335 | ||
|
|
a4bebc3083 | ||
|
|
c609db16b3 | ||
|
|
00fa63ea19 | ||
|
|
e8d3c06b60 | ||
|
|
359eba7145 | ||
|
|
e76edeb19e | ||
|
|
79532f2a85 | ||
|
|
2b805b897b | ||
|
|
44cf2b594a | ||
|
|
f4aaa595f8 | ||
|
|
a78e955691 | ||
|
|
632f63b34b | ||
|
|
cd50f949ae | ||
|
|
352121d60f | ||
|
|
3b4a6f21f9 | ||
|
|
cd012b57ed | ||
|
|
8f3f25b832 | ||
|
|
81dd57b402 | ||
|
|
5764a72f1b | ||
|
|
7a27d5dde1 | ||
|
|
a00d241cad | ||
|
|
c10c06eeb5 | ||
|
|
f94661561d | ||
|
|
a53efcf646 | ||
|
|
623aec2984 | ||
|
|
aba49c3d4f | ||
|
|
28189ea026 |
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -32,7 +32,7 @@ runs:
|
||||
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
|
||||
# downstream jobs rely on is preserved.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: Set up uv
|
||||
# ``--system`` (below) installs into the setup-python interpreter;
|
||||
# no venv is created or restored by this workflow.
|
||||
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull-request-only workflow: a save could never be shared and
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
|
||||
# that ``. venv/bin/activate`` see an identical layout.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -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
|
||||
@@ -413,7 +413,7 @@ jobs:
|
||||
- name: Set up uv
|
||||
# Only needed on cache miss to populate the venv.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -1274,7 +1274,7 @@ jobs:
|
||||
# install step (order-of-magnitude faster on cold boots,
|
||||
# with its own wheel cache). actions/setup-python still
|
||||
# provides the interpreter.
|
||||
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
|
||||
@@ -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}}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
# setup-python interpreter so subsequent ``prek`` /
|
||||
# ``script/run-in-env.py`` steps find the deps without a
|
||||
# ``uv run`` prefix.
|
||||
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pin uv version so the action does not have to fetch the
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:**
|
||||
|
||||
@@ -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
|
||||
@@ -548,6 +549,7 @@ esphome/components/sx127x/* @swoboda1337
|
||||
esphome/components/sy6970/* @linkedupbits
|
||||
esphome/components/syslog/* @clydebarrow
|
||||
esphome/components/t6615/* @tylermenezes
|
||||
esphome/components/tas2780/* @remcom
|
||||
esphome/components/tc74/* @sethgirvan
|
||||
esphome/components/tca9548a/* @andreashergert1984
|
||||
esphome/components/tca9555/* @mobrembski
|
||||
|
||||
+1
-4
@@ -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 \
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
# Home Assistant Add-on: ESPHome
|
||||
# Sends discovery information to Home Assistant.
|
||||
# ==============================================================================
|
||||
declare config
|
||||
declare port
|
||||
|
||||
# We only disable it when disabled explicitly
|
||||
@@ -19,14 +18,17 @@ port=$(bashio::addon.ingress_port)
|
||||
# Wait for the ESPHome Device Builder to become available
|
||||
bashio::net.wait_for "${port}" "127.0.0.1" 300
|
||||
|
||||
config=$(\
|
||||
bashio::var.json \
|
||||
host "127.0.0.1" \
|
||||
port "^${port}" \
|
||||
)
|
||||
# Send one discovery message; the config is a JSON string built with bashio::var.json.
|
||||
send_discovery() {
|
||||
local service=$1
|
||||
local config=$2
|
||||
if bashio::discovery "${service}" "${config}" > /dev/null; then
|
||||
bashio::log.info "Successfully send ${service} discovery information to Home Assistant."
|
||||
else
|
||||
bashio::log.error "${service} discovery message to Home Assistant failed!"
|
||||
fi
|
||||
}
|
||||
|
||||
if bashio::discovery "esphome" "${config}" > /dev/null; then
|
||||
bashio::log.info "Successfully send discovery information to Home Assistant."
|
||||
else
|
||||
bashio::log.error "Discovery message to Home Assistant failed!"
|
||||
fi
|
||||
send_discovery "esphome" "$(bashio::var.json host "127.0.0.1" port "^${port}")"
|
||||
# The Device Builder MCP server, consumed by Home Assistant's mcp integration.
|
||||
send_discovery "mcp" "$(bashio::var.json url "http://127.0.0.1:${port}/api/mcp")"
|
||||
|
||||
@@ -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
|
||||
|
||||
+295
-17
@@ -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,291 @@ 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))
|
||||
|
||||
|
||||
def literal_with_length(config: ConfigType, value: str) -> str:
|
||||
"""Renderer for a ``(const char *, size_t)`` target: a plain literal plus its byte length.
|
||||
|
||||
The target compares or copies the bytes in place, so it needs the RAM literal rather than
|
||||
the PROGMEM rendering on ESP8266, and the length saves a strlen.
|
||||
"""
|
||||
return f"{cg.safe_exp(value)}, {len(value.encode('utf-8'))}"
|
||||
|
||||
|
||||
@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, id_key: str = CONF_ID) -> str:
|
||||
# Global-scope qualified so a trigger arg named like the id cannot shadow it.
|
||||
return f"::{await cg.get_variable(config[id_key])}"
|
||||
|
||||
|
||||
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,
|
||||
id_key: str = CONF_ID,
|
||||
) -> None:
|
||||
"""Register an action that only forwards config values to its parent, with no C++ class.
|
||||
|
||||
Generates one stateless function for ``ApplyAction<Ts...>``: the parent (read from
|
||||
``id_key``) and constants are baked in, lambdas are called inline with the trigger args.
|
||||
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)
|
||||
]
|
||||
_check_key_in_schema(name, schema, id_key)
|
||||
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, id_key)
|
||||
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, id_key: str = CONF_ID
|
||||
) -> 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
|
||||
_check_key_in_schema(name, schema, id_key)
|
||||
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, id_key)
|
||||
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 +745,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
@@ -64,7 +64,6 @@ from esphome.cpp_types import ( # noqa: F401
|
||||
Application,
|
||||
Component,
|
||||
ComponentPtr,
|
||||
Controller,
|
||||
EntityBase,
|
||||
EntityCategory,
|
||||
ESPTime,
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -81,7 +81,7 @@ def AUTO_LOAD(config: ConfigType) -> list[str]:
|
||||
|
||||
|
||||
api_ns = cg.esphome_ns.namespace("api")
|
||||
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)
|
||||
APIServer = api_ns.class_("APIServer", cg.Component)
|
||||
HomeAssistantServiceCallAction = api_ns.class_(
|
||||
"HomeAssistantServiceCallAction", automation.Action
|
||||
)
|
||||
@@ -462,8 +462,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
# Track controller registration for StaticVector sizing
|
||||
CORE.register_controller()
|
||||
CORE.register_controller(var)
|
||||
|
||||
# Request a log listener slot for API log streaming
|
||||
request_log_listener()
|
||||
|
||||
@@ -600,7 +600,7 @@ bool APIConnection::send_light_state(light::LightState *light) {
|
||||
uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
|
||||
auto *light = static_cast<light::LightState *>(entity);
|
||||
LightStateResponse resp;
|
||||
auto values = light->remote_values;
|
||||
auto values = light->get_reported_values();
|
||||
auto color_mode = values.get_color_mode();
|
||||
resp.state = values.is_on();
|
||||
resp.color_mode = static_cast<enums::ColorMode>(color_mode);
|
||||
@@ -2267,7 +2267,12 @@ bool APIConnection::send_message_(uint32_t payload_size, uint16_t message_type,
|
||||
// Capacity reserved above, cannot fail
|
||||
(void) shared_buf.resize(write_start + payload_size);
|
||||
ProtoWriteBuffer buffer{&shared_buf, write_start};
|
||||
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
|
||||
uint8_t *end = encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
proto_check_encode_end(end, shared_buf.data() + shared_buf.size());
|
||||
#else
|
||||
(void) end;
|
||||
#endif
|
||||
return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type);
|
||||
}
|
||||
// encode_to_buffer is defined inline in api_connection.h (ESPHOME_ALWAYS_INLINE)
|
||||
|
||||
@@ -346,11 +346,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
/// Returns false as soon as the TCP buffer is full. Marked nodiscard so we
|
||||
/// have no silent failures: every caller must handle (or log) a refusal.
|
||||
template<typename T> [[nodiscard]] bool send_message(const T &msg) {
|
||||
if constexpr (T::ESTIMATED_SIZE == 0) {
|
||||
return this->send_message_(0, T::MESSAGE_TYPE, &encode_msg_noop, &msg);
|
||||
} else {
|
||||
return this->send_message_(msg.calculate_size(), T::MESSAGE_TYPE, &proto_encode_msg<T>, &msg);
|
||||
}
|
||||
return this->send_message_(T::calc_size_msg(&msg), T::MESSAGE_TYPE, &T::encode_msg, &msg);
|
||||
}
|
||||
|
||||
/// Clear the shared write buffer and reserve space for the first message.
|
||||
@@ -406,16 +402,6 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
void process_state_subscriptions_();
|
||||
#endif
|
||||
|
||||
// Size thunk — converts void* back to concrete type for direct calculate_size() call
|
||||
template<typename T> static uint32_t calc_size(const void *msg) {
|
||||
return static_cast<const T *>(msg)->calculate_size();
|
||||
}
|
||||
|
||||
// Shared no-op encode thunk for empty messages (ESTIMATED_SIZE == 0)
|
||||
static uint8_t *encode_msg_noop(const void *, ProtoWriteBuffer &buf PROTO_ENCODE_DEBUG_PARAM) {
|
||||
return buf.get_pos();
|
||||
}
|
||||
|
||||
// Non-template buffer management for send_message
|
||||
bool send_message_(uint32_t payload_size, uint16_t message_type, MessageEncodeFn encode_fn, const void *msg);
|
||||
|
||||
@@ -434,11 +420,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
// Hot paths (state/info) go through fill_and_encode_entity_state/info instead.
|
||||
// batch_message_type_ is already set by dispatch_message_ before reaching here.
|
||||
template<typename T> static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) {
|
||||
if constexpr (T::ESTIMATED_SIZE == 0) {
|
||||
return encode_to_buffer_slow(0, &encode_msg_noop, &msg, conn, remaining_size);
|
||||
} else {
|
||||
return encode_to_buffer_slow(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size);
|
||||
}
|
||||
return encode_to_buffer_slow(T::calc_size_msg(&msg), &T::encode_msg, &msg, conn, remaining_size);
|
||||
}
|
||||
|
||||
// Non-template core — fills state fields and encodes
|
||||
@@ -450,7 +432,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
template<typename T>
|
||||
static uint16_t fill_and_encode_entity_state(EntityBase *entity, T &msg, APIConnection *conn,
|
||||
uint32_t remaining_size) {
|
||||
return fill_and_encode_entity_state(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size);
|
||||
return fill_and_encode_entity_state(entity, msg, &T::calc_size_msg, &T::encode_msg, conn, remaining_size);
|
||||
}
|
||||
|
||||
// Non-template core — fills info fields, allocates buffers, and encodes
|
||||
@@ -462,7 +444,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
template<typename T>
|
||||
static uint16_t fill_and_encode_entity_info(EntityBase *entity, T &msg, APIConnection *conn,
|
||||
uint32_t remaining_size) {
|
||||
return fill_and_encode_entity_info(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size);
|
||||
return fill_and_encode_entity_info(entity, msg, &T::calc_size_msg, &T::encode_msg, conn, remaining_size);
|
||||
}
|
||||
|
||||
// Non-template core — fills device_class, then delegates to fill_and_encode_entity_info
|
||||
@@ -476,8 +458,8 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, T &msg,
|
||||
StringRef &device_class_field, APIConnection *conn,
|
||||
uint32_t remaining_size) {
|
||||
return fill_and_encode_entity_info_with_device_class(entity, msg, device_class_field, &calc_size<T>,
|
||||
&proto_encode_msg<T>, conn, remaining_size);
|
||||
return fill_and_encode_entity_info_with_device_class(entity, msg, device_class_field, &T::calc_size_msg,
|
||||
&T::encode_msg, conn, remaining_size);
|
||||
}
|
||||
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
|
||||
@@ -46,7 +46,13 @@ inline uint16_t ESPHOME_ALWAYS_INLINE APIConnection::encode_to_buffer(uint32_t c
|
||||
return 0;
|
||||
}
|
||||
ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size};
|
||||
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
|
||||
uint8_t *end = encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
// A body that writes fewer bytes than calculate_size() promised would ship stale buffer bytes
|
||||
proto_check_encode_end(end, shared_buf.data() + shared_buf.size());
|
||||
#else
|
||||
(void) end;
|
||||
#endif
|
||||
|
||||
return total_calculated_size;
|
||||
}
|
||||
|
||||
@@ -5,16 +5,13 @@
|
||||
#include "esphome/components/noise/noise.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "proto.h"
|
||||
#include <cstring>
|
||||
#include <cinttypes>
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
#include <pgmspace.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
using noise::noise_err_to_logstr;
|
||||
@@ -26,11 +23,7 @@ static_assert(MAX_HANDSHAKE_SIZE == noise::MAX_HANDSHAKE_SIZE,
|
||||
"api and noise component handshake size limits must match");
|
||||
|
||||
static const char *const TAG = "api.noise";
|
||||
#ifdef USE_ESP8266
|
||||
static constexpr char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit";
|
||||
#else
|
||||
static const char *const PROLOGUE_INIT = "NoiseAPIInit";
|
||||
#endif
|
||||
static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
|
||||
|
||||
// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512)
|
||||
@@ -72,11 +65,7 @@ APIError APINoiseFrameHelper::init() {
|
||||
state_ = State::FAILED;
|
||||
return APIError::OUT_OF_MEMORY;
|
||||
}
|
||||
#ifdef USE_ESP8266
|
||||
memcpy_P(dst, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
|
||||
#else
|
||||
std::memcpy(dst, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
|
||||
#endif
|
||||
progmem_memcpy(dst, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
|
||||
|
||||
state_ = State::CLIENT_HELLO;
|
||||
return APIError::OK;
|
||||
|
||||
+2662
-2638
File diff suppressed because it is too large
Load Diff
+875
-352
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@
|
||||
#include "api_connection.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -61,8 +60,6 @@ void APIServer::socket_failed_(const LogString *msg) {
|
||||
}
|
||||
|
||||
void APIServer::setup() {
|
||||
ControllerRegistry::register_controller(this);
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
// Always reserve the slot: flash preferences are positional on esp8266, so
|
||||
// a yaml key build must keep the layout of a runtime key build
|
||||
@@ -455,8 +452,9 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
|
||||
// Home Assistant subscribes to actions shortly *after* authenticating, so actions
|
||||
// fired right at connection time (on_client_connected, on_time_sync, ...) can
|
||||
// arrive before the subscription and are lost - warn instead of failing silently.
|
||||
ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s",
|
||||
call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"), call.service.c_str(),
|
||||
ESP_LOGW(TAG, "Home Assistant %s '%.*s' dropped; %s",
|
||||
call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"),
|
||||
static_cast<int>(call.service.size()), call.service.empty() ? "" : call.service.c_str(),
|
||||
this->is_connected() ? LOG_STR_LITERAL("client has not subscribed to actions (yet)")
|
||||
: LOG_STR_LITERAL("no client connected"));
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/controller.h"
|
||||
#include "esphome/core/entity_includes.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#ifdef USE_PROVISIONING
|
||||
@@ -49,8 +49,7 @@ struct SavedNoisePsk {
|
||||
bool load_saved_noise_psk(noise::psk_t &out);
|
||||
#endif
|
||||
|
||||
class APIServer final : public Component,
|
||||
public Controller
|
||||
class APIServer final : public Component
|
||||
#ifdef USE_CAMERA
|
||||
,
|
||||
public camera::CameraListener
|
||||
@@ -94,58 +93,58 @@ class APIServer final : public Component,
|
||||
|
||||
void handle_disconnect(APIConnection *conn);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override;
|
||||
void on_binary_sensor_update(binary_sensor::BinarySensor *obj);
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
void on_cover_update(cover::Cover *obj) override;
|
||||
void on_cover_update(cover::Cover *obj);
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
void on_fan_update(fan::Fan *obj) override;
|
||||
void on_fan_update(fan::Fan *obj);
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
void on_light_update(light::LightState *obj) override;
|
||||
void on_light_update(light::LightState *obj);
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
void on_sensor_update(sensor::Sensor *obj) override;
|
||||
void on_sensor_update(sensor::Sensor *obj);
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
void on_switch_update(switch_::Switch *obj) override;
|
||||
void on_switch_update(switch_::Switch *obj);
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void on_text_sensor_update(text_sensor::TextSensor *obj) override;
|
||||
void on_text_sensor_update(text_sensor::TextSensor *obj);
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
void on_climate_update(climate::Climate *obj) override;
|
||||
void on_climate_update(climate::Climate *obj);
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
void on_number_update(number::Number *obj) override;
|
||||
void on_number_update(number::Number *obj);
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
void on_date_update(datetime::DateEntity *obj) override;
|
||||
void on_date_update(datetime::DateEntity *obj);
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
void on_time_update(datetime::TimeEntity *obj) override;
|
||||
void on_time_update(datetime::TimeEntity *obj);
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
void on_datetime_update(datetime::DateTimeEntity *obj) override;
|
||||
void on_datetime_update(datetime::DateTimeEntity *obj);
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
void on_text_update(text::Text *obj) override;
|
||||
void on_text_update(text::Text *obj);
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
void on_select_update(select::Select *obj) override;
|
||||
void on_select_update(select::Select *obj);
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
void on_lock_update(lock::Lock *obj) override;
|
||||
void on_lock_update(lock::Lock *obj);
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
void on_valve_update(valve::Valve *obj) override;
|
||||
void on_valve_update(valve::Valve *obj);
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
void on_media_player_update(media_player::MediaPlayer *obj) override;
|
||||
void on_media_player_update(media_player::MediaPlayer *obj);
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
void on_water_heater_update(water_heater::WaterHeater *obj) override;
|
||||
void on_water_heater_update(water_heater::WaterHeater *obj);
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void send_homeassistant_action(const HomeassistantActionRequest &call);
|
||||
@@ -188,13 +187,13 @@ class APIServer final : public Component,
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override;
|
||||
void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj);
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
void on_event(event::Event *obj) override;
|
||||
void on_event(event::Event *obj);
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
void on_update(update::UpdateEntity *obj) override;
|
||||
void on_update(update::UpdateEntity *obj);
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_zwave_proxy_request(const ZWaveProxyRequest &msg);
|
||||
|
||||
@@ -195,6 +195,12 @@ void proto_check_bounds_failed(const uint8_t *pos, size_t bytes, const uint8_t *
|
||||
ESP_LOGE(TAG, "Proto encode bounds check failed in %s: need %zu bytes, %td available", caller, bytes, end - pos);
|
||||
abort();
|
||||
}
|
||||
void proto_check_encode_end(const uint8_t *end, const uint8_t *expected) {
|
||||
if (end == expected)
|
||||
return;
|
||||
ESP_LOGE(TAG, "Proto encode ended %td bytes off the calculated size", end - expected);
|
||||
abort();
|
||||
}
|
||||
void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) {
|
||||
if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) {
|
||||
ESP_LOGE(TAG, "ProtoWriteBuffer bounds check failed in %s: bytes=%zu offset=%td buf_size=%zu", caller, bytes,
|
||||
@@ -210,77 +216,78 @@ void ProtoWriteBuffer::debug_check_encode_size_(uint32_t field_id, uint32_t expe
|
||||
|
||||
#endif
|
||||
|
||||
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
||||
void ProtoDecodableMessage::decode_fields(void *msg, const uint8_t *buffer, size_t length, DecodeFieldFn field) {
|
||||
const uint8_t *ptr = buffer;
|
||||
const uint8_t *end = buffer + length;
|
||||
|
||||
while (ptr < end) {
|
||||
// Parse field header - ptr < end guarantees len >= 1
|
||||
// Single-byte varints dominate, so that case advances the cursor inline.
|
||||
auto read_varint = [&](proto_varint_value_t &value) ESPHOME_ALWAYS_INLINE {
|
||||
if (ptr == end)
|
||||
return false;
|
||||
if (*ptr < 0x80) [[likely]] {
|
||||
value = *ptr++;
|
||||
return true;
|
||||
}
|
||||
auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr);
|
||||
if (!res.has_value()) {
|
||||
if (!res.has_value())
|
||||
return false;
|
||||
value = res.value;
|
||||
ptr += res.consumed;
|
||||
return true;
|
||||
};
|
||||
|
||||
while (ptr < end) {
|
||||
proto_varint_value_t tag_value;
|
||||
if (!read_varint(tag_value)) {
|
||||
ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t tag = static_cast<uint32_t>(res.value);
|
||||
uint32_t tag = static_cast<uint32_t>(tag_value);
|
||||
uint32_t field_type = tag & WIRE_TYPE_MASK;
|
||||
uint32_t field_id = tag >> 3;
|
||||
ptr += res.consumed;
|
||||
// Length-delimited fields move this past the length prefix
|
||||
const uint8_t *data = ptr;
|
||||
proto_varint_value_t scalar;
|
||||
|
||||
switch (field_type) {
|
||||
case WIRE_TYPE_VARINT: { // VarInt
|
||||
res = ProtoVarInt::parse(ptr, end - ptr);
|
||||
if (!res.has_value()) {
|
||||
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
if (!this->decode_varint(field_id, res.value)) {
|
||||
ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu64 "!", field_id,
|
||||
static_cast<uint64_t>(res.value));
|
||||
}
|
||||
ptr += res.consumed;
|
||||
break;
|
||||
}
|
||||
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited
|
||||
res = ProtoVarInt::parse(ptr, end - ptr);
|
||||
if (!res.has_value()) {
|
||||
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
uint32_t field_length = static_cast<uint32_t>(res.value);
|
||||
ptr += res.consumed;
|
||||
if (field_length > static_cast<size_t>(end - ptr)) {
|
||||
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
if (!this->decode_length(field_id, ProtoLengthDelimited(ptr, field_length))) {
|
||||
ESP_LOGV(TAG, "Cannot decode Length Delimited field %" PRIu32 "!", field_id);
|
||||
}
|
||||
ptr += field_length;
|
||||
break;
|
||||
}
|
||||
case WIRE_TYPE_FIXED32: { // 32-bit
|
||||
if (end - ptr < 4) {
|
||||
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
uint32_t val;
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
// Protobuf fixed32 is little-endian — direct load on LE platforms
|
||||
memcpy(&val, ptr, 4);
|
||||
#else
|
||||
val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
|
||||
#endif
|
||||
if (!this->decode_32bit(field_id, Proto32Bit(val))) {
|
||||
ESP_LOGV(TAG, "Cannot decode 32-bit field %" PRIu32 " with value %" PRIu32 "!", field_id, val);
|
||||
}
|
||||
ptr += 4;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer));
|
||||
if (field_type == WIRE_TYPE_VARINT) [[likely]] {
|
||||
if (!read_varint(scalar)) {
|
||||
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
switch (field_type) {
|
||||
case WIRE_TYPE_LENGTH_DELIMITED: {
|
||||
proto_varint_value_t length_value;
|
||||
if (!read_varint(length_value)) {
|
||||
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
uint32_t field_length = static_cast<uint32_t>(length_value);
|
||||
if (field_length > static_cast<size_t>(end - ptr)) {
|
||||
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
data = ptr;
|
||||
scalar = field_length;
|
||||
ptr += field_length;
|
||||
break;
|
||||
}
|
||||
case WIRE_TYPE_FIXED32: {
|
||||
if (end - ptr < 4) {
|
||||
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
// Byte loads instead of memcpy: ESP-IDF passes -fno-builtin-memcpy, which made this a call
|
||||
scalar = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
|
||||
ptr += 4;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
}
|
||||
field(msg, tag, data, scalar);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+243
-170
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
@@ -170,40 +171,43 @@ class ProtoVarInt {
|
||||
class ProtoMessage;
|
||||
class ProtoSize;
|
||||
|
||||
class ProtoLengthDelimited {
|
||||
/// Case label for decode_field(): the wire tag of a field, so a field that arrives with another wire
|
||||
/// type matches no case.
|
||||
constexpr uint32_t proto_tag(uint32_t field_id, uint32_t wire_type) { return (field_id << 3) | wire_type; }
|
||||
|
||||
/// One decoded field: the payload pointer and a scalar holding the varint or fixed32 value, or the
|
||||
/// length of a length-delimited field. The wire type in the tag says which applies; accessors do not check.
|
||||
class ProtoFieldValue {
|
||||
public:
|
||||
explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {}
|
||||
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); }
|
||||
ProtoFieldValue(const uint8_t *data, proto_varint_value_t scalar) : data_(data), scalar_(scalar) {}
|
||||
|
||||
// Direct access to raw data without string allocation
|
||||
const uint8_t *data() const { return this->value_; }
|
||||
size_t size() const { return this->length_; }
|
||||
proto_varint_value_t as_varint() const { return this->scalar_; }
|
||||
// A bool is sent as 0 or 1, so the low word is enough and saves a second compare with 64 bit varints
|
||||
bool as_bool() const { return static_cast<uint32_t>(this->scalar_) != 0; }
|
||||
|
||||
/// Decode the length-delimited data into a message instance.
|
||||
// Length-delimited accessors
|
||||
const uint8_t *data() const { return this->data_; }
|
||||
size_t size() const { return static_cast<size_t>(this->scalar_); }
|
||||
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->data_), this->size()); }
|
||||
/// Decode the length-delimited payload into a message instance.
|
||||
/// Template preserves concrete type so decode() resolves statically.
|
||||
template<typename T> void decode_to_message(T &msg) const;
|
||||
template<typename T> void decode_to_message(T &msg) const { msg.decode(this->data_, this->size()); }
|
||||
|
||||
protected:
|
||||
const uint8_t *const value_;
|
||||
const size_t length_;
|
||||
};
|
||||
|
||||
class Proto32Bit {
|
||||
public:
|
||||
explicit Proto32Bit(uint32_t value) : value_(value) {}
|
||||
uint32_t as_fixed32() const { return this->value_; }
|
||||
int32_t as_sfixed32() const { return static_cast<int32_t>(this->value_); }
|
||||
// Fixed32 accessors
|
||||
uint32_t as_fixed32() const { return static_cast<uint32_t>(this->scalar_); }
|
||||
int32_t as_sfixed32() const { return static_cast<int32_t>(this->as_fixed32()); }
|
||||
float as_float() const {
|
||||
union {
|
||||
uint32_t raw;
|
||||
float value;
|
||||
} s{};
|
||||
s.raw = this->value_;
|
||||
s.raw = this->as_fixed32();
|
||||
return s.value;
|
||||
}
|
||||
|
||||
protected:
|
||||
const uint32_t value_;
|
||||
private:
|
||||
const uint8_t *data_;
|
||||
proto_varint_value_t scalar_;
|
||||
};
|
||||
|
||||
// NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported
|
||||
@@ -221,6 +225,9 @@ class Proto32Bit {
|
||||
proto_check_bounds_failed(pos, n, proto_debug_end_, __builtin_FUNCTION()); \
|
||||
} while (0)
|
||||
void proto_check_bounds_failed(const uint8_t *pos, size_t bytes, const uint8_t *end, const char *caller);
|
||||
/// Aborts unless an encode body ended exactly where calculate_size() promised. A plain check rather than
|
||||
/// assert(), so NDEBUG cannot switch it off.
|
||||
void proto_check_encode_end(const uint8_t *end, const uint8_t *expected);
|
||||
#else
|
||||
#define PROTO_ENCODE_DEBUG_PARAM
|
||||
#define PROTO_ENCODE_DEBUG_ARG
|
||||
@@ -252,7 +259,7 @@ class ProtoWriteBuffer {
|
||||
*
|
||||
* Following https://protobuf.dev/programming-guides/encoding/#structure
|
||||
*/
|
||||
void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); }
|
||||
void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw(proto_tag(field_id, type)); }
|
||||
/// Single-pass encode for repeated submessage elements.
|
||||
/// Thin template wrapper; all buffer work is in the non-template core.
|
||||
template<typename T> void encode_sub_message(uint32_t field_id, const T &value);
|
||||
@@ -287,19 +294,31 @@ class ProtoWriteBuffer {
|
||||
uint8_t *pos_;
|
||||
};
|
||||
|
||||
// A four byte unaligned store is a memcpy call on ESP-IDF (-fno-builtin-memcpy) and on ARM cores without
|
||||
// unaligned access (Cortex-M0+, ARM9), so those targets share one outlined byte store helper per fixed32
|
||||
// field. Elsewhere the write inlines to a single store, or on ESP8266 to a few stores that measured
|
||||
// faster than a call, so it stays inline.
|
||||
#if defined(USE_ESP32) || (defined(__arm__) && !defined(__ARM_FEATURE_UNALIGNED))
|
||||
#define PROTO_OUTLINE_FOR_SIZE __attribute__((noinline))
|
||||
#define PROTO_FIXED32_BYTE_STORES true
|
||||
#else
|
||||
#define PROTO_OUTLINE_FOR_SIZE inline
|
||||
#define PROTO_FIXED32_BYTE_STORES false
|
||||
#endif
|
||||
|
||||
// Varint encoding thresholds — used by both proto_encode_* free functions and ProtoSize.
|
||||
constexpr uint32_t VARINT_MAX_1_BYTE = 1 << 7; // 128
|
||||
constexpr uint32_t VARINT_MAX_2_BYTE = 1 << 14; // 16384
|
||||
|
||||
/// Static encode helpers for generated encode() functions.
|
||||
/// Generated code hoists buffer.pos_ into a local uint8_t *__restrict__ pos,
|
||||
/// then calls these methods which take pos by reference. No struct, no overhead.
|
||||
/// For sub-messages, pos is synced back to buffer before the call and reloaded after.
|
||||
/// Static encode helpers for the generated encode bodies. Each takes the write cursor by value and
|
||||
/// returns it advanced, so outlined calls at -Os chain through the return register instead of a
|
||||
/// stack slot. Helpers without a _force suffix skip fields holding the proto3 default.
|
||||
class ProtoEncode {
|
||||
public:
|
||||
/// Write a multi-byte varint directly through a pos pointer.
|
||||
template<typename T>
|
||||
static inline void encode_varint_raw_loop(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, T value) {
|
||||
[[nodiscard]] static inline uint8_t *encode_varint_raw_loop(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
T value) {
|
||||
do {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = static_cast<uint8_t>(value | 0x80);
|
||||
@@ -307,48 +326,49 @@ class ProtoEncode {
|
||||
} while (value > 0x7F);
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = static_cast<uint8_t>(value);
|
||||
return pos;
|
||||
}
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t value) {
|
||||
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
|
||||
encode_varint_raw(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint32_t value) {
|
||||
if (value < VARINT_MAX_1_BYTE) [[likely]] {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = static_cast<uint8_t>(value);
|
||||
return;
|
||||
return pos;
|
||||
}
|
||||
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
return encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
/// Encode a varint that is expected to be 1-2 bytes (e.g. zigzag RSSI, small lengths).
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_short(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t value) {
|
||||
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
|
||||
encode_varint_raw_short(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint32_t value) {
|
||||
if (value < VARINT_MAX_1_BYTE) [[likely]] {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = static_cast<uint8_t>(value);
|
||||
return;
|
||||
return pos;
|
||||
}
|
||||
if (value < VARINT_MAX_2_BYTE) [[likely]] {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 2);
|
||||
*pos++ = static_cast<uint8_t>(value | 0x80);
|
||||
*pos++ = static_cast<uint8_t>(value >> 7);
|
||||
return;
|
||||
return pos;
|
||||
}
|
||||
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
return encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint64_t value) {
|
||||
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
|
||||
encode_varint_raw_64(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint64_t value) {
|
||||
if (value < VARINT_MAX_1_BYTE) [[likely]] {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = static_cast<uint8_t>(value);
|
||||
return;
|
||||
return pos;
|
||||
}
|
||||
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
return encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
/// Encode a 48-bit MAC address (stored in a uint64) as varint.
|
||||
/// Real MAC addresses occupy the full 48 bits (OUI in upper 24), so the
|
||||
/// fast path -- any non-zero bit in the top 6 of 48 -- emits exactly 7 bytes
|
||||
/// with no per-byte branch. Falls back to the general loop otherwise.
|
||||
/// Caller must guarantee value fits in 48 bits (checked in debug builds).
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_48bit(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint64_t value) {
|
||||
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
|
||||
encode_varint_raw_48bit(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint64_t value) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(value < (1ULL << (MAC_ADDRESS_SIZE * 8)) && "encode_varint_raw_48bit: value exceeds 48 bits");
|
||||
#endif
|
||||
@@ -363,38 +383,39 @@ class ProtoEncode {
|
||||
pos[4] = static_cast<uint8_t>((value >> 28) | 0x80);
|
||||
pos[5] = static_cast<uint8_t>((value >> 35) | 0x80);
|
||||
pos[6] = static_cast<uint8_t>(value >> 42);
|
||||
pos += 7;
|
||||
return;
|
||||
return pos + 7;
|
||||
}
|
||||
encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
return encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_field_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, uint32_t type) {
|
||||
encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, (field_id << 3) | type);
|
||||
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
|
||||
encode_field_raw(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, uint32_t type) {
|
||||
return encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, proto_tag(field_id, type));
|
||||
}
|
||||
/// Write a single precomputed tag byte. Tag must be < 128.
|
||||
static inline void ESPHOME_ALWAYS_INLINE write_raw_byte(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint8_t b) {
|
||||
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
|
||||
write_raw_byte(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint8_t b) {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = b;
|
||||
return pos;
|
||||
}
|
||||
/// Reserve one byte for later backpatch (e.g., sub-message length).
|
||||
/// Advances pos past the reserved byte without writing a value.
|
||||
static inline void ESPHOME_ALWAYS_INLINE reserve_byte(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
|
||||
reserve_byte(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
pos++;
|
||||
return pos + 1;
|
||||
}
|
||||
/// Write raw bytes to the buffer (no tag, no length prefix).
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
const void *data, size_t len) {
|
||||
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
|
||||
encode_raw(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, const void *data, size_t len) {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, len);
|
||||
std::memcpy(pos, data, len);
|
||||
pos += len;
|
||||
return pos + len;
|
||||
}
|
||||
/// Encode tag + 1-byte length + raw string data. For strings with max_data_length < 128.
|
||||
/// Tag must be a single-byte varint (< 128). Always encodes (no zero check).
|
||||
static inline void encode_short_string_force(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag,
|
||||
const StringRef &ref) {
|
||||
[[nodiscard]] static inline uint8_t *encode_short_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint8_t tag, const StringRef &ref) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(ref.size() < 128 && "encode_short_string_force: string exceeds max_data_length < 128");
|
||||
#endif
|
||||
@@ -402,137 +423,191 @@ class ProtoEncode {
|
||||
pos[0] = tag;
|
||||
pos[1] = static_cast<uint8_t>(ref.size());
|
||||
std::memcpy(pos + 2, ref.c_str(), ref.size());
|
||||
pos += 2 + ref.size();
|
||||
return pos + 2 + ref.size();
|
||||
}
|
||||
/// Write a precomputed tag byte + 32-bit value in one operation.
|
||||
static inline void ESPHOME_ALWAYS_INLINE write_tag_and_fixed32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint8_t tag, uint32_t value) {
|
||||
/// Write a precomputed tag byte + 32-bit value. Outlined on embedded: one copy beats inline stores per field.
|
||||
[[nodiscard]] static PROTO_OUTLINE_FOR_SIZE uint8_t *write_tag_and_fixed32(
|
||||
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, uint32_t value) {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 5);
|
||||
pos[0] = tag;
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
std::memcpy(pos + 1, &value, 4);
|
||||
#else
|
||||
pos[1] = static_cast<uint8_t>(value & 0xFF);
|
||||
pos[2] = static_cast<uint8_t>((value >> 8) & 0xFF);
|
||||
pos[3] = static_cast<uint8_t>((value >> 16) & 0xFF);
|
||||
pos[4] = static_cast<uint8_t>((value >> 24) & 0xFF);
|
||||
#endif
|
||||
pos += 5;
|
||||
write_fixed32_le(pos + 1, value);
|
||||
return pos + 5;
|
||||
}
|
||||
static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
|
||||
const char *string, size_t len, bool force = false) {
|
||||
if (len == 0 && !force)
|
||||
return;
|
||||
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 2); // type 2: Length-delimited string
|
||||
[[nodiscard]] static inline uint8_t *encode_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, const char *string, size_t len) {
|
||||
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 2); // type 2: Length-delimited string
|
||||
// NOLINTNEXTLINE(readability-inconsistent-ifelse-braces) -- false positive on [[likely]] attribute
|
||||
if (len < VARINT_MAX_1_BYTE) [[likely]] {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1 + len);
|
||||
*pos++ = static_cast<uint8_t>(len);
|
||||
} else {
|
||||
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, len);
|
||||
pos = encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, len);
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, len);
|
||||
}
|
||||
std::memcpy(pos, string, len);
|
||||
pos += len;
|
||||
return pos + len;
|
||||
}
|
||||
static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
|
||||
const std::string &value, bool force = false) {
|
||||
encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, value.data(), value.size(), force);
|
||||
[[nodiscard]] static inline uint8_t *encode_string(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, const char *string, size_t len) {
|
||||
if (len == 0)
|
||||
return pos;
|
||||
return encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, string, len);
|
||||
}
|
||||
static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
|
||||
const StringRef &ref, bool force = false) {
|
||||
encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, ref.c_str(), ref.size(), force);
|
||||
[[nodiscard]] static inline uint8_t *encode_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, const std::string &value) {
|
||||
return encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value.data(), value.size());
|
||||
}
|
||||
static inline void encode_bytes(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
|
||||
const uint8_t *data, size_t len, bool force = false) {
|
||||
encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, reinterpret_cast<const char *>(data), len, force);
|
||||
[[nodiscard]] static inline uint8_t *encode_string(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, const StringRef &ref) {
|
||||
return encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, ref.c_str(), ref.size());
|
||||
}
|
||||
static inline void encode_uint32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
|
||||
uint32_t value, bool force = false) {
|
||||
if (value == 0 && !force)
|
||||
return;
|
||||
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
|
||||
encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
[[nodiscard]] static inline uint8_t *encode_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, const StringRef &ref) {
|
||||
return encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, ref.c_str(), ref.size());
|
||||
}
|
||||
static inline void encode_uint64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
|
||||
uint64_t value, bool force = false) {
|
||||
if (value == 0 && !force)
|
||||
return;
|
||||
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
|
||||
encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
[[nodiscard]] static inline uint8_t *encode_bytes(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, const uint8_t *data, size_t len) {
|
||||
return encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, reinterpret_cast<const char *>(data), len);
|
||||
}
|
||||
static inline void encode_bool(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, bool value,
|
||||
bool force = false) {
|
||||
if (!value && !force)
|
||||
return;
|
||||
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
|
||||
[[nodiscard]] static inline uint8_t *encode_bytes_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, const uint8_t *data, size_t len) {
|
||||
return encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, reinterpret_cast<const char *>(data), len);
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_uint32_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, uint32_t value) {
|
||||
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
|
||||
return encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_uint32(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, uint32_t value) {
|
||||
if (value == 0)
|
||||
return pos;
|
||||
return encode_uint32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value);
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_uint64_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, uint64_t value) {
|
||||
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
|
||||
return encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_uint64(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, uint64_t value) {
|
||||
if (value == 0)
|
||||
return pos;
|
||||
return encode_uint64_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value);
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_bool_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, bool value) {
|
||||
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = value ? 0x01 : 0x00;
|
||||
return pos;
|
||||
}
|
||||
static inline void encode_fixed32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
|
||||
uint32_t value, bool force = false) {
|
||||
if (value == 0 && !force)
|
||||
return;
|
||||
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 5);
|
||||
[[nodiscard]] static inline uint8_t *encode_bool(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, bool value) {
|
||||
if (!value)
|
||||
return pos;
|
||||
return encode_bool_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value);
|
||||
}
|
||||
/// Tag + fixed32 for multi-byte tags; single-byte tags use write_tag_and_fixed32.
|
||||
[[nodiscard]] static PROTO_OUTLINE_FOR_SIZE uint8_t *encode_fixed32_force(
|
||||
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, uint32_t value) {
|
||||
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 5);
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 4);
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
std::memcpy(pos, &value, 4);
|
||||
pos += 4;
|
||||
#else
|
||||
*pos++ = (value >> 0) & 0xFF;
|
||||
*pos++ = (value >> 8) & 0xFF;
|
||||
*pos++ = (value >> 16) & 0xFF;
|
||||
*pos++ = (value >> 24) & 0xFF;
|
||||
#endif
|
||||
write_fixed32_le(pos, value);
|
||||
return pos + 4;
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_fixed32(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, uint32_t value) {
|
||||
if (value == 0)
|
||||
return pos;
|
||||
return encode_fixed32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value);
|
||||
}
|
||||
// NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally
|
||||
// not supported to reduce overhead on embedded systems. All ESPHome devices are
|
||||
// 32-bit microcontrollers where 64-bit operations are expensive. If 64-bit support
|
||||
// is needed in the future, the necessary encoding/decoding functions must be added.
|
||||
static inline void encode_float(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, float value,
|
||||
bool force = false) {
|
||||
uint32_t raw = float_to_raw(value);
|
||||
if (raw == 0 && !force)
|
||||
return;
|
||||
encode_fixed32(pos PROTO_ENCODE_DEBUG_ARG, field_id, raw);
|
||||
[[nodiscard]] static inline uint8_t *encode_float(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, float value) {
|
||||
return encode_fixed32(pos PROTO_ENCODE_DEBUG_ARG, field_id, float_to_raw(value));
|
||||
}
|
||||
static inline void encode_int32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, int32_t value,
|
||||
bool force = false) {
|
||||
[[nodiscard]] static inline uint8_t *encode_float_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, float value) {
|
||||
return encode_fixed32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, float_to_raw(value));
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_int32_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, int32_t value) {
|
||||
if (value < 0) {
|
||||
// negative int32 is always 10 byte long
|
||||
encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value), force);
|
||||
return;
|
||||
return encode_uint64_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value));
|
||||
}
|
||||
encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint32_t>(value), force);
|
||||
return encode_uint32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint32_t>(value));
|
||||
}
|
||||
static inline void encode_int64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, int64_t value,
|
||||
bool force = false) {
|
||||
encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value), force);
|
||||
[[nodiscard]] static inline uint8_t *encode_int32(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, int32_t value) {
|
||||
if (value == 0)
|
||||
return pos;
|
||||
return encode_int32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value);
|
||||
}
|
||||
static inline void encode_sint32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
|
||||
int32_t value, bool force = false) {
|
||||
encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag32(value), force);
|
||||
[[nodiscard]] static inline uint8_t *encode_int64(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, int64_t value) {
|
||||
return encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value));
|
||||
}
|
||||
static inline void encode_sint64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
|
||||
int64_t value, bool force = false) {
|
||||
encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag64(value), force);
|
||||
[[nodiscard]] static inline uint8_t *encode_int64_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, int64_t value) {
|
||||
return encode_uint64_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value));
|
||||
}
|
||||
/// Sub-message encoding: sync pos to buffer, delegate, get pos from return value.
|
||||
[[nodiscard]] static inline uint8_t *encode_sint32(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, int32_t value) {
|
||||
return encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag32(value));
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_sint32_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, int32_t value) {
|
||||
return encode_uint32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag32(value));
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_sint64(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, int64_t value) {
|
||||
return encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag64(value));
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_sint64_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, int64_t value) {
|
||||
return encode_uint64_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag64(value));
|
||||
}
|
||||
/// Sub-message encoding: sync pos to buffer, delegate, read the cursor back.
|
||||
template<typename T>
|
||||
static inline void encode_sub_message(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, ProtoWriteBuffer &buffer,
|
||||
uint32_t field_id, const T &value) {
|
||||
[[nodiscard]] static inline uint8_t *encode_sub_message(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
ProtoWriteBuffer &buffer, uint32_t field_id, const T &value) {
|
||||
buffer.set_pos(pos);
|
||||
buffer.encode_sub_message(field_id, value);
|
||||
pos = buffer.get_pos();
|
||||
return buffer.get_pos();
|
||||
}
|
||||
template<typename T>
|
||||
static inline void encode_optional_sub_message(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
ProtoWriteBuffer &buffer, uint32_t field_id, const T &value) {
|
||||
[[nodiscard]] static inline uint8_t *encode_optional_sub_message(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
ProtoWriteBuffer &buffer, uint32_t field_id,
|
||||
const T &value) {
|
||||
buffer.set_pos(pos);
|
||||
buffer.encode_optional_sub_message(field_id, value);
|
||||
pos = buffer.get_pos();
|
||||
return buffer.get_pos();
|
||||
}
|
||||
|
||||
private:
|
||||
/// Unaligned little endian store of four bytes: byte stores where the outlined helper lives (ESP-IDF, ARM
|
||||
/// without unaligned access), otherwise a memcpy the compiler folds into one store. Callers bounds check
|
||||
/// and advance the cursor themselves.
|
||||
static inline void ESPHOME_ALWAYS_INLINE write_fixed32_le(uint8_t *__restrict__ pos, uint32_t value) {
|
||||
if constexpr (PROTO_FIXED32_BYTE_STORES) {
|
||||
// Spelled out so the outlined helper does not itself become a memcpy call
|
||||
pos[0] = static_cast<uint8_t>(value);
|
||||
pos[1] = static_cast<uint8_t>(value >> 8);
|
||||
pos[2] = static_cast<uint8_t>(value >> 16);
|
||||
pos[3] = static_cast<uint8_t>(value >> 24);
|
||||
} else {
|
||||
const uint32_t le = convert_little_endian(value);
|
||||
__builtin_memcpy(pos, &le, 4);
|
||||
}
|
||||
}
|
||||
};
|
||||
#undef PROTO_OUTLINE_FOR_SIZE
|
||||
#undef PROTO_FIXED32_BYTE_STORES
|
||||
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
/**
|
||||
@@ -624,11 +699,12 @@ class DumpBuffer {
|
||||
|
||||
class ProtoMessage {
|
||||
public:
|
||||
// Non-virtual defaults for messages with no fields.
|
||||
// Concrete message classes hide these with their own implementations.
|
||||
// All call sites use templates to preserve the concrete type, so virtual
|
||||
// dispatch is not needed. This eliminates per-message vtable entries for
|
||||
// encode/calculate_size, saving ~1.3 KB of flash across all message types.
|
||||
// Non-virtual defaults for messages with no fields; generated classes hide all four. The
|
||||
// static encode_msg/calc_size_msg take const void * so &T::encode_msg needs no thunk.
|
||||
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) {
|
||||
return buffer.get_pos();
|
||||
}
|
||||
static uint32_t calc_size_msg(const void *self) { return 0; }
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { return buffer.get_pos(); }
|
||||
uint32_t calculate_size() const { return 0; }
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -648,7 +724,13 @@ class ProtoMessage {
|
||||
// Base class for messages that support decoding
|
||||
class ProtoDecodableMessage : public ProtoMessage {
|
||||
public:
|
||||
void decode(const uint8_t *buffer, size_t length);
|
||||
/// Stores one decoded field into \p msg; generated per message type. \p scalar is the varint or
|
||||
/// fixed32 value, or the length of the length-delimited payload at \p data. An unknown field or
|
||||
/// wrong wire type matches no case and is skipped.
|
||||
using DecodeFieldFn = void (*)(void *msg, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar);
|
||||
/// Walk \p buffer and hand every field to \p field. The generated decode() passes the message's
|
||||
/// own decode_field, so decodable messages carry no vtable.
|
||||
static void decode_fields(void *msg, const uint8_t *buffer, size_t length, DecodeFieldFn field);
|
||||
|
||||
/**
|
||||
* Count occurrences of a repeated field in a protobuf buffer.
|
||||
@@ -660,14 +742,15 @@ class ProtoDecodableMessage : public ProtoMessage {
|
||||
* @return Number of times the field appears in the buffer
|
||||
*/
|
||||
static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id);
|
||||
|
||||
protected:
|
||||
~ProtoDecodableMessage() = default;
|
||||
virtual bool decode_varint(uint32_t field_id, proto_varint_value_t value) { return false; }
|
||||
virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; }
|
||||
virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; }
|
||||
// NOTE: decode_64bit removed - wire type 1 not supported
|
||||
// The destructor stays accessible on purpose: the generated messages are aggregates that brace
|
||||
// initialise sub message members, which copies a base temporary. That trades away the compile time
|
||||
// guard against deleting through this type; messages are stack locals and never owned through a base
|
||||
// pointer. ProtoMessage keeps its guard for the dump builds.
|
||||
};
|
||||
#ifndef HAS_PROTO_MESSAGE_DUMP
|
||||
// decode() passes decode_field explicitly, so nothing here may add a vtable
|
||||
static_assert(!std::is_polymorphic_v<ProtoDecodableMessage>, "decodable messages carry no vtable");
|
||||
#endif
|
||||
|
||||
class ProtoSize {
|
||||
public:
|
||||
@@ -792,7 +875,7 @@ class ProtoSize {
|
||||
* @return The number of bytes needed to encode the field ID and wire type
|
||||
*/
|
||||
static constexpr uint32_t field(uint32_t field_id, uint32_t type) {
|
||||
uint32_t tag = (field_id << 3) | (type & WIRE_TYPE_MASK);
|
||||
uint32_t tag = proto_tag(field_id, type & WIRE_TYPE_MASK);
|
||||
return varint(tag);
|
||||
}
|
||||
|
||||
@@ -876,24 +959,14 @@ class ProtoSize {
|
||||
|
||||
// Implementation of methods that depend on ProtoSize being fully defined
|
||||
|
||||
// Encode thunk — converts void* back to concrete type for direct encode() call
|
||||
template<typename T> uint8_t *proto_encode_msg(const void *msg, ProtoWriteBuffer &buf PROTO_ENCODE_DEBUG_PARAM) {
|
||||
return static_cast<const T *>(msg)->encode(buf PROTO_ENCODE_DEBUG_ARG);
|
||||
}
|
||||
|
||||
// Thin template wrapper; delegates to non-template core in proto.cpp.
|
||||
template<typename T> inline void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const T &value) {
|
||||
this->encode_sub_message(field_id, &value, &proto_encode_msg<T>);
|
||||
this->encode_sub_message(field_id, &value, &T::encode_msg);
|
||||
}
|
||||
|
||||
// Thin template wrapper; delegates to non-template core.
|
||||
template<typename T> inline void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, const T &value) {
|
||||
this->encode_optional_sub_message(field_id, value.calculate_size(), &value, &proto_encode_msg<T>);
|
||||
}
|
||||
|
||||
// Template decode_to_message - preserves concrete type so decode() resolves statically
|
||||
template<typename T> void ProtoLengthDelimited::decode_to_message(T &msg) const {
|
||||
msg.decode(this->value_, this->length_);
|
||||
this->encode_optional_sub_message(field_id, T::calc_size_msg(&value), &value, &T::encode_msg);
|
||||
}
|
||||
|
||||
template<typename T> const char *proto_enum_to_string(T value);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#ifdef USE_API
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/component_iterator.h"
|
||||
#include "esphome/core/controller.h"
|
||||
namespace esphome::api {
|
||||
|
||||
class APIConnection;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components.const import UNIT_COUNTS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CLEAR,
|
||||
@@ -33,8 +34,6 @@ CONF_F7 = "f7"
|
||||
CONF_F8 = "f8"
|
||||
CONF_NIR = "nir"
|
||||
|
||||
UNIT_COUNTS = "#"
|
||||
|
||||
AS7341_GAIN = as7341_ns.enum("AS7341Gain")
|
||||
GAIN_OPTIONS = {
|
||||
"X0.5": AS7341_GAIN.AS7341_GAIN_0_5X,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -2,9 +2,8 @@ from esphome import automation
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_VOLUME
|
||||
from esphome.core import ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.const import CONF_VOLUME
|
||||
from esphome.core import CoroPriority, coroutine_with_priority
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
@@ -13,10 +12,6 @@ IS_PLATFORM_COMPONENT = True
|
||||
audio_dac_ns = cg.esphome_ns.namespace("audio_dac")
|
||||
AudioDac = audio_dac_ns.class_("AudioDac")
|
||||
|
||||
MuteOffAction = audio_dac_ns.class_("MuteOffAction", automation.Action)
|
||||
MuteOnAction = audio_dac_ns.class_("MuteOnAction", automation.Action)
|
||||
SetVolumeAction = audio_dac_ns.class_("SetVolumeAction", automation.Action)
|
||||
|
||||
|
||||
MUTE_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
@@ -33,41 +28,19 @@ SET_VOLUME_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"audio_dac.mute_off", MuteOffAction, MUTE_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
@automation.register_action(
|
||||
"audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def audio_dac_mute_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
for _name, _call in (
|
||||
("audio_dac.mute_off", "set_mute_off()"),
|
||||
("audio_dac.mute_on", "set_mute_on()"),
|
||||
):
|
||||
automation.register_apply_action(
|
||||
_name, MUTE_ACTION_SCHEMA, automation.ApplyCall(_call)
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"audio_dac.set_volume",
|
||||
SetVolumeAction,
|
||||
SET_VOLUME_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
automation.ApplyField(CONF_VOLUME, "set_volume", cg.float_),
|
||||
)
|
||||
async def audio_dac_set_volume_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.get(CONF_VOLUME), args, cg.float_)
|
||||
cg.add(var.set_volume(template_))
|
||||
|
||||
return var
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "audio_dac.h"
|
||||
|
||||
namespace esphome::audio_dac {
|
||||
|
||||
template<typename... Ts> class MuteOffAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit MuteOffAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
|
||||
|
||||
void play(const Ts &...x) override { this->audio_dac_->set_mute_off(); }
|
||||
|
||||
protected:
|
||||
AudioDac *audio_dac_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class MuteOnAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit MuteOnAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
|
||||
|
||||
void play(const Ts &...x) override { this->audio_dac_->set_mute_on(); }
|
||||
|
||||
protected:
|
||||
AudioDac *audio_dac_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetVolumeAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit SetVolumeAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
|
||||
|
||||
TEMPLATABLE_VALUE(float, volume)
|
||||
|
||||
void play(const Ts &...x) override { this->audio_dac_->set_volume(this->volume_.value(x...)); }
|
||||
|
||||
protected:
|
||||
AudioDac *audio_dac_;
|
||||
};
|
||||
|
||||
} // namespace esphome::audio_dac
|
||||
@@ -1,7 +1,7 @@
|
||||
from logging import getLogger
|
||||
|
||||
from esphome import automation, core
|
||||
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, zigbee
|
||||
from esphome.components.const import CONF_ON_STATE_CHANGE
|
||||
@@ -134,16 +134,10 @@ 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
|
||||
)
|
||||
|
||||
# Condition
|
||||
BinarySensorCondition = binary_sensor_ns.class_("BinarySensorCondition", Condition)
|
||||
|
||||
# Filters
|
||||
Filter = binary_sensor_ns.class_("Filter")
|
||||
TimeoutFilter = binary_sensor_ns.class_("TimeoutFilter", Filter)
|
||||
@@ -648,20 +642,12 @@ BINARY_SENSOR_CONDITION_SCHEMA = maybe_simple_id(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"binary_sensor.is_on", BinarySensorCondition, BINARY_SENSOR_CONDITION_SCHEMA
|
||||
automation.register_apply_condition(
|
||||
"binary_sensor.is_on", BINARY_SENSOR_CONDITION_SCHEMA, "state"
|
||||
)
|
||||
async def binary_sensor_is_on_to_code(config, condition_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren, True)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"binary_sensor.is_off", BinarySensorCondition, BINARY_SENSOR_CONDITION_SCHEMA
|
||||
automation.register_apply_condition(
|
||||
"binary_sensor.is_off", BINARY_SENSOR_CONDITION_SCHEMA, "state == false"
|
||||
)
|
||||
async def binary_sensor_is_off_to_code(config, condition_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren, False)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
|
||||
@@ -18,26 +18,6 @@ struct MultiClickTriggerEvent {
|
||||
uint32_t max_length;
|
||||
};
|
||||
|
||||
class PressTrigger final : public Trigger<> {
|
||||
public:
|
||||
explicit PressTrigger(BinarySensor *parent) {
|
||||
parent->add_on_state_callback([this](bool state) {
|
||||
if (state)
|
||||
this->trigger();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
class ReleaseTrigger final : public Trigger<> {
|
||||
public:
|
||||
explicit ReleaseTrigger(BinarySensor *parent) {
|
||||
parent->add_on_state_callback([this](bool state) {
|
||||
if (!state)
|
||||
this->trigger();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length);
|
||||
|
||||
class ClickTrigger final : public Trigger<> {
|
||||
@@ -140,45 +120,6 @@ template<size_t N> class MultiClickTrigger final : public MultiClickTriggerBase
|
||||
std::array<MultiClickTriggerEvent, N> timing_storage_{};
|
||||
};
|
||||
|
||||
class StateTrigger final : public Trigger<bool> {
|
||||
public:
|
||||
explicit StateTrigger(BinarySensor *parent) {
|
||||
parent->add_on_state_callback([this](bool state) { this->trigger(state); });
|
||||
}
|
||||
};
|
||||
|
||||
class StateChangeTrigger final : public Trigger<optional<bool>, optional<bool> > {
|
||||
public:
|
||||
explicit StateChangeTrigger(BinarySensor *parent) {
|
||||
parent->add_full_state_callback(
|
||||
[this](optional<bool> old_state, optional<bool> state) { this->trigger(old_state, state); });
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class BinarySensorCondition final : public Condition<Ts...> {
|
||||
public:
|
||||
BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {}
|
||||
bool check(const Ts &...x) override { return this->parent_->state == this->state_; }
|
||||
|
||||
protected:
|
||||
BinarySensor *parent_;
|
||||
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
|
||||
|
||||
@@ -16,11 +16,4 @@ template<typename... Ts> class PressAction final : public Action<Ts...> {
|
||||
Button *button_;
|
||||
};
|
||||
|
||||
class ButtonPressTrigger final : public Trigger<> {
|
||||
public:
|
||||
ButtonPressTrigger(Button *button) {
|
||||
button->add_on_press_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::button
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,35 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
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=automation.literal_with_length,
|
||||
),
|
||||
automation.ApplyField(CONF_PRESET, "set_preset", ClimatePreset),
|
||||
automation.ApplyField(
|
||||
CONF_CUSTOM_PRESET,
|
||||
"set_preset",
|
||||
cg.std_string,
|
||||
const_fn=automation.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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -13,15 +13,7 @@ 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});
|
||||
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_dry_)
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_DRY);
|
||||
if (this->supports_fan_only_)
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY);
|
||||
traits.set_supported_modes(this->modes_);
|
||||
|
||||
traits.set_visual_min_temperature(this->minimum_temperature_);
|
||||
traits.set_visual_max_temperature(this->maximum_temperature_);
|
||||
@@ -94,9 +86,12 @@ void ClimateIR::dump_config() {
|
||||
" Min. Temperature: %.1f°C\n"
|
||||
" Max. Temperature: %.1f°C\n"
|
||||
" Supports HEAT: %s\n"
|
||||
" Supports COOL: %s",
|
||||
this->minimum_temperature_, this->maximum_temperature_, YESNO(this->supports_heat_),
|
||||
YESNO(this->supports_cool_));
|
||||
" Supports COOL: %s\n"
|
||||
" Supports HEAT_COOL: %s",
|
||||
this->minimum_temperature_, this->maximum_temperature_,
|
||||
YESNO(this->modes_.count(climate::CLIMATE_MODE_HEAT)),
|
||||
YESNO(this->modes_.count(climate::CLIMATE_MODE_COOL)),
|
||||
YESNO(this->modes_.count(climate::CLIMATE_MODE_HEAT_COOL)));
|
||||
}
|
||||
|
||||
} // namespace esphome::climate_ir
|
||||
|
||||
@@ -30,8 +30,10 @@ class ClimateIR : public Component,
|
||||
this->minimum_temperature_ = minimum_temperature;
|
||||
this->maximum_temperature_ = maximum_temperature;
|
||||
this->temperature_step_ = temperature_step;
|
||||
this->supports_dry_ = supports_dry;
|
||||
this->supports_fan_only_ = supports_fan_only;
|
||||
if (supports_dry)
|
||||
this->modes_.insert(climate::CLIMATE_MODE_DRY);
|
||||
if (supports_fan_only)
|
||||
this->modes_.insert(climate::CLIMATE_MODE_FAN_ONLY);
|
||||
this->fan_modes_ = fan_modes;
|
||||
this->swing_modes_ = swing_modes;
|
||||
this->presets_ = presets;
|
||||
@@ -39,8 +41,11 @@ class ClimateIR : public Component,
|
||||
|
||||
void setup() override;
|
||||
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_cool(bool supports_cool) { this->set_mode_supported_(climate::CLIMATE_MODE_COOL, supports_cool); }
|
||||
void set_supports_heat(bool supports_heat) { this->set_mode_supported_(climate::CLIMATE_MODE_HEAT, supports_heat); }
|
||||
void set_supports_heat_cool(bool supports_heat_cool) {
|
||||
this->set_mode_supported_(climate::CLIMATE_MODE_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; }
|
||||
|
||||
@@ -58,10 +63,18 @@ class ClimateIR : public Component,
|
||||
// Dummy implement on_receive so implementation is optional for inheritors
|
||||
bool on_receive(remote_base::RemoteReceiveData data) override { return false; };
|
||||
|
||||
bool supports_cool_{true};
|
||||
bool supports_heat_{true};
|
||||
bool supports_dry_{false};
|
||||
bool supports_fan_only_{false};
|
||||
ESPHOME_ALWAYS_INLINE void set_mode_supported_(climate::ClimateMode mode, bool supported) {
|
||||
if (supported) {
|
||||
this->modes_.insert(mode);
|
||||
} else {
|
||||
this->modes_.erase(mode);
|
||||
}
|
||||
}
|
||||
|
||||
// The HEAT_COOL default (supports_cool && supports_heat) is resolved during code generation.
|
||||
static constexpr climate::ClimateModeMask DEFAULT_MODES{climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL,
|
||||
climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_HEAT_COOL};
|
||||
climate::ClimateModeMask modes_{DEFAULT_MODES};
|
||||
climate::ClimateFanModeMask fan_modes_{};
|
||||
climate::ClimateSwingModeMask swing_modes_{};
|
||||
climate::ClimatePresetMask presets_{};
|
||||
|
||||
@@ -23,6 +23,7 @@ CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection"
|
||||
CONF_ENABLED = "enabled"
|
||||
CONF_GYROSCOPE_ODR = "gyroscope_odr"
|
||||
CONF_GYROSCOPE_RANGE = "gyroscope_range"
|
||||
CONF_HUMIDITY_SOURCE = "humidity_source"
|
||||
CONF_IAQ = "iaq"
|
||||
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
|
||||
CONF_IS_WRGB = "is_wrgb"
|
||||
@@ -61,3 +62,4 @@ ICON_SOLAR_POWER = "mdi:solar-power"
|
||||
KEY_METADATA = "metadata"
|
||||
|
||||
UNIT_AMPERE_HOUR = "Ah"
|
||||
UNIT_COUNTS = "#"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -422,8 +422,12 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
if CONF_TOUCH_WAKEUP in config:
|
||||
cg.add(var.set_touch_wakeup(config[CONF_TOUCH_WAKEUP]))
|
||||
if CORE.using_zephyr and "zigbee" not in CORE.loaded_integrations:
|
||||
zephyr_add_prj_conf("POWEROFF", True)
|
||||
if CORE.using_zephyr:
|
||||
# Devices are suspended when CPU is entering a low power state
|
||||
# https://github.com/nrfconnect/sdk-zephyr/blob/v3.7.99-ncs2-2/doc/services/pm/device.rst#system-managed-device-power-management
|
||||
zephyr_add_prj_conf("PM_DEVICE", True)
|
||||
if "zigbee" not in CORE.loaded_integrations:
|
||||
zephyr_add_prj_conf("POWEROFF", True)
|
||||
|
||||
cg.add_define("USE_DEEP_SLEEP")
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -34,23 +37,6 @@ DEVICE = {
|
||||
"TF_CARD": Device.TF_CARD,
|
||||
}
|
||||
|
||||
NextAction = dfplayer_ns.class_("NextAction", automation.Action)
|
||||
PreviousAction = dfplayer_ns.class_("PreviousAction", automation.Action)
|
||||
PlayMp3Action = dfplayer_ns.class_("PlayMp3Action", automation.Action)
|
||||
PlayFileAction = dfplayer_ns.class_("PlayFileAction", automation.Action)
|
||||
PlayFolderAction = dfplayer_ns.class_("PlayFolderAction", automation.Action)
|
||||
SetVolumeAction = dfplayer_ns.class_("SetVolumeAction", automation.Action)
|
||||
VolumeUpAction = dfplayer_ns.class_("VolumeUpAction", automation.Action)
|
||||
VolumeDownAction = dfplayer_ns.class_("VolumeDownAction", automation.Action)
|
||||
SetEqAction = dfplayer_ns.class_("SetEqAction", automation.Action)
|
||||
SleepAction = dfplayer_ns.class_("SleepAction", automation.Action)
|
||||
ResetAction = dfplayer_ns.class_("ResetAction", automation.Action)
|
||||
StartAction = dfplayer_ns.class_("StartAction", automation.Action)
|
||||
PauseAction = dfplayer_ns.class_("PauseAction", automation.Action)
|
||||
StopAction = dfplayer_ns.class_("StopAction", automation.Action)
|
||||
RandomAction = dfplayer_ns.class_("RandomAction", automation.Action)
|
||||
SetDeviceAction = dfplayer_ns.class_("SetDeviceAction", automation.Action)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -84,41 +70,30 @@ async def to_code(config):
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"dfplayer.play_next",
|
||||
NextAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DFPlayer),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
DFPLAYER_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DFPlayer),
|
||||
}
|
||||
)
|
||||
async def dfplayer_next_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
|
||||
|
||||
for _name, _call in (
|
||||
("dfplayer.play_next", "next()"),
|
||||
("dfplayer.play_previous", "previous()"),
|
||||
("dfplayer.volume_up", "volume_up()"),
|
||||
("dfplayer.volume_down", "volume_down()"),
|
||||
("dfplayer.sleep", "sleep()"),
|
||||
("dfplayer.reset", "reset()"),
|
||||
("dfplayer.start", "start()"),
|
||||
("dfplayer.pause", "pause()"),
|
||||
("dfplayer.stop", "stop()"),
|
||||
("dfplayer.random", "random()"),
|
||||
):
|
||||
automation.register_apply_action(
|
||||
_name, DFPLAYER_ACTION_SCHEMA, automation.ApplyCall(_call)
|
||||
)
|
||||
|
||||
@automation.register_action(
|
||||
"dfplayer.play_previous",
|
||||
PreviousAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DFPlayer),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def dfplayer_previous_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(
|
||||
"dfplayer.play_mp3",
|
||||
PlayMp3Action,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DFPlayer),
|
||||
@@ -126,70 +101,43 @@ async def dfplayer_previous_to_code(config, action_id, template_arg, args):
|
||||
},
|
||||
key=CONF_FILE,
|
||||
),
|
||||
synchronous=True,
|
||||
automation.ApplyField(CONF_FILE, "play_mp3", cg.uint16),
|
||||
)
|
||||
async def dfplayer_play_mp3_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_FILE], args, cg.uint16)
|
||||
cg.add(var.set_file(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
# loop and file default to what the old action's unset templatable values evaluated to
|
||||
automation.register_apply_action(
|
||||
"dfplayer.play",
|
||||
PlayFileAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DFPlayer),
|
||||
cv.Required(CONF_FILE): cv.templatable(cv.int_),
|
||||
cv.Optional(CONF_LOOP): cv.templatable(cv.boolean),
|
||||
cv.Optional(CONF_LOOP, default=False): cv.templatable(cv.boolean),
|
||||
},
|
||||
key=CONF_FILE,
|
||||
),
|
||||
synchronous=True,
|
||||
automation.ApplyCall(
|
||||
"play_file({}, {})", ((CONF_FILE, cg.uint16), (CONF_LOOP, cg.bool_))
|
||||
),
|
||||
)
|
||||
async def dfplayer_play_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_FILE], args, cg.uint16)
|
||||
cg.add(var.set_file(template_))
|
||||
if CONF_LOOP in config:
|
||||
template_ = await cg.templatable(config[CONF_LOOP], args, cg.bool_)
|
||||
cg.add(var.set_loop(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"dfplayer.play_folder",
|
||||
PlayFolderAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DFPlayer),
|
||||
cv.Required(CONF_FOLDER): cv.templatable(cv.int_),
|
||||
cv.Optional(CONF_FILE): cv.templatable(cv.int_),
|
||||
cv.Optional(CONF_LOOP): cv.templatable(cv.boolean),
|
||||
cv.Optional(CONF_FILE, default=0): cv.templatable(cv.int_),
|
||||
cv.Optional(CONF_LOOP, default=False): cv.templatable(cv.boolean),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
automation.ApplyCall(
|
||||
"play_folder({}, {}, {})",
|
||||
((CONF_FOLDER, cg.uint16), (CONF_FILE, cg.uint16), (CONF_LOOP, cg.bool_)),
|
||||
),
|
||||
)
|
||||
async def dfplayer_play_folder_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_FOLDER], args, cg.uint16)
|
||||
cg.add(var.set_folder(template_))
|
||||
if CONF_FILE in config:
|
||||
template_ = await cg.templatable(config[CONF_FILE], args, cg.uint16)
|
||||
cg.add(var.set_file(template_))
|
||||
if CONF_LOOP in config:
|
||||
template_ = await cg.templatable(config[CONF_LOOP], args, cg.bool_)
|
||||
cg.add(var.set_loop(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"dfplayer.set_device",
|
||||
SetDeviceAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DFPlayer),
|
||||
@@ -197,19 +145,11 @@ async def dfplayer_play_folder_to_code(config, action_id, template_arg, args):
|
||||
},
|
||||
key=CONF_DEVICE,
|
||||
),
|
||||
synchronous=True,
|
||||
automation.ApplyField(CONF_DEVICE, "set_device", Device),
|
||||
)
|
||||
async def dfplayer_set_device_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_DEVICE], args, Device)
|
||||
cg.add(var.set_device(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"dfplayer.set_volume",
|
||||
SetVolumeAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DFPlayer),
|
||||
@@ -217,51 +157,11 @@ async def dfplayer_set_device_to_code(config, action_id, template_arg, args):
|
||||
},
|
||||
key=CONF_VOLUME,
|
||||
),
|
||||
synchronous=True,
|
||||
automation.ApplyField(CONF_VOLUME, "set_volume", cg.uint8),
|
||||
)
|
||||
async def dfplayer_set_volume_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_VOLUME], args, cg.uint8)
|
||||
cg.add(var.set_volume(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"dfplayer.volume_up",
|
||||
VolumeUpAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DFPlayer),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def dfplayer_volume_up_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(
|
||||
"dfplayer.volume_down",
|
||||
VolumeDownAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DFPlayer),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def dfplayer_volume_down_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(
|
||||
"dfplayer.set_eq",
|
||||
SetEqAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DFPlayer),
|
||||
@@ -269,110 +169,34 @@ async def dfplayer_volume_down_to_code(config, action_id, template_arg, args):
|
||||
},
|
||||
key=CONF_EQ_PRESET,
|
||||
),
|
||||
synchronous=True,
|
||||
automation.ApplyField(CONF_EQ_PRESET, "set_eq", EqPreset),
|
||||
)
|
||||
async def dfplayer_set_eq_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_EQ_PRESET], args, EqPreset)
|
||||
cg.add(var.set_eq(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"dfplayer.sleep",
|
||||
SleepAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DFPlayer),
|
||||
}
|
||||
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,
|
||||
),
|
||||
),
|
||||
synchronous=True,
|
||||
automation.ApplyField(CONF_ENABLE, "set_current_track_repeat", cg.bool_),
|
||||
)
|
||||
async def dfplayer_sleep_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(
|
||||
"dfplayer.reset",
|
||||
ResetAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DFPlayer),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def dfplayer_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
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"dfplayer.start",
|
||||
StartAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DFPlayer),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def dfplayer_start_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(
|
||||
"dfplayer.pause",
|
||||
PauseAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DFPlayer),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def dfplayer_pause_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(
|
||||
"dfplayer.stop",
|
||||
StopAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DFPlayer),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def dfplayer_stop_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(
|
||||
"dfplayer.random",
|
||||
RandomAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DFPlayer),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def dfplayer_random_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_condition(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -33,8 +33,13 @@ class DFPlayer final : public uart::UARTDevice, public Component {
|
||||
void play_mp3(uint16_t file);
|
||||
void play_file(uint16_t file);
|
||||
void play_file_loop(uint16_t file);
|
||||
void play_file(uint16_t file, bool loop) { loop ? this->play_file_loop(file) : this->play_file(file); }
|
||||
void play_folder(uint16_t folder, uint16_t file);
|
||||
void play_folder_loop(uint16_t folder);
|
||||
// The loop command plays the whole folder, so file is ignored when loop is set.
|
||||
void play_folder(uint16_t folder, uint16_t file, bool loop) {
|
||||
loop ? this->play_folder_loop(folder) : this->play_folder(folder, file);
|
||||
}
|
||||
void volume_up();
|
||||
void volume_down();
|
||||
void set_device(Device device);
|
||||
@@ -46,6 +51,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;
|
||||
@@ -71,100 +77,6 @@ class DFPlayer final : public uart::UARTDevice, public Component {
|
||||
CallbackManager<void()> on_finished_playback_callback_;
|
||||
};
|
||||
|
||||
#define DFPLAYER_SIMPLE_ACTION(ACTION_CLASS, ACTION_METHOD) \
|
||||
template<typename... Ts> \
|
||||
class ACTION_CLASS : /* NOLINT */ \
|
||||
public Action<Ts...>, \
|
||||
public Parented<DFPlayer> { \
|
||||
void play(const Ts &...x) override { this->parent_->ACTION_METHOD(); } \
|
||||
};
|
||||
|
||||
DFPLAYER_SIMPLE_ACTION(NextAction, next)
|
||||
DFPLAYER_SIMPLE_ACTION(PreviousAction, previous)
|
||||
|
||||
template<typename... Ts> class PlayMp3Action final : public Action<Ts...>, public Parented<DFPlayer> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint16_t, file)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto file = this->file_.value(x...);
|
||||
this->parent_->play_mp3(file);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class PlayFileAction final : public Action<Ts...>, public Parented<DFPlayer> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint16_t, file)
|
||||
TEMPLATABLE_VALUE(bool, loop)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto file = this->file_.value(x...);
|
||||
auto loop = this->loop_.value(x...);
|
||||
if (loop) {
|
||||
this->parent_->play_file_loop(file);
|
||||
} else {
|
||||
this->parent_->play_file(file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class PlayFolderAction final : public Action<Ts...>, public Parented<DFPlayer> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint16_t, folder)
|
||||
TEMPLATABLE_VALUE(uint16_t, file)
|
||||
TEMPLATABLE_VALUE(bool, loop)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto folder = this->folder_.value(x...);
|
||||
auto file = this->file_.value(x...);
|
||||
auto loop = this->loop_.value(x...);
|
||||
if (loop) {
|
||||
this->parent_->play_folder_loop(folder);
|
||||
} else {
|
||||
this->parent_->play_folder(folder, file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetDeviceAction final : public Action<Ts...>, public Parented<DFPlayer> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(Device, device)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto device = this->device_.value(x...);
|
||||
this->parent_->set_device(device);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetVolumeAction final : public Action<Ts...>, public Parented<DFPlayer> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint8_t, volume)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto volume = this->volume_.value(x...);
|
||||
this->parent_->set_volume(volume);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetEqAction final : public Action<Ts...>, public Parented<DFPlayer> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(EqPreset, eq)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto eq = this->eq_.value(x...);
|
||||
this->parent_->set_eq(eq);
|
||||
}
|
||||
};
|
||||
|
||||
DFPLAYER_SIMPLE_ACTION(SleepAction, sleep)
|
||||
DFPLAYER_SIMPLE_ACTION(ResetAction, reset)
|
||||
DFPLAYER_SIMPLE_ACTION(StartAction, start)
|
||||
DFPLAYER_SIMPLE_ACTION(PauseAction, pause)
|
||||
DFPLAYER_SIMPLE_ACTION(StopAction, stop)
|
||||
DFPLAYER_SIMPLE_ACTION(RandomAction, random)
|
||||
DFPLAYER_SIMPLE_ACTION(VolumeUpAction, volume_up)
|
||||
DFPLAYER_SIMPLE_ACTION(VolumeDownAction, volume_down)
|
||||
|
||||
template<typename... Ts> class DFPlayerIsPlayingCondition final : public Condition<Ts...>, public Parented<DFPlayer> {
|
||||
public:
|
||||
bool check(const Ts &...x) override { return this->parent_->is_playing(); }
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -3,7 +3,7 @@ import encodings
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32_ble
|
||||
from esphome.components.const import CONF_MANUFACTURER
|
||||
from esphome.components.const import CONF_DESCRIPTION, CONF_MANUFACTURER
|
||||
from esphome.components.esp32 import request_bluetooth
|
||||
from esphome.components.esp32_ble import BTLoggers, bt_uuid
|
||||
import esphome.config_validation as cv
|
||||
@@ -37,7 +37,6 @@ CONF_ADVERTISE = "advertise"
|
||||
CONF_APPEARANCE = "appearance"
|
||||
CONF_BROADCAST = "broadcast"
|
||||
CONF_CHARACTERISTICS = "characteristics"
|
||||
CONF_DESCRIPTION = "description"
|
||||
CONF_DESCRIPTORS = "descriptors"
|
||||
CONF_ENDIANNESS = "endianness"
|
||||
CONF_FIRMWARE_VERSION = "firmware_version"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -467,6 +467,8 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
if (this->extended_proto_()) {
|
||||
// Read ota type, 1 byte
|
||||
if (!this->data_readall_(buf, 1)) {
|
||||
if (this->client_left_before_start_())
|
||||
return;
|
||||
this->log_read_error_(LOG_STR("OTA type"));
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
@@ -476,6 +478,9 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
|
||||
// Read size, 4 bytes MSB first
|
||||
if (!this->data_readall_(buf, 4)) {
|
||||
// The first request byte is the type on the extended protocol; a close after it was a cut-off request
|
||||
if (!this->extended_proto_() && this->client_left_before_start_())
|
||||
return;
|
||||
this->log_read_error_(LOG_STR("size"));
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
@@ -542,6 +547,8 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
// there is no would-block retry here and failures are already logged.
|
||||
read = this->noise_read_data_(buf, requested);
|
||||
if (read <= 0) {
|
||||
if (this->remote_closed_)
|
||||
this->log_remote_closed_(LOG_STR("data"));
|
||||
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
@@ -666,7 +673,11 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) {
|
||||
return false;
|
||||
}
|
||||
} else if (read == 0) {
|
||||
ESP_LOGW(TAG, "Remote closed");
|
||||
// A partial message is a cut-off request, not a clean close; the caller reports the clean one
|
||||
this->remote_closed_ = at == 0;
|
||||
if (at > 0) {
|
||||
ESP_LOGW(TAG, "Remote closed after %u of %zu bytes", (unsigned) at, len);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
at += read;
|
||||
@@ -712,7 +723,22 @@ void ESPHomeOTAComponent::log_socket_error_(const LogString *msg) {
|
||||
ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
|
||||
}
|
||||
|
||||
void ESPHomeOTAComponent::log_read_error_(const LogString *what) { ESP_LOGW(TAG, "Read %s failed", LOG_STR_ARG(what)); }
|
||||
bool ESPHomeOTAComponent::client_left_before_start_() {
|
||||
// Key probes and scanners hang up right after the handshake; nothing started, so no error status or callback
|
||||
if (!this->remote_closed_)
|
||||
return false;
|
||||
ESP_LOGD(TAG, "Client left after the handshake");
|
||||
this->cleanup_connection_();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ESPHomeOTAComponent::log_read_error_(const LogString *what) {
|
||||
if (this->remote_closed_) {
|
||||
this->log_remote_closed_(what);
|
||||
return;
|
||||
}
|
||||
ESP_LOGW(TAG, "Read %s failed", LOG_STR_ARG(what));
|
||||
}
|
||||
|
||||
void ESPHomeOTAComponent::log_start_(const LogString *phase) {
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
@@ -793,6 +819,7 @@ void ESPHomeOTAComponent::cleanup_connection_() {
|
||||
this->handshake_buf_pos_ = 0;
|
||||
this->ota_state_ = OTAState::IDLE;
|
||||
this->ota_features_ = 0;
|
||||
this->remote_closed_ = false;
|
||||
this->backend_ = nullptr;
|
||||
#ifdef USE_OTA_PASSWORD
|
||||
this->cleanup_auth_();
|
||||
|
||||
@@ -134,6 +134,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
void server_failed_(const LogString *msg);
|
||||
void log_socket_error_(const LogString *msg);
|
||||
void log_read_error_(const LogString *what);
|
||||
bool client_left_before_start_();
|
||||
void log_start_(const LogString *phase);
|
||||
void log_remote_closed_(const LogString *during);
|
||||
void cleanup_connection_();
|
||||
@@ -186,6 +187,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
OTAState ota_state_{OTAState::IDLE};
|
||||
uint8_t handshake_buf_pos_{0};
|
||||
uint8_t ota_features_{0};
|
||||
bool remote_closed_{false}; // the peer hung up cleanly during a blocking read
|
||||
#ifdef USE_OTA_PASSWORD
|
||||
uint8_t auth_buf_pos_{0};
|
||||
uint8_t auth_type_{0}; // Store auth type to know which hasher to use
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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,18 @@ TRIGGER_EVENT_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"event.trigger", TriggerEventAction, TRIGGER_EVENT_SCHEMA, synchronous=True
|
||||
def _event_type_literal(config: ConfigType, value: str) -> str:
|
||||
"""A constant event type is a plain literal; trigger() only compares it, so no copy is needed."""
|
||||
return str(cg.safe_exp(value))
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
"event.trigger",
|
||||
TRIGGER_EVENT_SCHEMA,
|
||||
automation.ApplyField(
|
||||
CONF_EVENT_TYPE, "trigger", cg.std_string, _event_type_literal
|
||||
),
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/event/event.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
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) {
|
||||
event->add_on_event_callback([this](StringRef event_type) { this->trigger(event_type); });
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::event
|
||||
@@ -7,17 +7,17 @@ namespace esphome::event {
|
||||
|
||||
static const char *const TAG = "event";
|
||||
|
||||
void Event::trigger(const std::string &event_type) {
|
||||
void Event::trigger(const char *event_type) {
|
||||
// Linear search with strcmp - faster than std::set for small datasets (1-5 items typical)
|
||||
const char *found = nullptr;
|
||||
for (const char *type : this->types_) {
|
||||
if (strcmp(type, event_type.c_str()) == 0) {
|
||||
if (strcmp(type, event_type) == 0) {
|
||||
found = type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found == nullptr) {
|
||||
ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str());
|
||||
ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type);
|
||||
return;
|
||||
}
|
||||
this->last_event_type_ = found;
|
||||
@@ -36,12 +36,4 @@ void Event::set_event_types(const FixedVector<const char *> &event_types) {
|
||||
this->last_event_type_ = nullptr; // Reset when types change
|
||||
}
|
||||
|
||||
void Event::set_event_types(const std::vector<const char *> &event_types) {
|
||||
this->types_.init(event_types.size());
|
||||
for (const char *type : event_types) {
|
||||
this->types_.push_back(type);
|
||||
}
|
||||
this->last_event_type_ = nullptr; // Reset when types change
|
||||
}
|
||||
|
||||
} // namespace esphome::event
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
@@ -21,22 +20,21 @@ namespace esphome::event {
|
||||
|
||||
class Event : public EntityBase {
|
||||
public:
|
||||
void trigger(const std::string &event_type);
|
||||
/// Trigger an event; the type is matched against the configured types by string compare.
|
||||
void trigger(const char *event_type);
|
||||
void trigger(const std::string &event_type) { this->trigger(event_type.c_str()); }
|
||||
|
||||
/// Set the event types supported by this event (from initializer list).
|
||||
/// Set the event types supported by this event; called by generated code with string literals.
|
||||
void set_event_types(std::initializer_list<const char *> event_types) {
|
||||
this->types_ = event_types;
|
||||
this->last_event_type_ = nullptr; // Reset when types change
|
||||
}
|
||||
/// Set the event types supported by this event (from FixedVector).
|
||||
/// Copy the event types of another event, for components that wrap one.
|
||||
void set_event_types(const FixedVector<const char *> &event_types);
|
||||
/// Set the event types supported by this event (from vector).
|
||||
void set_event_types(const std::vector<const char *> &event_types);
|
||||
|
||||
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
|
||||
void set_event_types(std::initializer_list<std::string> event_types) = delete;
|
||||
void set_event_types(const FixedVector<std::string> &event_types) = delete;
|
||||
void set_event_types(const std::vector<std::string> &event_types) = delete;
|
||||
|
||||
/// Return the event types supported by this event.
|
||||
const FixedVector<const char *> &get_event_types() const { return this->types_; }
|
||||
|
||||
@@ -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)
|
||||
@@ -86,9 +83,6 @@ FanPresetSetTrigger = fan_ns.class_(
|
||||
"FanPresetSetTrigger", automation.Trigger.template(cg.StringRef)
|
||||
)
|
||||
|
||||
FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template())
|
||||
FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template())
|
||||
|
||||
_FAN_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
|
||||
@@ -332,9 +326,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 +338,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(
|
||||
@@ -410,27 +364,16 @@ async def fan_cycle_speed_to_code(config, action_id, template_arg, args):
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"fan.is_on",
|
||||
FanIsOnCondition,
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(Fan),
|
||||
}
|
||||
),
|
||||
FAN_CONDITION_SCHEMA = automation.maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(Fan),
|
||||
}
|
||||
)
|
||||
@automation.register_condition(
|
||||
"fan.is_off",
|
||||
FanIsOffCondition,
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(Fan),
|
||||
}
|
||||
),
|
||||
|
||||
automation.register_apply_condition("fan.is_on", FAN_CONDITION_SCHEMA, "state")
|
||||
automation.register_apply_condition(
|
||||
"fan.is_off", FAN_CONDITION_SCHEMA, "state == false"
|
||||
)
|
||||
async def fan_is_on_off_to_code(config, condition_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
|
||||
@@ -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) {}
|
||||
@@ -95,23 +68,6 @@ template<typename... Ts> class CycleSpeedAction final : public Action<Ts...> {
|
||||
Fan *state_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class FanIsOnCondition final : public Condition<Ts...> {
|
||||
public:
|
||||
explicit FanIsOnCondition(Fan *state) : state_(state) {}
|
||||
bool check(const Ts &...x) override { return this->state_->state; }
|
||||
|
||||
protected:
|
||||
Fan *state_;
|
||||
};
|
||||
template<typename... Ts> class FanIsOffCondition final : public Condition<Ts...> {
|
||||
public:
|
||||
explicit FanIsOffCondition(Fan *state) : state_(state) {}
|
||||
bool check(const Ts &...x) override { return !this->state_->state; }
|
||||
|
||||
protected:
|
||||
Fan *state_;
|
||||
};
|
||||
|
||||
class FanStateTrigger final : public Trigger<Fan *> {
|
||||
public:
|
||||
FanStateTrigger(Fan *state) : fan_(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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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_();
|
||||
|
||||
@@ -22,6 +22,7 @@ from esphome.const import (
|
||||
UNIT_AMPERE,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_HERTZ,
|
||||
UNIT_KILOWATT_HOURS,
|
||||
UNIT_VOLT,
|
||||
UNIT_WATT,
|
||||
)
|
||||
@@ -33,7 +34,6 @@ CONF_TOTAL_GENERATION_TIME = "total_generation_time"
|
||||
CONF_TODAY_GENERATION_TIME = "today_generation_time"
|
||||
CONF_PV1 = "pv1"
|
||||
CONF_PV2 = "pv2"
|
||||
UNIT_KILOWATT_HOURS = "kWh"
|
||||
UNIT_HOURS = "h"
|
||||
UNIT_KOHM = "kΩ"
|
||||
UNIT_MILLIAMPERE = "mA"
|
||||
|
||||
@@ -23,6 +23,7 @@ from esphome.const import (
|
||||
UNIT_AMPERE,
|
||||
UNIT_DEGREES,
|
||||
UNIT_HERTZ,
|
||||
UNIT_KILOWATT_HOURS,
|
||||
UNIT_MINUTE,
|
||||
UNIT_VOLT,
|
||||
UNIT_VOLT_AMPS_REACTIVE,
|
||||
@@ -36,7 +37,6 @@ CONF_TOTAL_GENERATION_TIME = "total_generation_time"
|
||||
CONF_TODAY_GENERATION_TIME = "today_generation_time"
|
||||
CONF_PV1 = "pv1"
|
||||
CONF_PV2 = "pv2"
|
||||
UNIT_KILOWATT_HOURS = "kWh"
|
||||
UNIT_HOURS = "h"
|
||||
UNIT_KOHM = "kΩ"
|
||||
UNIT_MILLIAMPERE = "mA"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -24,6 +24,7 @@ from esphome.const import (
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_AMPERE,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_MILLIVOLT,
|
||||
UNIT_VOLT,
|
||||
UNIT_WATT,
|
||||
UNIT_WATT_HOURS,
|
||||
@@ -44,7 +45,6 @@ CONF_TEMPERATURE_COEFFICIENT = "temperature_coefficient"
|
||||
CONF_RESET_ON_BOOT = "reset_on_boot"
|
||||
UNIT_COULOMB = "C"
|
||||
UNIT_JOULE = "J"
|
||||
UNIT_MILLIVOLT = "mV"
|
||||
|
||||
ina2xx_base_ns = cg.esphome_ns.namespace("ina2xx_base")
|
||||
INA2XX = ina2xx_base_ns.class_("INA2XX", cg.PollingComponent)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user