Merge remote-tracking branch 'origin/dev' into esp8266-native-ninja-emission

# Conflicts:
#	esphome/build_gen/arduino8266.py
#	tests/unit_tests/build_gen/test_arduino8266.py
This commit is contained in:
J. Nick Koston
2026-09-23 22:54:13 +01:00
178 changed files with 3726 additions and 3206 deletions
+116
View File
@@ -0,0 +1,116 @@
---
name: code-review
description: Review guidance for ESPHome pull requests. Use this when reviewing a pull request that changes ESPHome Python, C++, or component code, to check it against the project's coding conventions, embedded-systems memory rules, testing requirements, and breaking-change policy.
---
# Reviewing ESPHome pull requests
ESPHome parses YAML into C++ firmware for memory-constrained microcontrollers
(ESP32, ESP8266, RP2040, LibreTiny). Review changes with that in mind: RAM and
flash are scarce, and code runs unattended for months.
`AGENTS.md` in the repository root is the full contributor guide and the
authority when it disagrees with this summary. The developer documentation at
https://developers.esphome.io explains the component lifecycle and the reasoning
behind these rules. This skill lists the concrete things worth flagging in a
review; read `AGENTS.md` for the detail behind any item.
Only raise findings that the diff actually introduces or changes. Do not ask for
drive-by cleanup of pre-existing code the PR did not touch.
## Memory and embedded constraints (highest value)
Heap allocation after `setup()` is treated as a reliability bug, not a
performance nit, because it fragments a small shared heap. Flag:
- New heap allocation on a hot path or after setup that could be avoided.
- `std::vector` where the size is known at compile time (use `std::array`, or
`StaticVector<T, N>` when a `push_back` API is needed) or fixed at runtime
init (use `FixedVector<T>`).
- Listener / child-entity registration lists stored as `std::vector`; these have
a compile-time-known count and should use `cg.slot_counter()` plus
`StaticVector`.
- `std::vector<uint8_t>` for a byte buffer that never grows: prefer
`std::unique_ptr<uint8_t[]>` or `std::array`.
- `std::map` / `std::set` / `std::unordered_map` for small datasets (1-16
elements): a `std::vector` of a small struct with linear search is lighter.
- `std::deque` anywhere: it allocates 512-byte blocks and should be avoided.
- `std::string` storing a value set once from config: prefer `StringRef` (the
literal already lives in flash).
- `std::string` / `std::to_string` / string-returning helpers on hot paths where
a buffer or view API exists.
## C++ conventions
- Include what you use: a file referencing a symbol must include the header that
declares it, even if it currently arrives transitively. New or changed uses of
a symbol need the matching include.
- Prefix all member access with `this->`.
- Naming: `lower_snake_case` for functions/methods/variables, `UpperCamelCase`
for classes/structs/enums, `UPPER_SNAKE_CASE` for namespace-scope constants,
trailing underscore on protected/private fields.
- `enum class` values must be prefixed with the enum name in `UPPER_SNAKE_CASE`
(e.g. `UARTFlushResult::UART_FLUSH_RESULT_SUCCESS`). Bare names like `SUCCESS`,
`FAIL`, or `OK` collide with SDK macros on some platforms and break the build.
- Prefer `const`/`enum` over `#define`; `#define` is only for conditional
compilation and code-generation sizes.
- Never call `millis()` in a `loop()` body; use
`App.get_loop_component_start_time()`. A rate-limit gate below ~16 ms (the loop
period) does nothing.
- Pick the timing primitive by cadence: gated `loop()` under 250 ms,
`set_interval` at 500 ms and above.
- Do not override a base method to return the value it already returns (e.g.
`get_setup_priority()` returning `setup_priority::DATA`).
- Wrap string literals passed as printf `%s` args in `LOG_STR_LITERAL()`.
- Required, invariant dependencies should be constructor parameters, not setters.
- Callback registration methods must be templated (`template<typename F>`), not
typed as `std::function`, so lightweight forwarders avoid a heap allocation.
- Two-space indent, `using` over `typedef`, wrap at 120 columns.
## Python conventions
- Type-annotate every new function signature (params and return), new dataclass
fields, and new module-level variables. Import `ConfigType` from
`esphome.types`.
- Use the walrus operator to avoid a double lookup, e.g.
`if (blah := config.get(CONF_BLAH)) is not None:`.
- Reuse existing validators from `config_validation.py` (`cv.rename_key`,
`cv.has_exactly_one_key`, etc.) via `cv.All(...)` instead of hand-rolling.
- `esphome/const.py` is frozen: no new `CONF_` constants there. Define them in
the component's own `.py`, or in `esphome/components/const/__init__.py` when
shared. The same constant defined in three or more component files fails CI.
- State that must persist during code generation goes in `CORE.data` namespaced
under the component `DOMAIN` (a `@dataclass`), not module-level mutable globals.
- Prefer callback-based triggers via `build_callback_automation()`; only use a
`Trigger<Ts...>` subclass when the forwarder needs mutable state.
## Testing and coverage
- New and changed lines and branches need test coverage, including defensive
early-returns, error paths, and no-op guards. A mocked-out function is not
covered; exercise the real call path too.
- Component YAML tests live in `tests/components/<component>/`. Never define
buses (uart, i2c, spi, modbus) directly in a test file: pull them from
`tests/test_build_components/common/` through dict-style `packages:` so CI can
group builds. List-style packages or top-level merge keys block grouping.
- Config-only checks use the `validate.*.yaml` prefix; compiled checks use
`test.*.yaml`.
## Breaking changes and public API
- Base classes under `esphome/core/` and documented config options are public
API. Undocumented `public` members of a component are internal.
- A breaking change needs justification, a migration path in the PR description,
and a deprecation window where feasible (`ESPDEPRECATED` in C++,
`cv.rename_key(..., removed_in=...)` in Python). Changing a codegen-injected
lambda signature is not a breaking change.
## Process and PR hygiene
- PR titles start with a `[tag]` prefix: the component name (e.g. `[uart] ...`)
or `[core]` for shared code.
- Prose in docs, comments, and commit messages should be plain English. Keep
inline comments short and only where the code is not self-explanatory; do not
restate what the code says.
- Verify the PR fills out `.github/PULL_REQUEST_TEMPLATE.md` and adds
`CODEOWNERS` entries for a new component.
-3
View File
@@ -31,7 +31,4 @@ RUN \
platformio settings set enable_telemetry No \
&& platformio settings set check_platformio_interval 1000000
COPY script/platformio_install_deps.py platformio.ini ./
RUN ./platformio_install_deps.py platformio.ini --libraries --platforms --tools
WORKDIR /workspaces
+2 -2
View File
@@ -42,7 +42,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
@@ -67,7 +67,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
+2 -4
View File
@@ -12,9 +12,7 @@ on:
- ".github/workflows/ci-docker.yml"
- "requirements*.txt"
- "pyproject.toml"
- "platformio.ini"
- "esphome/idf_component.yml"
- "script/platformio_install_deps.py"
# Core, build pipeline, toolchain, and target-platform changes can change
# how a toolchain is set up or built, so re-run the per-toolchain compile
# smoke test when they change.
@@ -67,7 +65,7 @@ jobs:
with:
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- name: Determine tag and whether to push
id: tag
@@ -159,7 +157,7 @@ jobs:
with:
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- name: Log in to the GitHub container registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
+2 -2
View File
@@ -321,7 +321,7 @@ jobs:
. venv/bin/activate
pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.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@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
run_command: empty-upload
force: true
+2 -2
View File
@@ -56,7 +56,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -84,6 +84,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -14,4 +14,4 @@ jobs:
permissions:
issues: write # issues.lock on closed issues
pull-requests: write # issues.lock on closed pull requests
uses: esphome/workflows/.github/workflows/lock.yml@0fdd5e311b7e744069166696072a1a9cbc5fbeb6 # 2026.8.1
uses: esphome/workflows/.github/workflows/lock.yml@cc3e76de337dc59bc1cba8da58d963cd23b873f1 # 2026.9.0
+2 -2
View File
@@ -123,7 +123,7 @@ jobs:
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- name: Log in to docker hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
@@ -202,7 +202,7 @@ jobs:
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
# No GITHUB_TOKEN permissions: the reusable workflow mints an ESPHome
# GitHub App token so the labels, comments and closures come from
# esphome[bot] instead of github-actions[bot].
uses: esphome/workflows/.github/workflows/stale.yml@a1c1485ab46ef41a84a6a9d8abd7fa4b7628fd70 # main
uses: esphome/workflows/.github/workflows/stale.yml@cc3e76de337dc59bc1cba8da58d963cd23b873f1 # main
secrets:
ESPHOME_GITHUB_APP_PRIVATE_KEY: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
with:
+1 -1
View File
@@ -10,7 +10,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.16.7
rev: v0.16.8
hooks:
# Run the linter.
- id: ruff
+14
View File
@@ -443,6 +443,20 @@ file does, and it is the authority when they disagree. The most useful starting
Use `synchronous=True` for actions that run to completion inside `play()` without deferring. Use `synchronous=False` if the action may suspend/defer execution (e.g. `delay`, `wait_until`, `script.wait`) or store trigger arguments for later use.
**Actions that only forward templatable values to their parent need no C++ class.** Register them
with `register_apply_action`; do not write a `TEMPLATABLE_VALUE` class or a builder for this shape.
```python
automation.register_apply_action(
"my_component.set_gains",
schema,
automation.ApplyField(CONF_KP, "set_kp", cg.float_),
automation.ApplyField(CONF_KI, "set_ki", cg.float_),
)
```
The `ApplyField`, `ApplyCall` and `register_apply_action` docstrings in `esphome/automation.py` cover
the rest; `cover.control` and `cover.template.publish` are in-tree examples. `TEMPLATABLE_VALUE` with
`cg.templatable` stays for actions whose `play()` has real logic beyond forwarding values.
* **Conditions:**
```cpp
template<typename... Ts> class MyCondition : public Condition<Ts...> {
+1
View File
@@ -457,6 +457,7 @@ esphome/components/rtl87xx/* @kuba2k2
esphome/components/rtttl/* @glmnet @ximex
esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt
esphome/components/runtime_stats/* @bdraco
esphome/components/rx8025t/* @remcom
esphome/components/rx8130/* @beormund
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
esphome/components/scd4x/* @martgras @sjtrny
+1 -4
View File
@@ -22,16 +22,13 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.9
RUN uv pip install --no-cache-dir esphome-device-builder==1.15.0
RUN \
platformio settings set enable_telemetry No \
&& platformio settings set check_platformio_interval 1000000 \
&& mkdir -p /piolibs
COPY script/platformio_install_deps.py platformio.ini /
RUN /platformio_install_deps.py /platformio.ini --libraries
ARG BUILD_VERSION
LABEL \
+12
View File
@@ -5,3 +5,15 @@ bk72xx:
board: generic-bk7231n-qfn32-tuya
logger:
wifi:
ssid: MySSID
password: password1
ap:
# mqtt and captive_portal together pull in AsyncTCP and ESPAsyncWebServer;
# a stray ESP32 AsyncTCP copy on the library search path breaks this build
captive_portal:
mqtt:
broker: 192.168.178.84
+191 -2
View File
@@ -1,5 +1,8 @@
from collections.abc import Callable
from dataclasses import dataclass, field
import logging
import string
from typing import Any
import esphome.codegen as cg
import esphome.config_validation as cv
@@ -18,15 +21,17 @@ from esphome.const import (
CONF_TYPE_ID,
CONF_UPDATE_INTERVAL,
)
from esphome.core import ID, Lambda
from esphome.core import CORE, ID, EsphomeError, Lambda
from esphome.cpp_generator import (
FlashStringLiteral,
LambdaExpression,
MockObj,
MockObjClass,
TemplateArgsType,
call_lambda,
)
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType
from esphome.types import ConfigType, SafeExpType
from esphome.util import Registry
@@ -57,6 +62,7 @@ def maybe_conf(conf, *validators):
with cv.remove_prepend_path([conf]):
return validator({conf: value})
validate.inner_schema = validator
return validate
@@ -207,6 +213,189 @@ 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)
def flash_string(config: ConfigType, value: str) -> str:
"""Default renderer for ``std::string`` constants; copies the literal out of flash on ESP8266."""
if CORE.is_esp8266:
return f"progmem_string({FlashStringLiteral(value)})"
return str(cg.safe_exp(value))
@dataclass(frozen=True)
class ApplyCall:
"""One statement from config keys, e.g. ``"set_range({}, {})"`` with ``((CONF_LOW, cg.float_), ...)``.
Each arg is ``(conf_key, type_)`` or ``(conf_key, type_, const_fn)``. A ``conf_key`` may be a
path into nested sections. A plain ``str`` ``type_`` is raw C++ type text and may use
``{parent}``. ``const_fn(config, value)`` renders a constant's argument text; a lambda bypasses
it. The statement is skipped when none of its keys is set, always emitted when it has no
keys, and a partial set is a config error.
"""
target: str
args: tuple[tuple[Any, ...], ...] = ()
def __post_init__(self) -> None:
fields = [
f for _, f, _, _ in string.Formatter().parse(self.target) if f is not None
]
if any(fields):
raise ValueError(
f"apply target {self.target!r}: only bare {{}} placeholders"
)
if len(fields) != len(self.args):
raise ValueError(
f"apply target {self.target!r} has {len(fields)} "
f"placeholder(s) for {len(self.args)} config key(s)"
)
if any(len(arg) not in (2, 3) for arg in self.args):
raise ValueError(
f"apply target {self.target!r}: each arg is (conf_key, type_[, const_fn])"
)
@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]]
def register_apply_action(
name: str,
schema: cv.Schema,
*fields: ApplyField | ApplyCall,
call: str | None = None,
) -> None:
"""Register an action that only forwards config values to its parent, with no C++ class.
Generates one stateless function for ``ApplyAction<Ts...>``: parent and constants are baked
in, lambdas are called inline with the trigger args. With ``call`` every statement targets
the call object ``auto apply_call = parent->call()``, and ``apply_call.perform()`` is appended.
"""
statements_spec = [
(
c.target,
[(arg[0], arg[1], arg[2] if len(arg) == 3 else None) for arg in c.args],
)
for c in (f if isinstance(f, ApplyCall) else f.call() for f in fields)
]
for _, members in statements_spec:
for conf_key, _, _ in members:
_check_key_in_schema(name, schema, conf_key)
async def builder(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
# Global-scope qualified so a trigger arg named like the id cannot shadow it.
parent = f"::{await cg.get_variable(config[CONF_ID])}"
# Must match ApplyAction::ApplyFn exactly for the function pointer conversion.
lambda_args = [
(cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), arg)
for t, arg in args
]
receiver = "apply_call." if call else f"{parent}->"
statements: list[str] = []
for target, members in statements_spec:
values = [_config_lookup(config, key) for key, _, _ in members]
if members and all(value is None for value in values):
continue
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_
)
exprs.append(str(call_lambda(inner)))
elif const_fn is not None:
exprs.append(const_fn(config, value))
elif type_ is cg.std_string:
exprs.append(flash_string(config, value))
else:
exprs.append(str(cg.safe_exp(value)))
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 validate_potentially_and_condition(value):
if isinstance(value, list):
+24 -2
View File
@@ -29,6 +29,29 @@ static const char *const TAG = "api";
// APIServer
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#ifdef USE_API_NOISE
static constexpr uint32_t NOISE_PSK_PREF_HASH = 88491486UL;
#endif
#if defined(USE_API_NOISE) && defined(USE_OTA_ENCRYPTION_PROVISIONED)
bool load_saved_noise_psk(noise::psk_t &out) {
SavedNoisePsk saved;
#ifdef USE_PREFERENCE_KEY_LOOKUP
const bool loaded =
global_preferences->load_from_key(NOISE_PSK_PREF_HASH, reinterpret_cast<uint8_t *>(&saved), sizeof(saved));
#else
// Slot backends need the reservation walk; it only lands on the record when the reservations before
// it match a normal boot, otherwise the type checked checksum fails the load
const bool loaded = global_preferences->make_preference<SavedNoisePsk>(NOISE_PSK_PREF_HASH, true).load(&saved);
#endif
// The all-zeros record means no key
if (!loaded || noise::NoiseContext::is_all_zeros(saved.psk))
return false;
out = saved.psk;
return true;
}
#endif
APIServer::APIServer() { global_api_server = this; }
void APIServer::socket_failed_(const LogString *msg) {
@@ -43,8 +66,7 @@ void APIServer::setup() {
#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
uint32_t hash = 88491486UL;
this->noise_pref_ = global_preferences->make_preference<SavedNoisePsk>(hash, true);
this->noise_pref_ = global_preferences->make_preference<SavedNoisePsk>(NOISE_PSK_PREF_HASH, true);
#ifndef USE_API_NOISE_PSK_FROM_YAML
// A cleared record loads fine but holds no key
if (this->load_and_apply_noise_psk_() && this->noise_ctx_.has_psk()) {
+5
View File
@@ -43,6 +43,11 @@ struct SavedNoisePsk {
noise::psk_t psk;
} PACKED; // NOLINT
#endif
#if defined(USE_API_NOISE) && defined(USE_OTA_ENCRYPTION_PROVISIONED)
/// One-shot read of the provisioned key for a boot without an api server (safe mode); false when
/// there is no key
bool load_saved_noise_psk(noise::psk_t &out);
#endif
class APIServer final : public Component,
public Controller
+28 -77
View File
@@ -4,8 +4,6 @@ import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import CONF_FREQUENCY, CONF_ID
from esphome.core import ID
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
CODEOWNERS = ["@X-Ryl669"]
@@ -80,30 +78,21 @@ async def to_code(config: ConfigType) -> None:
# Actions
AT581XResetAction = at581x_ns.class_("AT581XResetAction", automation.Action)
AT581XSettingsAction = at581x_ns.class_("AT581XSettingsAction", automation.Action)
@automation.register_action(
automation.register_apply_action(
"at581x.reset",
AT581XResetAction,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(AT581XComponent),
}
),
synchronous=True,
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(AT581XComponent)}),
automation.ApplyCall("reset_hardware_frontend()"),
)
async def at581x_reset_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
def _megahertz(value: float) -> int:
return int(value / 1000000)
def _microamps(value: float) -> int:
return int(value * 1000000)
RADAR_SETTINGS_SCHEMA = cv.Schema(
@@ -111,7 +100,7 @@ RADAR_SETTINGS_SCHEMA = cv.Schema(
cv.Required(CONF_ID): cv.use_id(AT581XComponent),
cv.Optional(CONF_HW_FRONTEND_RESET): cv.templatable(cv.boolean),
cv.Optional(CONF_FREQUENCY, default="5800MHz"): cv.templatable(
cv.All(cv.frequency, cv.one_of(*RADAR_ALLOWED_FREQ))
cv.All(cv.frequency, cv.one_of(*RADAR_ALLOWED_FREQ), _megahertz)
),
cv.Optional(CONF_SENSING_DISTANCE, default=823): cv.templatable(
cv.int_range(min=0, max=1023)
@@ -123,7 +112,7 @@ RADAR_SETTINGS_SCHEMA = cv.Schema(
)
),
cv.Optional(CONF_POWER_CONSUMPTION, default="70uA"): cv.templatable(
cv.All(cv.current, cv.one_of(*RADAR_ALLOWED_CUR_CONSUMPTION))
cv.All(cv.current, cv.one_of(*RADAR_ALLOWED_CUR_CONSUMPTION), _microamps)
),
cv.Optional(CONF_PROTECT_TIME, default="1000ms"): cv.templatable(
cv.All(
@@ -165,60 +154,22 @@ RADAR_SETTINGS_SCHEMA = cv.Schema(
)
@automation.register_action(
# i2c_write_config() must follow the setters: it flushes the staged values.
automation.register_apply_action(
"at581x.settings",
AT581XSettingsAction,
RADAR_SETTINGS_SCHEMA,
synchronous=True,
automation.ApplyField(CONF_FREQUENCY, "set_frequency", cg.int_),
automation.ApplyField(CONF_SENSING_DISTANCE, "set_sensing_distance", cg.int_),
automation.ApplyField(
CONF_POWERON_SELFCHECK_TIME, "set_poweron_selfcheck_time", cg.int_
),
automation.ApplyField(CONF_POWER_CONSUMPTION, "set_power_consumption", cg.int_),
automation.ApplyField(CONF_PROTECT_TIME, "set_protect_time", cg.int_),
automation.ApplyField(CONF_TRIGGER_BASE, "set_trigger_base", cg.int_),
automation.ApplyField(CONF_TRIGGER_KEEP, "set_trigger_keep", cg.int_),
automation.ApplyField(CONF_STAGE_GAIN, "set_stage_gain", cg.int_),
automation.ApplyCall("i2c_write_config()"),
automation.ApplyField(
CONF_HW_FRONTEND_RESET, "reset_hardware_frontend_if", cg.bool_
),
)
async def at581x_settings_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
# Radar configuration
if frontend_reset := config.get(CONF_HW_FRONTEND_RESET):
template_ = await cg.templatable(frontend_reset, args, cg.int8)
cg.add(var.set_hw_frontend_reset(template_))
if freq := config.get(CONF_FREQUENCY):
if not cg.is_template(freq):
freq = int(freq / 1000000)
template_ = await cg.templatable(freq, args, cg.int_)
cg.add(var.set_frequency(template_))
if (sens_dist := config.get(CONF_SENSING_DISTANCE)) is not None:
template_ = await cg.templatable(sens_dist, args, cg.int_)
cg.add(var.set_sensing_distance(template_))
if selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME):
template_ = await cg.templatable(selfcheck, args, cg.int_)
cg.add(var.set_poweron_selfcheck_time(template_))
if protect := config.get(CONF_PROTECT_TIME):
template_ = await cg.templatable(protect, args, cg.int_)
cg.add(var.set_protect_time(template_))
if trig_base := config.get(CONF_TRIGGER_BASE):
template_ = await cg.templatable(trig_base, args, cg.int_)
cg.add(var.set_trigger_base(template_))
if trig_keep := config.get(CONF_TRIGGER_KEEP):
template_ = await cg.templatable(trig_keep, args, cg.int_)
cg.add(var.set_trigger_keep(template_))
if (stage_gain := config.get(CONF_STAGE_GAIN)) is not None:
template_ = await cg.templatable(stage_gain, args, cg.int_)
cg.add(var.set_stage_gain(template_))
if power := config.get(CONF_POWER_CONSUMPTION):
if not cg.is_template(power):
power = int(power * 1000000)
template_ = await cg.templatable(power, args, cg.int_)
cg.add(var.set_power_consumption(template_))
return var
+4
View File
@@ -38,6 +38,10 @@ class AT581XComponent final : public Component, public i2c::I2CDevice {
bool i2c_write_config();
bool reset_hardware_frontend();
void reset_hardware_frontend_if(bool reset) {
if (reset)
this->reset_hardware_frontend();
}
bool i2c_write_reg(uint8_t addr, uint8_t data);
bool i2c_write_reg(uint8_t addr, uint32_t data);
bool i2c_write_reg(uint8_t addr, uint16_t data);
-69
View File
@@ -1,69 +0,0 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
#include "at581x.h"
namespace esphome::at581x {
template<typename... Ts> class AT581XResetAction final : public Action<Ts...>, public Parented<AT581XComponent> {
public:
void play(const Ts &...x) { this->parent_->reset_hardware_frontend(); }
};
template<typename... Ts> class AT581XSettingsAction final : public Action<Ts...>, public Parented<AT581XComponent> {
public:
TEMPLATABLE_VALUE(int8_t, hw_frontend_reset)
TEMPLATABLE_VALUE(int, frequency)
TEMPLATABLE_VALUE(int, sensing_distance)
TEMPLATABLE_VALUE(int, poweron_selfcheck_time)
TEMPLATABLE_VALUE(int, power_consumption)
TEMPLATABLE_VALUE(int, protect_time)
TEMPLATABLE_VALUE(int, trigger_base)
TEMPLATABLE_VALUE(int, trigger_keep)
TEMPLATABLE_VALUE(int, stage_gain)
void play(const Ts &...x) {
if (this->frequency_.has_value()) {
int v = this->frequency_.value(x...);
this->parent_->set_frequency(v);
}
if (this->sensing_distance_.has_value()) {
int v = this->sensing_distance_.value(x...);
this->parent_->set_sensing_distance(v);
}
if (this->poweron_selfcheck_time_.has_value()) {
int v = this->poweron_selfcheck_time_.value(x...);
this->parent_->set_poweron_selfcheck_time(v);
}
if (this->power_consumption_.has_value()) {
int v = this->power_consumption_.value(x...);
this->parent_->set_power_consumption(v);
}
if (this->protect_time_.has_value()) {
int v = this->protect_time_.value(x...);
this->parent_->set_protect_time(v);
}
if (this->trigger_base_.has_value()) {
int v = this->trigger_base_.value(x...);
this->parent_->set_trigger_base(v);
}
if (this->trigger_keep_.has_value()) {
int v = this->trigger_keep_.value(x...);
this->parent_->set_trigger_keep(v);
}
if (this->stage_gain_.has_value()) {
int v = this->stage_gain_.value(x...);
this->parent_->set_stage_gain(v);
}
// This actually perform all the modification on the system
this->parent_->i2c_write_config();
if (this->hw_frontend_reset_.has_value() && this->hw_frontend_reset_.value(x...) == true) {
this->parent_->reset_hardware_frontend();
}
}
};
} // namespace esphome::at581x
+10 -2
View File
@@ -14,6 +14,8 @@ from esphome.types import ConfigType, SafeExpType
_LOGGER = logging.getLogger(__name__)
CONF_SUPPORTS_HEAT_COOL = "supports_heat_cool"
DEPENDENCIES = ["remote_transmitter"]
AUTO_LOAD = ["sensor", "remote_base"]
CODEOWNERS = ["@glmnet"]
@@ -37,6 +39,7 @@ def climate_ir_schema(
{
cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
cv.Optional(CONF_SUPPORTS_HEAT_COOL): cv.boolean,
cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor),
}
@@ -61,8 +64,13 @@ def climate_ir_with_receiver_schema(
async def register_climate_ir(var: MockObj, config: ConfigType) -> None:
await cg.register_component(var, config)
await remote_base.register_transmittable(var, config)
cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL]))
cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT]))
supports_cool = config[CONF_SUPPORTS_COOL]
supports_heat = config[CONF_SUPPORTS_HEAT]
cg.add(var.set_supports_cool(supports_cool))
cg.add(var.set_supports_heat(supports_heat))
# The header default is true, so only the false case needs a call.
if not config.get(CONF_SUPPORTS_HEAT_COOL, supports_cool and supports_heat):
cg.add(var.set_supports_heat_cool(False))
if remote_base.CONF_RECEIVER_ID in config:
await remote_base.register_listener(var, config)
if sensor_id := config.get(CONF_SENSOR):
+6 -3
View File
@@ -13,11 +13,13 @@ climate::ClimateTraits ClimateIR::traits() {
if (this->humidity_sensor_ != nullptr) {
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
}
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL});
traits.set_supported_modes({climate::CLIMATE_MODE_OFF});
if (this->supports_cool_)
traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
if (this->supports_heat_)
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
if (this->supports_heat_cool_)
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
if (this->supports_dry_)
traits.add_supported_mode(climate::CLIMATE_MODE_DRY);
if (this->supports_fan_only_)
@@ -94,9 +96,10 @@ void ClimateIR::dump_config() {
" Min. Temperature: %.1f°C\n"
" Max. Temperature: %.1f°C\n"
" Supports HEAT: %s\n"
" Supports COOL: %s",
" Supports COOL: %s\n"
" Supports HEAT_COOL: %s",
this->minimum_temperature_, this->maximum_temperature_, YESNO(this->supports_heat_),
YESNO(this->supports_cool_));
YESNO(this->supports_cool_), YESNO(this->supports_heat_cool_));
}
} // namespace esphome::climate_ir
@@ -41,6 +41,7 @@ class ClimateIR : public Component,
void dump_config() override;
void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
void set_supports_heat_cool(bool supports_heat_cool) { this->supports_heat_cool_ = supports_heat_cool; }
void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void set_humidity_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; }
@@ -60,6 +61,8 @@ class ClimateIR : public Component,
bool supports_cool_{true};
bool supports_heat_{true};
// Default (supports_cool && supports_heat) is resolved during code generation.
bool supports_heat_cool_{true};
bool supports_dry_{false};
bool supports_fan_only_{false};
climate::ClimateFanModeMask fan_modes_{};
+19 -157
View File
@@ -1,5 +1,3 @@
from collections.abc import Callable
from dataclasses import dataclass
import logging
from esphome import automation
@@ -38,14 +36,14 @@ 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, ID, 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.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType, SafeExpType, TemplateArgsType
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,12 +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_(
@@ -256,56 +247,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,107 +269,19 @@ 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
)
-84
View File
@@ -6,90 +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) {}
+29
View File
@@ -1,3 +1,5 @@
from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.components import uart
@@ -16,6 +18,7 @@ DFPlayerIsPlayingCondition = dfplayer_ns.class_(
MULTI_CONF = True
CONF_FOLDER = "folder"
CONF_ENABLE = "enable"
CONF_EQ_PRESET = "eq_preset"
CONF_ON_FINISHED_PLAYBACK = "on_finished_playback"
@@ -375,6 +378,32 @@ async def dfplayer_random_to_code(config, action_id, template_arg, args):
return var
def _default_enable(value: Any) -> Any:
"""Fill in ``enable: true`` for a bare action or a mapping that only picks the player.
Done before ``maybe_simple_value`` so neither form is wrapped as the ``enable`` value.
"""
if value is None or isinstance(value, dict):
return {CONF_ENABLE: True, **(value or {})}
return value
automation.register_apply_action(
"dfplayer.set_current_track_repeat",
cv.All(
_default_enable,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(DFPlayer),
cv.Optional(CONF_ENABLE, default=True): cv.templatable(cv.boolean),
},
key=CONF_ENABLE,
),
),
automation.ApplyField(CONF_ENABLE, "set_current_track_repeat", cg.bool_),
)
@automation.register_condition(
"dfplayer.is_playing",
DFPlayerIsPlayingCondition,
+7
View File
@@ -102,6 +102,13 @@ void DFPlayer::random() {
this->send_cmd_(0x18);
}
void DFPlayer::set_current_track_repeat(bool enable) {
uint16_t arg = enable ? 0x00 : 0x01;
ESP_LOGD(TAG, "Setting current track repeat to %s",
enable ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"));
this->send_cmd_(0x19, arg);
}
void DFPlayer::play_folder(uint16_t folder, uint16_t file) {
ESP_LOGD(TAG, "Playing file %d in folder %d", file, folder);
if (folder < 100 && file < 256) {
+1
View File
@@ -46,6 +46,7 @@ class DFPlayer final : public uart::UARTDevice, public Component {
void pause();
void stop();
void random();
void set_current_track_repeat(bool enable);
bool is_playing() { return is_playing_; }
void dump_config() override;
+15 -7
View File
@@ -262,7 +262,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"esp_lcd", # LCD controller drivers - only needed by display component
"esp_local_ctrl", # Local control over HTTPS/BLE - ESPHome has native API
"esp_phy", # RF PHY - re-included by internal_temperature on the original ESP32; esp_wifi/bt/ieee802154 pull it back
"esp_wifi", # WiFi stack - re-included by request_wifi(), espnow; bt pulls it back for BLE builds
"esp_wifi", # WiFi stack - re-included by request_wifi(), espnow, esp32_hosted; bt pulls it back for BLE builds
"espcoredump", # Core dump support - ESPHome has its own debug component
"fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage
"ieee802154", # 802.15.4 radio - IDF openthread and the Zigbee libs pull it back
@@ -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 -1
View File
@@ -290,6 +290,8 @@ async def to_code(config: ConfigType) -> None:
# symbols are simply unused and never register a callback at runtime.
if esp32.get_esp32_variant() == esp32.VARIANT_ESP32P4:
add_define("USE_ESP_NOW_HOSTED")
# esp_now_hosted.cpp includes esp_now.h, which esp_wifi provides
esp32.include_builtin_idf_component("esp_wifi")
# esp-hosted's CustomRpc ("peer data transfer") path — off by default.
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_ENABLE_PEER_DATA_TRANSFER", True
@@ -311,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",
+7 -11
View File
@@ -316,20 +316,16 @@ async def to_code(config: ConfigType) -> None:
# One key per device: an api encryption block supplies it (static or
# runtime) and offers; the ota block only adds the requirement
api_conf = CORE.config.get(CONF_API) or {}
encryption_conf = config.get(CONF_ENCRYPTION)
own_key = None
if encryption_conf is not None and static_encryption_key(api_conf) is None:
own_key = encryption_conf[CONF_KEY]
if own_key is not None:
if key := static_encryption_key(config) or static_encryption_key(api_conf):
# Build time key: the ota keeps its own pointer so safe mode, which
# has no api server, still has it
cg.add_define("USE_OTA_ENCRYPTION")
cg.add(var.set_noise_psk(new_psk_progmem(config[CONF_ID], own_key)))
cg.add(var.set_noise_psk(new_psk_progmem(config[CONF_ID], key)))
elif CONF_ENCRYPTION in api_conf:
# Runtime key: found in the api server, or in preferences in safe mode
cg.add_define("USE_OTA_ENCRYPTION")
cg.add_define("USE_OTA_ENCRYPTION_FROM_API")
if static_encryption_key(api_conf) is None:
# The key arrives at runtime, so the offer has to look for it
cg.add_define("USE_OTA_ENCRYPTION_PROVISIONED")
if encryption_conf is not None:
cg.add_define("USE_OTA_ENCRYPTION_PROVISIONED")
if CONF_ENCRYPTION in config:
cg.add_define("USE_OTA_ENCRYPTION_REQUIRED")
# Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it.
+17 -5
View File
@@ -1,5 +1,5 @@
#include "ota_esphome.h"
#ifdef USE_OTA_ENCRYPTION_FROM_API
#ifdef USE_OTA_ENCRYPTION_PROVISIONED
#include "esphome/components/api/api_server.h"
#endif
#ifdef USE_OTA
@@ -32,11 +32,13 @@ static const char *const TAG = "esphome.ota";
#ifdef USE_OTA_ENCRYPTION
const noise::NoiseContext &ESPHomeOTAComponent::noise_context_() const {
#ifdef USE_OTA_ENCRYPTION_FROM_API
return api::global_api_server->get_noise_ctx();
#else
return this->noise_ctx_;
#ifdef USE_OTA_ENCRYPTION_PROVISIONED
// The api server holds the live key; safe mode never constructs it, and then
// noise_ctx_ holds the saved key setup() found, if any
if (api::global_api_server != nullptr)
return api::global_api_server->get_noise_ctx();
#endif
return this->noise_ctx_;
}
#endif
static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
@@ -58,6 +60,16 @@ extern "C" void esphome_wake_ota_component_any_context() {
}
void ESPHomeOTAComponent::setup() {
#ifdef USE_OTA_ENCRYPTION_PROVISIONED
// Safe mode never constructs the api server, so read the key it saved
noise::psk_t psk;
if (api::global_api_server == nullptr && api::load_saved_noise_psk(psk)) {
this->saved_psk_ = RAMAllocator<noise::psk_t>().make_unique(psk);
if (this->saved_psk_ != nullptr) {
this->noise_ctx_.set_psk(this->saved_psk_->data());
}
}
#endif
this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections
if (this->server_ == nullptr) {
this->server_failed_(LOG_STR("creation"));
+6 -3
View File
@@ -44,7 +44,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
}
#endif // USE_OTA_PASSWORD
#if defined(USE_OTA_ENCRYPTION) && !defined(USE_OTA_ENCRYPTION_FROM_API)
#ifdef USE_OTA_ENCRYPTION
/// psk points at 32 bytes that live in flash for the life of the program
void set_noise_psk(const uint8_t *psk) { this->noise_ctx_.set_psk(psk); }
#endif
@@ -86,7 +86,8 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
bool writing{false}; // a produced handshake frame is still being flushed
uint8_t frame_buf[noise::FRAME_HEADER_SIZE + 1 + noise::MAX_HANDSHAKE_SIZE];
};
// The api server's live context when the api has encryption, else our own
// The api server's live context when it exists, otherwise our own (a build
// time key, or the saved key loaded in safe mode)
const noise::NoiseContext &noise_context_() const;
bool noise_start_session_(uint8_t server_feature_flags);
bool handle_noise_handshake_();
@@ -148,8 +149,10 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
RAMUniquePtr<uint8_t[]> auth_buf_;
#endif // USE_OTA_PASSWORD
#ifdef USE_OTA_ENCRYPTION
#ifndef USE_OTA_ENCRYPTION_FROM_API
noise::NoiseContext noise_ctx_;
#ifdef USE_OTA_ENCRYPTION_PROVISIONED
// Backs noise_ctx_ in safe mode, where no api server holds the saved key
RAMUniquePtr<noise::psk_t> saved_psk_;
#endif
RAMUniquePtr<NoiseSession> noise_;
#endif // USE_OTA_ENCRYPTION
@@ -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()
+11 -22
View File
@@ -5,17 +5,6 @@ namespace esphome::gree {
static const char *const TAG = "gree.climate";
climate::ClimateTraits GreeClimate::traits() {
auto t = climate_ir::ClimateIR::traits();
// ClimateIR unconditionally includes HEAT_COOL in the base mode set; remove it when heat is not supported.
if (!this->supports_heat_) {
auto modes = t.get_supported_modes();
modes.erase(climate::CLIMATE_MODE_HEAT_COOL);
t.set_supported_modes(modes);
}
return t;
}
void GreeClimate::set_model(Model model) {
if (model == GREE_YAN) {
// YAN only has a vertical vane; the horizontal swing IR bytes are not defined for this model.
@@ -42,23 +31,24 @@ void GreeClimate::set_mode_bit(uint8_t bit_mask, bool enabled) {
void GreeClimate::transmit_state() {
uint8_t remote_state[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00};
const Model model = this->model_;
remote_state[0] = this->fan_speed_() | this->operation_mode_();
remote_state[1] = this->temperature_();
if (this->model_ == GREE_YAN) {
if (model == GREE_YAN) {
remote_state[2] = 0x20; // bits 0..3 always 0000, bits 4..7 TURBO, LIGHT, HEALTH, X-FAN
remote_state[3] = 0x50; // bits 4..7 always 0101
remote_state[4] = this->vertical_swing_();
}
if (this->model_ == GREE_YX1FF || this->model_ == GREE_YAG) {
if (model == GREE_YX1FF || model == GREE_YAG) {
remote_state[2] = 0x60;
remote_state[3] = 0x50;
remote_state[4] = this->vertical_swing_();
}
if (this->model_ == GREE_YAG) {
if (model == GREE_YAG) {
remote_state[5] = 0x40;
if (this->vertical_swing_() == GREE_VDIR_SWING || this->horizontal_swing_() == GREE_HDIR_SWING) {
@@ -66,11 +56,11 @@ void GreeClimate::transmit_state() {
}
}
if (this->model_ == GREE_YAC || this->model_ == GREE_YAG) {
if (model == GREE_YAC || model == GREE_YAG) {
remote_state[4] |= (this->horizontal_swing_() << 4);
}
if (this->model_ == GREE_YAA || this->model_ == GREE_YAC || this->model_ == GREE_YAC1FB9) {
if (model == GREE_YAA || model == GREE_YAC || model == GREE_YAC1FB9) {
remote_state[2] = 0x20; // bits 0..3 always 0000, bits 4..7 TURBO, LIGHT, HEALTH, X-FAN
remote_state[3] = 0x50; // bits 4..7 always 0101
remote_state[6] = 0x20; // YAA1FB, FAA1FB1, YB1F2 bits 4..7 always 0010
@@ -82,14 +72,13 @@ void GreeClimate::transmit_state() {
}
}
if (this->model_ == GREE_YAN || this->model_ == GREE_YAA || this->model_ == GREE_YAC ||
this->model_ == GREE_YAC1FB9) {
if (model == GREE_YAN || model == GREE_YAA || model == GREE_YAC || model == GREE_YAC1FB9) {
// Merge the mode bits into remote_state[2]
// Clear the mode bits (bits 4-7) and OR in the current mode_bits_
remote_state[2] = (remote_state[2] & 0x0F) | this->mode_bits_;
}
if (this->model_ == GREE_YX1FF) {
if (model == GREE_YX1FF) {
if (this->fan_speed_() == GREE_FAN_TURBO) {
remote_state[2] |= GREE_FAN_TURBO_BIT;
}
@@ -100,7 +89,7 @@ void GreeClimate::transmit_state() {
}
// Calculate the checksum
if (this->model_ == GREE_YAN || this->model_ == GREE_YX1FF) {
if (model == GREE_YAN || model == GREE_YX1FF) {
remote_state[7] = ((remote_state[0] << 4) + (remote_state[1] << 4) + 0xC0);
} else {
remote_state[7] =
@@ -115,7 +104,7 @@ void GreeClimate::transmit_state() {
data->set_carrier_frequency(GREE_IR_FREQUENCY);
data->mark(GREE_HEADER_MARK);
if (this->model_ == GREE_YAC1FB9) {
if (model == GREE_YAC1FB9) {
data->space(GREE_YAC1FB9_HEADER_SPACE);
} else {
data->space(GREE_HEADER_SPACE);
@@ -137,7 +126,7 @@ void GreeClimate::transmit_state() {
data->space(GREE_ZERO_SPACE);
data->mark(GREE_BIT_MARK);
if (this->model_ == GREE_YAC1FB9) {
if (model == GREE_YAC1FB9) {
data->space(GREE_YAC1FB9_MESSAGE_SPACE);
} else {
data->space(GREE_MESSAGE_SPACE);
+1 -2
View File
@@ -77,7 +77,7 @@ static constexpr uint8_t GREE_PRESET_SLEEP = 0x01;
static constexpr uint8_t GREE_PRESET_SLEEP_BIT = 0x80;
// Model codes
enum Model { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9, GREE_YX1FF, GREE_YAG };
enum Model : uint8_t { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9, GREE_YX1FF, GREE_YAG };
class GreeClimate final : public climate_ir::ClimateIR {
public:
@@ -94,7 +94,6 @@ class GreeClimate final : public climate_ir::ClimateIR {
protected:
// Transmit via IR the state of this climate controller.
void transmit_state() override;
climate::ClimateTraits traits() override;
uint8_t operation_mode_();
uint8_t fan_speed_();
+6
View File
@@ -513,6 +513,8 @@ async def component_to_code(config):
# it for project source files only. GCC uses the last -O flag.
build_src_flags += " -Os"
cg.add_platformio_option("build_src_flags", build_src_flags)
# Must run before the platform's builder scripts are loaded; see the script.
cg.add_platformio_option("extra_scripts", ["pre:scons_dont_inherit.py"])
cg.add_platformio_option("extra_scripts", ["pre:ccache.py"])
# IRAM_ATTR is a no-op on BK72xx (SDK masks FIQ+IRQ around flash ops).
# On other families, patch_linker.py routes .sram.text into the right
@@ -618,4 +620,8 @@ def copy_files() -> None:
patch_linker_file,
CORE.relative_build_path("patch_linker.py"),
)
copy_file_if_changed(
script_dir / "scons_dont_inherit.py.script",
CORE.relative_build_path("scons_dont_inherit.py"),
)
copy_ccache_script()
@@ -0,0 +1,16 @@
# pylint: disable=E0602
Import("env") # noqa
import builtins
import functools
import sys
# SCons 4.11 compiles SConscript files without dont_inherit=True, so its own
# `from __future__ import annotations` leaks into every builder script and
# turns their annotations into strings. That breaks LibreTiny's Library
# dataclass, whose InitVar is then no longer recognised. Runs before the
# platform's builder scripts are loaded. The module comes from sys.modules
# because SCons.Script also exports a function named SConscript.
sconscript = sys.modules["SCons.Script.SConscript"]
if sconscript.__dict__.get("compile") is None:
sconscript.compile = functools.partial(builtins.compile, dont_inherit=True)
@@ -118,6 +118,9 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
}
#endif
#endif
#ifdef USE_OTA_SIGNED_VERIFICATION
txt_count++; // ota_signed
#endif
#ifdef ESPHOME_PROJECT_NAME
txt_count += 2; // project_name and project_version
#endif
@@ -186,6 +189,13 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
#endif
#endif
#ifdef USE_OTA_SIGNED_VERIFICATION
// Signals that an unsigned OTA image is rejected; serial flash is unaffected.
MDNS_STATIC_CONST_CHAR(TXT_OTA_SIGNED, "ota_signed");
MDNS_STATIC_CONST_CHAR(VALUE_TRUE, "1");
txt_records.push_back({MDNS_STR(TXT_OTA_SIGNED), MDNS_STR(VALUE_TRUE)});
#endif
#ifdef ESPHOME_PROJECT_NAME
MDNS_STATIC_CONST_CHAR(TXT_PROJECT_NAME, "project_name");
MDNS_STATIC_CONST_CHAR(TXT_PROJECT_VERSION, "project_version");
@@ -53,46 +53,6 @@ const uint8_t MITSUBISHI_BYTE04 = 0x00;
const uint8_t MITSUBISHI_BYTE13 = 0x00;
const uint8_t MITSUBISHI_BYTE16 = 0x00;
climate::ClimateTraits MitsubishiClimate::traits() {
auto traits = climate::ClimateTraits();
if (this->sensor_ != nullptr) {
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
}
traits.set_visual_min_temperature(MITSUBISHI_TEMP_MIN);
traits.set_visual_max_temperature(MITSUBISHI_TEMP_MAX);
traits.set_visual_temperature_step(1.0f);
traits.set_supported_modes({climate::CLIMATE_MODE_OFF});
if (this->supports_cool_)
traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
if (this->supports_heat_)
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
if (this->supports_cool_ && this->supports_heat_)
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
if (this->supports_dry_)
traits.add_supported_mode(climate::CLIMATE_MODE_DRY);
if (this->supports_fan_only_)
traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY);
// Default to only 3 levels in ESPHome even if most unit supports 4. The 3rd level is not used.
traits.set_supported_fan_modes(
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH});
if (this->fan_mode_ == MITSUBISHI_FAN_Q4L)
traits.add_supported_fan_mode(climate::CLIMATE_FAN_QUIET);
if (/*this->fan_mode_ == MITSUBISHI_FAN_5L ||*/ this->fan_mode_ >= MITSUBISHI_FAN_4L)
traits.add_supported_fan_mode(climate::CLIMATE_FAN_MIDDLE); // Shouldn't be used for this but it helps
traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH,
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL});
traits.set_supported_presets({climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO,
climate::CLIMATE_PRESET_BOOST, climate::CLIMATE_PRESET_SLEEP});
return traits;
}
void MitsubishiClimate::transmit_state() {
// Byte 0-4: Constant: 0x23, 0xCB, 0x26, 0x01, 0x00
// Byte 5: On=0x20, Off: 0x00
+16 -9
View File
@@ -11,7 +11,7 @@ const uint8_t MITSUBISHI_TEMP_MIN = 16; // Celsius
const uint8_t MITSUBISHI_TEMP_MAX = 31; // Celsius
// Fan mode
enum SetFanMode {
enum SetFanMode : uint8_t {
MITSUBISHI_FAN_3L = 0, // 3 levels + auto
MITSUBISHI_FAN_4L, // 4 levels + auto
MITSUBISHI_FAN_Q4L, // Quiet + 4 levels + auto
@@ -19,7 +19,7 @@ enum SetFanMode {
};
// Enum to represent horizontal directios
enum HorizontalDirection {
enum HorizontalDirection : uint8_t {
HORIZONTAL_DIRECTION_LEFT = 0x10,
HORIZONTAL_DIRECTION_MIDDLE_LEFT = 0x20,
HORIZONTAL_DIRECTION_MIDDLE = 0x30,
@@ -29,7 +29,7 @@ enum HorizontalDirection {
};
// Enum to represent vertical directions
enum VerticalDirection {
enum VerticalDirection : uint8_t {
VERTICAL_DIRECTION_AUTO = 0x00,
VERTICAL_DIRECTION_UP = 0x08,
VERTICAL_DIRECTION_MIDDLE_UP = 0x10,
@@ -38,12 +38,14 @@ enum VerticalDirection {
VERTICAL_DIRECTION_DOWN = 0x28,
};
// Fan modes offered by every set_fan_mode option.
static constexpr climate::ClimateFanModeMask MITSUBISHI_BASE_FAN_MODES{
climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH};
class MitsubishiClimate final : public climate_ir::ClimateIR {
public:
MitsubishiClimate()
: climate_ir::ClimateIR(MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MIDDLE,
climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_QUIET},
: climate_ir::ClimateIR(MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX, 1.0f, true, true, MITSUBISHI_BASE_FAN_MODES,
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL,
climate::CLIMATE_SWING_HORIZONTAL},
{climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST,
@@ -54,7 +56,14 @@ class MitsubishiClimate final : public climate_ir::ClimateIR {
void set_supports_fan_only(bool supports_fan_only) { this->supports_fan_only_ = supports_fan_only; }
void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
void set_fan_mode(SetFanMode fan_mode) { this->fan_mode_ = fan_mode; }
void set_fan_mode(SetFanMode fan_mode) {
this->fan_mode_ = fan_mode;
this->fan_modes_ = MITSUBISHI_BASE_FAN_MODES;
if (fan_mode == MITSUBISHI_FAN_Q4L)
this->fan_modes_.insert(climate::CLIMATE_FAN_QUIET);
if (fan_mode >= MITSUBISHI_FAN_4L)
this->fan_modes_.insert(climate::CLIMATE_FAN_MIDDLE); // Shouldn't be used for this but it helps
}
void set_horizontal_default(HorizontalDirection horizontal_direction) {
this->default_horizontal_direction_ = horizontal_direction;
@@ -74,8 +83,6 @@ class MitsubishiClimate final : public climate_ir::ClimateIR {
HorizontalDirection default_horizontal_direction_;
VerticalDirection default_vertical_direction_;
climate::ClimateTraits traits() override;
};
} // namespace esphome::mitsubishi
+12
View File
@@ -203,6 +203,12 @@ void ModbusClientHub::parse_modbus_frames() {
void ModbusServerHub::parse_modbus_frames() {
while (!this->rx_buffer_.empty()) {
if (this->deferred_payload_len_ != 0) {
// Another frame arrived before the deferred reply went out, so the client has moved on.
this->cancel_timeout("deferred_send");
ESP_LOGD(TAG, "Dropped deferred reply to %" PRIu8 ": a new frame arrived first", this->deferred_payload_[0]);
this->deferred_payload_len_ = 0;
}
size_t size = this->rx_buffer_.size();
ESP_LOGVV(TAG, "Parsing frames buffer size = %" PRIu32, size);
bool retry_as_client = false;
@@ -1213,6 +1219,7 @@ void ModbusServerHub::send_raw_(const uint8_t *payload, uint16_t len) {
this->set_timeout("deferred_send", (this->tx_delay_remaining() + US_PER_MS - 1) / US_PER_MS, [this]() {
ModbusFrame frame(this->deferred_payload_[0], this->deferred_payload_.data() + 1,
this->deferred_payload_len_ - 1);
this->deferred_payload_len_ = 0;
if (!this->send_frame_(frame)) {
ESP_LOGE(TAG, "Deferred server reply dropped: transmission still blocked");
}
@@ -1234,6 +1241,11 @@ void Modbus::clear_rx_buffer_(const LogString *reason, bool warn, size_t bytes_t
if (warn) {
ESP_LOGW(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "us after last send", bytes, LOG_STR_ARG(reason),
micros() - this->last_send_);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
ESP_LOGV(TAG, " discarded: %s%s", format_hex_pretty_to(hex_buf, this->rx_buffer_.data(), bytes),
bytes > MODBUS_MAX_LOG_BYTES ? LOG_STR_LITERAL(" ...") : LOG_STR_LITERAL(""));
#endif
} else {
ESP_LOGV(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "us after last send", bytes, LOG_STR_ARG(reason),
micros() - this->last_send_);
-9
View File
@@ -147,15 +147,6 @@ struct IPAddress {
}
return (ntohl(this->ip_addr_.u_addr.ip4.s_addr) & 0xF0000000UL) == 0xE0000000UL;
}
// Remove before 2026.8.0
ESPDEPRECATED(
"str() is deprecated: use 'char buf[IP_ADDRESS_BUFFER_SIZE]; ip.str_to(buf);' instead. Removed in 2026.8.0",
"2026.2.0")
std::string str() const {
char buf[IP_ADDRESS_BUFFER_SIZE];
this->str_to(buf);
return buf;
}
char *str_to(char *buf) const {
if (this->ip_addr_.type == IPADDR_TYPE_V6) {
inet_ntop(AF_INET6, &this->ip_addr_.u_addr.ip6, buf, IP_ADDRESS_BUFFER_SIZE);
-96
View File
@@ -1,96 +0,0 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/string_ref.h"
#include "nextion.h"
namespace esphome::nextion {
template<typename... Ts> class NextionSetBrightnessAction final : public Action<Ts...> {
public:
explicit NextionSetBrightnessAction(Nextion *component) : component_(component) {}
TEMPLATABLE_VALUE(float, brightness)
void play(const Ts &...x) override {
this->component_->set_brightness(this->brightness_.value(x...));
this->component_->set_backlight_brightness(this->brightness_.value(x...));
}
void set_brightness(std::function<void(Ts..., float)> brightness) { this->brightness_ = brightness; }
protected:
Nextion *component_;
};
template<typename... Ts> class NextionPublishFloatAction final : public Action<Ts...> {
public:
explicit NextionPublishFloatAction(NextionComponent *component) : component_(component) {}
TEMPLATABLE_VALUE(float, state)
TEMPLATABLE_VALUE(bool, publish_state)
TEMPLATABLE_VALUE(bool, send_to_nextion)
void play(const Ts &...x) override {
this->component_->set_state(this->state_.value(x...), this->publish_state_.value(x...),
this->send_to_nextion_.value(x...));
}
void set_state(std::function<void(Ts..., float)> state) { this->state_ = state; }
void set_publish_state(std::function<void(Ts..., bool)> publish_state) { this->publish_state_ = publish_state; }
void set_send_to_nextion(std::function<void(Ts..., bool)> send_to_nextion) {
this->send_to_nextion_ = send_to_nextion;
}
protected:
NextionComponent *component_;
};
template<typename... Ts> class NextionPublishTextAction final : public Action<Ts...> {
public:
explicit NextionPublishTextAction(NextionComponent *component) : component_(component) {}
TEMPLATABLE_VALUE(const char *, state)
TEMPLATABLE_VALUE(bool, publish_state)
TEMPLATABLE_VALUE(bool, send_to_nextion)
void play(const Ts &...x) override {
this->component_->set_state(this->state_.value(x...), this->publish_state_.value(x...),
this->send_to_nextion_.value(x...));
}
void set_state(std::function<void(Ts..., const char *)> state) { this->state_ = state; }
void set_publish_state(std::function<void(Ts..., bool)> publish_state) { this->publish_state_ = publish_state; }
void set_send_to_nextion(std::function<void(Ts..., bool)> send_to_nextion) {
this->send_to_nextion_ = send_to_nextion;
}
protected:
NextionComponent *component_;
};
template<typename... Ts> class NextionPublishBoolAction final : public Action<Ts...> {
public:
explicit NextionPublishBoolAction(NextionComponent *component) : component_(component) {}
TEMPLATABLE_VALUE(bool, state)
TEMPLATABLE_VALUE(bool, publish_state)
TEMPLATABLE_VALUE(bool, send_to_nextion)
void play(const Ts &...x) override {
this->component_->set_state(this->state_.value(x...), this->publish_state_.value(x...),
this->send_to_nextion_.value(x...));
}
void set_state(std::function<void(Ts..., bool)> state) { this->state_ = state; }
void set_publish_state(std::function<void(Ts..., bool)> publish_state) { this->publish_state_ = publish_state; }
void set_send_to_nextion(std::function<void(Ts..., bool)> send_to_nextion) {
this->send_to_nextion_ = send_to_nextion;
}
protected:
NextionComponent *component_;
};
} // namespace esphome::nextion
@@ -18,10 +18,6 @@ NextionBinarySensor = nextion_ns.class_(
"NextionBinarySensor", binary_sensor.BinarySensor, cg.PollingComponent
)
NextionPublishBoolAction = nextion_ns.class_(
"NextionPublishBoolAction", automation.Action
)
CONFIG_SCHEMA = cv.All(
binary_sensor.binary_sensor_schema(NextionBinarySensor)
.extend(
@@ -57,9 +53,8 @@ async def to_code(config):
cg.add(hub.register_binarysensor_component(var))
@automation.register_action(
automation.register_apply_action(
"binary_sensor.nextion.publish",
NextionPublishBoolAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(NextionBinarySensor),
@@ -70,19 +65,12 @@ async def to_code(config):
),
}
),
synchronous=True,
automation.ApplyCall(
"set_state({}, {}, {})",
(
(CONF_STATE, cg.bool_),
(CONF_PUBLISH_STATE, cg.bool_),
(CONF_SEND_TO_NEXTION, cg.bool_),
),
),
)
async def sensor_nextion_publish_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_)
cg.add(var.set_state(template_))
template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, cg.bool_)
cg.add(var.set_publish_state(template_))
template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, cg.bool_)
cg.add(var.set_send_to_nextion(template_))
return var
+3 -16
View File
@@ -56,11 +56,6 @@ def AUTO_LOAD() -> list[str]:
return base
NextionSetBrightnessAction = nextion_ns.class_(
"NextionSetBrightnessAction", automation.Action
)
def _deprecated_dump_device_info(value):
_LOGGER.warning(
"'dump_device_info' is deprecated and will be removed in ESPHome 2026.11.0. "
@@ -160,9 +155,8 @@ CONFIG_SCHEMA = cv.All(
)
@automation.register_action(
automation.register_apply_action(
"display.nextion.set_brightness",
NextionSetBrightnessAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(Nextion),
@@ -170,16 +164,9 @@ CONFIG_SCHEMA = cv.All(
},
key=CONF_BRIGHTNESS,
),
synchronous=True,
automation.ApplyField(CONF_BRIGHTNESS, "set_brightness", cg.float_),
automation.ApplyField(CONF_BRIGHTNESS, "set_backlight_brightness", cg.float_),
)
async def nextion_set_brightness_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, cg.float_)
cg.add(var.set_brightness(template_))
return var
_CALLBACK_AUTOMATIONS = (
+9 -21
View File
@@ -21,10 +21,6 @@ CODEOWNERS = ["@senexcrenshaw"]
NextionSensor = nextion_ns.class_("NextionSensor", sensor.Sensor, cg.PollingComponent)
NextionPublishFloatAction = nextion_ns.class_(
"NextionPublishFloatAction", automation.Action
)
def CheckWaveID(value):
value = cv.int_(value)
@@ -97,9 +93,8 @@ async def to_code(config):
cg.add(var.set_wave_max_length(config[CONF_WAVE_MAX_LENGTH]))
@automation.register_action(
automation.register_apply_action(
"sensor.nextion.publish",
NextionPublishFloatAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(NextionSensor),
@@ -110,19 +105,12 @@ async def to_code(config):
),
}
),
synchronous=True,
automation.ApplyCall(
"set_state({}, {}, {})",
(
(CONF_STATE, cg.float_),
(CONF_PUBLISH_STATE, cg.bool_),
(CONF_SEND_TO_NEXTION, cg.bool_),
),
),
)
async def sensor_nextion_publish_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_STATE], args, cg.float_)
cg.add(var.set_state(template_))
template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, cg.bool_)
cg.add(var.set_publish_state(template_))
template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, cg.bool_)
cg.add(var.set_send_to_nextion(template_))
return var
+9 -21
View File
@@ -16,10 +16,6 @@ CODEOWNERS = ["@senexcrenshaw"]
NextionSwitch = nextion_ns.class_("NextionSwitch", switch.Switch, cg.PollingComponent)
NextionPublishBoolAction = nextion_ns.class_(
"NextionPublishBoolAction", automation.Action
)
CONFIG_SCHEMA = cv.All(
switch.switch_schema(NextionSwitch)
.extend(CONFIG_SWITCH_COMPONENT_SCHEMA)
@@ -39,9 +35,8 @@ async def to_code(config):
await setup_component_core_(var, config, ".val")
@automation.register_action(
automation.register_apply_action(
"switch.nextion.publish",
NextionPublishBoolAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(NextionSwitch),
@@ -52,19 +47,12 @@ async def to_code(config):
),
}
),
synchronous=True,
automation.ApplyCall(
"set_state({}, {}, {})",
(
(CONF_STATE, cg.bool_),
(CONF_PUBLISH_STATE, cg.bool_),
(CONF_SEND_TO_NEXTION, cg.bool_),
),
),
)
async def sensor_nextion_publish_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_)
cg.add(var.set_state(template_))
template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, cg.bool_)
cg.add(var.set_publish_state(template_))
template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, cg.bool_)
cg.add(var.set_send_to_nextion(template_))
return var
@@ -13,10 +13,6 @@ NextionTextSensor = nextion_ns.class_(
"NextionTextSensor", text_sensor.TextSensor, cg.PollingComponent
)
NextionPublishTextAction = nextion_ns.class_(
"NextionPublishTextAction", automation.Action
)
CONFIG_SCHEMA = (
text_sensor.text_sensor_schema(NextionTextSensor)
.extend(CONFIG_TEXT_COMPONENT_SCHEMA)
@@ -35,9 +31,8 @@ async def to_code(config):
await setup_component_core_(var, config, ".txt")
@automation.register_action(
automation.register_apply_action(
"text_sensor.nextion.publish",
NextionPublishTextAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(NextionTextSensor),
@@ -48,19 +43,12 @@ async def to_code(config):
),
}
),
synchronous=True,
automation.ApplyCall(
"set_state({}, {}, {})",
(
(CONF_STATE, cg.std_string),
(CONF_PUBLISH_STATE, cg.bool_),
(CONF_SEND_TO_NEXTION, cg.bool_),
),
),
)
async def sensor_nextion_publish_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_STATE], args, cg.const_char_ptr)
cg.add(var.set_state(template_))
template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, cg.bool_)
cg.add(var.set_publish_state(template_))
template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, cg.bool_)
cg.add(var.set_send_to_nextion(template_))
return var
+12 -6
View File
@@ -5,11 +5,12 @@ from typing import Any
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ENCRYPTION, CONF_KEY
from esphome.core import ID
from esphome.core import CORE, ID
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
DOMAIN = "noise"
noise_ns = cg.esphome_ns.namespace("noise")
@@ -70,11 +71,16 @@ def static_encryption_key(conf: ConfigType) -> str | None:
def new_psk_progmem(parent_id: ID, key: str) -> MockObj:
"""Emit the decoded key as a PROGMEM array; the component keeps a pointer
so the key never occupies RAM."""
return cg.progmem_array(
ID(f"{parent_id.id}_psk", is_declaration=True, type=cg.uint8),
list(decode_encryption_key(key)),
)
so the key never occupies RAM. Components sharing one key (api and ota)
share the array."""
decoded = decode_encryption_key(key)
arrays: dict[bytes, MockObj] = CORE.data.setdefault(DOMAIN, {})
if (array := arrays.get(decoded)) is None:
array = arrays[decoded] = cg.progmem_array(
ID(f"{parent_id.id}_psk", is_declaration=True, type=cg.uint8),
list(decoded),
)
return array
def encryption_schema(config: ConfigType | None) -> ConfigType:
+8
View File
@@ -428,6 +428,10 @@ async def to_code(config: ConfigType) -> None:
)
zephyr_add_prj_conf("REBOOT", True)
# some boards enable USB by default.
# disable it to prevent extra current consumption.
zephyr_add_prj_conf("USB_DEVICE_STACK", False, False)
@coroutine_with_priority(CoroPriority.DIAGNOSTICS)
async def _dfu_to_code(dfu_config):
@@ -436,6 +440,10 @@ async def _dfu_to_code(dfu_config):
if CONF_RESET_PIN in dfu_config:
pin = await cg.gpio_pin_expression(dfu_config[CONF_RESET_PIN])
cg.add(var.set_reset_pin(pin))
# DFU uses cdc rate callback to enter bootloader which was disabled explicitly to save power.
zephyr_add_prj_conf("USB_DEVICE_STACK", True)
zephyr_add_prj_conf("USB_CDC_ACM", True)
zephyr_add_prj_conf("CDC_ACM_DTE_RATE_CALLBACK_SUPPORT", True)
await cg.register_component(var, dfu_config)
+61 -38
View File
@@ -3,19 +3,13 @@ import esphome.codegen as cg
from esphome.components import climate, output, sensor
import esphome.config_validation as cv
from esphome.const import CONF_HUMIDITY_SENSOR, CONF_ID, CONF_SENSOR
from esphome.core import ID
from esphome.core import ID, Lambda
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
pid_ns = cg.esphome_ns.namespace("pid")
PIDClimate = pid_ns.class_("PIDClimate", climate.Climate, cg.Component)
PIDAutotuneAction = pid_ns.class_("PIDAutotuneAction", automation.Action)
PIDResetIntegralTermAction = pid_ns.class_(
"PIDResetIntegralTermAction", automation.Action
)
PIDSetControlParametersAction = pid_ns.class_(
"PIDSetControlParametersAction", automation.Action
)
CONF_DEFAULT_TARGET_TEMPERATURE = "default_target_temperature"
@@ -43,6 +37,24 @@ CONF_KP_MULTIPLIER = "kp_multiplier"
CONF_KI_MULTIPLIER = "ki_multiplier"
CONF_KD_MULTIPLIER = "kd_multiplier"
def _validate_thresholds(config: ConfigType) -> ConfigType:
# Same rule as PIDClimate::set_deadband_thresholds; equal is allowed since 0/0 is the default.
if config[CONF_THRESHOLD_LOW] > config[CONF_THRESHOLD_HIGH]:
raise cv.Invalid(
f"{CONF_THRESHOLD_LOW} must not be greater than {CONF_THRESHOLD_HIGH}"
)
return config
def _validate_threshold_action(config: ConfigType) -> ConfigType:
threshold_low = config[CONF_THRESHOLD_LOW]
threshold_high = config[CONF_THRESHOLD_HIGH]
if isinstance(threshold_low, Lambda) or isinstance(threshold_high, Lambda):
return config
return _validate_thresholds(config)
CONFIG_SCHEMA = cv.All(
climate.climate_schema(PIDClimate).extend(
{
@@ -51,7 +63,7 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE): cv.temperature,
cv.Optional(CONF_COOL_OUTPUT): cv.use_id(output.FloatOutput),
cv.Optional(CONF_HEAT_OUTPUT): cv.use_id(output.FloatOutput),
cv.Optional(CONF_DEADBAND_PARAMETERS): cv.Schema(
cv.Optional(CONF_DEADBAND_PARAMETERS): cv.All(
{
cv.Required(CONF_THRESHOLD_HIGH): cv.temperature_delta,
cv.Required(CONF_THRESHOLD_LOW): cv.temperature_delta,
@@ -61,7 +73,8 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(
CONF_DEADBAND_OUTPUT_AVERAGING_SAMPLES, default=1
): cv.positive_not_null_int,
}
},
_validate_thresholds,
),
cv.Required(CONF_CONTROL_PARAMETERS): cv.Schema(
{
@@ -134,24 +147,15 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_default_target_temperature(config[CONF_DEFAULT_TARGET_TEMPERATURE]))
@automation.register_action(
automation.register_apply_action(
"climate.pid.reset_integral_term",
PIDResetIntegralTermAction,
automation.maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(PIDClimate),
}
),
synchronous=True,
automation.ApplyCall("reset_integral_term()"),
)
async def pid_reset_integral_term(
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(
@@ -185,9 +189,8 @@ async def esp8266_set_frequency_to_code(
return var
@automation.register_action(
automation.register_apply_action(
"climate.pid.set_control_parameters",
PIDSetControlParametersAction,
automation.maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(PIDClimate),
@@ -196,24 +199,44 @@ async def esp8266_set_frequency_to_code(
cv.Optional(CONF_KD, default=0.0): cv.templatable(cv.float_),
}
),
synchronous=True,
automation.ApplyField(CONF_KP, "set_kp", cg.float_),
automation.ApplyField(CONF_KI, "set_ki", cg.float_),
automation.ApplyField(CONF_KD, "set_kd", cg.float_),
)
async def set_control_parameters(
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)
kp_template_ = await cg.templatable(config[CONF_KP], args, cg.float_)
cg.add(var.set_kp(kp_template_))
ki_template_ = await cg.templatable(config[CONF_KI], args, cg.float_)
cg.add(var.set_ki(ki_template_))
automation.register_apply_action(
"climate.pid.set_deadband_control_parameters_multipliers",
automation.maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(PIDClimate),
# kp_multiplier is required for compatibility with the original action API;
# ki_multiplier and kd_multiplier are optional overrides.
cv.Required(CONF_KP_MULTIPLIER): cv.templatable(cv.float_),
cv.Optional(CONF_KI_MULTIPLIER): cv.templatable(cv.float_),
cv.Optional(CONF_KD_MULTIPLIER): cv.templatable(cv.float_),
}
),
automation.ApplyField(CONF_KP_MULTIPLIER, "set_kp_multiplier", cg.float_),
automation.ApplyField(CONF_KI_MULTIPLIER, "set_ki_multiplier", cg.float_),
automation.ApplyField(CONF_KD_MULTIPLIER, "set_kd_multiplier", cg.float_),
)
kd_template_ = await cg.templatable(config[CONF_KD], args, cg.float_)
cg.add(var.set_kd(kd_template_))
return var
automation.register_apply_action(
"climate.pid.set_deadband_threshold_parameters",
automation.maybe_simple_id(
cv.All(
{
cv.Required(CONF_ID): cv.use_id(PIDClimate),
cv.Required(CONF_THRESHOLD_HIGH): cv.templatable(cv.temperature_delta),
cv.Required(CONF_THRESHOLD_LOW): cv.templatable(cv.temperature_delta),
},
_validate_threshold_action,
)
),
automation.ApplyCall(
"set_deadband_thresholds({}, {})",
((CONF_THRESHOLD_LOW, cg.float_), (CONF_THRESHOLD_HIGH, cg.float_)),
),
)
+11
View File
@@ -5,6 +5,17 @@ namespace esphome::pid {
static const char *const TAG = "pid.climate";
bool PIDClimate::set_deadband_thresholds(float threshold_low, float threshold_high) {
if (threshold_low > threshold_high) {
ESP_LOGW(TAG, "Deadband threshold low %.2f must not be greater than high %.2f", threshold_low, threshold_high);
return false;
}
this->set_threshold_low(threshold_low);
this->set_threshold_high(threshold_high);
return true;
}
void PIDClimate::setup() {
this->sensor_->add_on_state_callback([this](float state) {
// only publish if state/current temperature has changed in two digits of precision
+1 -32
View File
@@ -26,6 +26,7 @@ class PIDClimate final : public climate::Climate, public Component {
void set_kd(float kd) { controller_.kd_ = kd; }
void set_min_integral(float min_integral) { controller_.min_integral_ = min_integral; }
void set_max_integral(float max_integral) { controller_.max_integral_ = max_integral; }
bool set_deadband_thresholds(float threshold_low, float threshold_high);
void set_output_samples(int in) { controller_.output_samples_ = in; }
void set_derivative_samples(int in) {
controller_.derivative_samples_ = in;
@@ -131,36 +132,4 @@ template<typename... Ts> class PIDAutotuneAction final : public Action<Ts...> {
PIDClimate *parent_;
};
template<typename... Ts> class PIDResetIntegralTermAction final : public Action<Ts...> {
public:
PIDResetIntegralTermAction(PIDClimate *parent) : parent_(parent) {}
void play(const Ts &...x) { this->parent_->reset_integral_term(); }
protected:
PIDClimate *parent_;
};
template<typename... Ts> class PIDSetControlParametersAction final : public Action<Ts...> {
public:
PIDSetControlParametersAction(PIDClimate *parent) : parent_(parent) {}
void play(const Ts &...x) {
auto kp = this->kp_.value(x...);
auto ki = this->ki_.value(x...);
auto kd = this->kd_.value(x...);
this->parent_->set_kp(kp);
this->parent_->set_ki(ki);
this->parent_->set_kd(kd);
}
protected:
TEMPLATABLE_VALUE(float, kp)
TEMPLATABLE_VALUE(float, ki)
TEMPLATABLE_VALUE(float, kd)
PIDClimate *parent_;
};
} // namespace esphome::pid
+57 -15
View File
@@ -2,7 +2,15 @@ import esphome.codegen as cg
from esphome.components import sensor
from esphome.components.const import CONF_CLIMATE_ID
import esphome.config_validation as cv
from esphome.const import CONF_TYPE, ICON_GAUGE, STATE_CLASS_MEASUREMENT, UNIT_PERCENT
from esphome.const import (
CONF_TYPE,
CONF_UNIT_OF_MEASUREMENT,
ICON_GAUGE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_EMPTY,
UNIT_PERCENT,
)
from esphome.types import ConfigType
from ..climate import PIDClimate, pid_ns
@@ -11,22 +19,55 @@ PIDClimateSensor = pid_ns.class_("PIDClimateSensor", sensor.Sensor, cg.Component
PIDClimateSensorType = pid_ns.enum("PIDClimateSensorType")
PID_CLIMATE_SENSOR_TYPES = {
"RESULT": PIDClimateSensorType.PID_SENSOR_TYPE_RESULT,
"ERROR": PIDClimateSensorType.PID_SENSOR_TYPE_ERROR,
"PROPORTIONAL": PIDClimateSensorType.PID_SENSOR_TYPE_PROPORTIONAL,
"INTEGRAL": PIDClimateSensorType.PID_SENSOR_TYPE_INTEGRAL,
"DERIVATIVE": PIDClimateSensorType.PID_SENSOR_TYPE_DERIVATIVE,
"HEAT": PIDClimateSensorType.PID_SENSOR_TYPE_HEAT,
"COOL": PIDClimateSensorType.PID_SENSOR_TYPE_COOL,
"KP": PIDClimateSensorType.PID_SENSOR_TYPE_KP,
"KI": PIDClimateSensorType.PID_SENSOR_TYPE_KI,
"KD": PIDClimateSensorType.PID_SENSOR_TYPE_KD,
"RESULT": (PIDClimateSensorType.PID_SENSOR_TYPE_RESULT, UNIT_PERCENT),
"ERROR": (PIDClimateSensorType.PID_SENSOR_TYPE_ERROR, UNIT_PERCENT),
"PROPORTIONAL": (PIDClimateSensorType.PID_SENSOR_TYPE_PROPORTIONAL, UNIT_PERCENT),
"INTEGRAL": (PIDClimateSensorType.PID_SENSOR_TYPE_INTEGRAL, UNIT_PERCENT),
"DERIVATIVE": (PIDClimateSensorType.PID_SENSOR_TYPE_DERIVATIVE, UNIT_PERCENT),
"HEAT": (PIDClimateSensorType.PID_SENSOR_TYPE_HEAT, UNIT_PERCENT),
"COOL": (PIDClimateSensorType.PID_SENSOR_TYPE_COOL, UNIT_PERCENT),
"KP": (PIDClimateSensorType.PID_SENSOR_TYPE_KP, UNIT_PERCENT),
"KI": (PIDClimateSensorType.PID_SENSOR_TYPE_KI, UNIT_PERCENT),
"KD": (PIDClimateSensorType.PID_SENSOR_TYPE_KD, UNIT_PERCENT),
"DEADBAND_THRESHOLD_HIGH": (
PIDClimateSensorType.PID_SENSOR_TYPE_DEADBAND_THRESHOLD_HIGH,
UNIT_CELSIUS,
),
"DEADBAND_THRESHOLD_LOW": (
PIDClimateSensorType.PID_SENSOR_TYPE_DEADBAND_THRESHOLD_LOW,
UNIT_CELSIUS,
),
"KP_DEADBAND_MULTIPLIER": (
PIDClimateSensorType.PID_SENSOR_TYPE_KP_DEADBAND_MULTIPLIER,
UNIT_EMPTY,
),
"KI_DEADBAND_MULTIPLIER": (
PIDClimateSensorType.PID_SENSOR_TYPE_KI_DEADBAND_MULTIPLIER,
UNIT_EMPTY,
),
"KD_DEADBAND_MULTIPLIER": (
PIDClimateSensorType.PID_SENSOR_TYPE_KD_DEADBAND_MULTIPLIER,
UNIT_EMPTY,
),
}
CONFIG_SCHEMA = (
PID_CLIMATE_SENSOR_ENUMS = {
sensor_type: sensor_config[0]
for sensor_type, sensor_config in PID_CLIMATE_SENSOR_TYPES.items()
}
def set_default_unit_of_measurement(config: ConfigType) -> ConfigType:
sensor_type = config[CONF_TYPE]
config.setdefault(
CONF_UNIT_OF_MEASUREMENT, PID_CLIMATE_SENSOR_TYPES[sensor_type][1]
)
return config
CONFIG_SCHEMA = cv.All(
sensor.sensor_schema(
PIDClimateSensor,
unit_of_measurement=UNIT_PERCENT,
icon=ICON_GAUGE,
accuracy_decimals=1,
state_class=STATE_CLASS_MEASUREMENT,
@@ -34,10 +75,11 @@ CONFIG_SCHEMA = (
.extend(
{
cv.GenerateID(CONF_CLIMATE_ID): cv.use_id(PIDClimate),
cv.Required(CONF_TYPE): cv.enum(PID_CLIMATE_SENSOR_TYPES, upper=True),
cv.Required(CONF_TYPE): cv.enum(PID_CLIMATE_SENSOR_ENUMS, upper=True),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
set_default_unit_of_measurement,
)
@@ -14,43 +14,55 @@ void PIDClimateSensor::update_from_parent_() {
float value;
switch (this->type_) {
case PID_SENSOR_TYPE_RESULT:
value = this->parent_->get_output_value();
value = this->parent_->get_output_value() * 100.0f;
break;
case PID_SENSOR_TYPE_ERROR:
value = this->parent_->get_error_value();
value = this->parent_->get_error_value() * 100.0f;
break;
case PID_SENSOR_TYPE_PROPORTIONAL:
value = this->parent_->get_proportional_term();
value = this->parent_->get_proportional_term() * 100.0f;
break;
case PID_SENSOR_TYPE_INTEGRAL:
value = this->parent_->get_integral_term();
value = this->parent_->get_integral_term() * 100.0f;
break;
case PID_SENSOR_TYPE_DERIVATIVE:
value = this->parent_->get_derivative_term();
value = this->parent_->get_derivative_term() * 100.0f;
break;
case PID_SENSOR_TYPE_HEAT:
value = clamp(this->parent_->get_output_value(), 0.0f, 1.0f);
value = clamp(this->parent_->get_output_value(), 0.0f, 1.0f) * 100.0f;
break;
case PID_SENSOR_TYPE_COOL:
value = clamp(-this->parent_->get_output_value(), 0.0f, 1.0f);
value = clamp(-this->parent_->get_output_value(), 0.0f, 1.0f) * 100.0f;
break;
case PID_SENSOR_TYPE_KP:
value = this->parent_->get_kp();
this->publish_state(value);
return;
break;
case PID_SENSOR_TYPE_KI:
value = this->parent_->get_ki();
this->publish_state(value);
return;
break;
case PID_SENSOR_TYPE_KD:
value = this->parent_->get_kd();
this->publish_state(value);
return;
break;
case PID_SENSOR_TYPE_DEADBAND_THRESHOLD_HIGH:
value = this->parent_->get_threshold_high();
break;
case PID_SENSOR_TYPE_DEADBAND_THRESHOLD_LOW:
value = this->parent_->get_threshold_low();
break;
case PID_SENSOR_TYPE_KP_DEADBAND_MULTIPLIER:
value = this->parent_->get_kp_multiplier();
break;
case PID_SENSOR_TYPE_KI_DEADBAND_MULTIPLIER:
value = this->parent_->get_ki_multiplier();
break;
case PID_SENSOR_TYPE_KD_DEADBAND_MULTIPLIER:
value = this->parent_->get_kd_multiplier();
break;
default:
value = NAN;
break;
}
this->publish_state(value * 100.0f);
this->publish_state(value);
}
void PIDClimateSensor::dump_config() { LOG_SENSOR("", "PID Climate Sensor", this); }
@@ -16,6 +16,11 @@ enum PIDClimateSensorType {
PID_SENSOR_TYPE_KP,
PID_SENSOR_TYPE_KI,
PID_SENSOR_TYPE_KD,
PID_SENSOR_TYPE_DEADBAND_THRESHOLD_HIGH,
PID_SENSOR_TYPE_DEADBAND_THRESHOLD_LOW,
PID_SENSOR_TYPE_KP_DEADBAND_MULTIPLIER,
PID_SENSOR_TYPE_KI_DEADBAND_MULTIPLIER,
PID_SENSOR_TYPE_KD_DEADBAND_MULTIPLIER,
};
class PIDClimateSensor final : public sensor::Sensor, public Component {
@@ -1,21 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/pulse_meter/pulse_meter_sensor.h"
namespace esphome::pulse_meter {
template<typename... Ts> class SetTotalPulsesAction final : public Action<Ts...> {
public:
SetTotalPulsesAction(PulseMeterSensor *pulse_meter) : pulse_meter_(pulse_meter) {}
TEMPLATABLE_VALUE(uint32_t, total_pulses)
void play(const Ts &...x) override { this->pulse_meter_->set_total_pulses(this->total_pulses_.value(x...)); }
protected:
PulseMeterSensor *pulse_meter_;
};
} // namespace esphome::pulse_meter
+3 -18
View File
@@ -19,8 +19,7 @@ from esphome.const import (
UNIT_PULSES,
UNIT_PULSES_PER_MINUTE,
)
from esphome.core import CORE, ID, TimePeriodMicroseconds
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.core import CORE, TimePeriodMicroseconds
from esphome.types import ConfigType
CODEOWNERS = ["@stevebaxter", "@cstaahl", "@TrentHouliston"]
@@ -38,8 +37,6 @@ FILTER_MODES = {
"PULSE": PulseMeterInternalFilterMode.FILTER_PULSE,
}
SetTotalPulsesAction = pulse_meter_ns.class_("SetTotalPulsesAction", automation.Action)
def validate_internal_filter(value: Any) -> TimePeriodMicroseconds:
return cv.positive_time_period_microseconds(value)
@@ -100,25 +97,13 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_total_sensor(sens))
@automation.register_action(
automation.register_apply_action(
"pulse_meter.set_total_pulses",
SetTotalPulsesAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(PulseMeterSensor),
cv.Required(CONF_VALUE): cv.templatable(cv.uint32_t),
}
),
synchronous=True,
automation.ApplyField(CONF_VALUE, "set_total_pulses", cg.uint32),
)
async def set_total_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_VALUE], args, cg.uint32)
cg.add(var.set_total_pulses(template_))
return var
+1 -1
View File
@@ -1149,7 +1149,7 @@ def gobox_dumper(var, config):
@register_action("gobox", GoboxAction, GOBOX_SCHEMA)
async def gobox_action(var, config, args):
template_ = await cg.templatable(config[CONF_CODE], args, cg.int_)
template_ = await cg.templatable(config[CONF_CODE], args, cg.uint64)
cg.add(var.set_code(template_))
+33 -97
View File
@@ -25,29 +25,6 @@ RFBridgeComponent = rf_bridge_ns.class_(
RFBridgeData = rf_bridge_ns.struct("RFBridgeData")
RFBridgeAdvancedData = rf_bridge_ns.struct("RFBridgeAdvancedData")
RFBridgeSendCodeAction = rf_bridge_ns.class_(
"RFBridgeSendCodeAction", automation.Action
)
RFBridgeSendAdvancedCodeAction = rf_bridge_ns.class_(
"RFBridgeSendAdvancedCodeAction", automation.Action
)
RFBridgeLearnAction = rf_bridge_ns.class_("RFBridgeLearnAction", automation.Action)
RFBridgeStartAdvancedSniffingAction = rf_bridge_ns.class_(
"RFBridgeStartAdvancedSniffingAction", automation.Action
)
RFBridgeStopAdvancedSniffingAction = rf_bridge_ns.class_(
"RFBridgeStopAdvancedSniffingAction", automation.Action
)
RFBridgeStartBucketSniffingAction = rf_bridge_ns.class_(
"RFBridgeStartBucketSniffingAction", automation.Action
)
RFBridgeBeepAction = rf_bridge_ns.class_("RFBridgeBeepAction", automation.Action)
RFBridgeSendRawAction = rf_bridge_ns.class_("RFBridgeSendRawAction", automation.Action)
CONF_ON_CODE_RECEIVED = "on_code_received"
CONF_ON_ADVANCED_CODE_RECEIVED = "on_advanced_code_received"
@@ -110,74 +87,48 @@ RFBRIDGE_SEND_CODE_SCHEMA = cv.Schema(
)
@automation.register_action(
automation.register_apply_action(
"rf_bridge.send_code",
RFBridgeSendCodeAction,
RFBRIDGE_SEND_CODE_SCHEMA,
synchronous=True,
automation.ApplyCall(
"send_code(rf_bridge::RFBridgeData{{.sync = {}, .low = {}, .high = {}, .code = {}}})",
(
(CONF_SYNC, cg.uint16),
(CONF_LOW, cg.uint16),
(CONF_HIGH, cg.uint16),
(CONF_CODE, cg.uint32),
),
),
)
async def rf_bridge_send_code_to_code(config, action_id, template_args, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_args, paren)
template_ = await cg.templatable(config[CONF_SYNC], args, cg.uint16)
cg.add(var.set_sync(template_))
template_ = await cg.templatable(config[CONF_LOW], args, cg.uint16)
cg.add(var.set_low(template_))
template_ = await cg.templatable(config[CONF_HIGH], args, cg.uint16)
cg.add(var.set_high(template_))
template_ = await cg.templatable(config[CONF_CODE], args, cg.uint32)
cg.add(var.set_code(template_))
return var
RFBRIDGE_ID_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(RFBridgeComponent)})
@automation.register_action(
"rf_bridge.learn", RFBridgeLearnAction, RFBRIDGE_ID_SCHEMA, synchronous=True
automation.register_apply_action(
"rf_bridge.learn", RFBRIDGE_ID_SCHEMA, automation.ApplyCall("learn()")
)
async def rf_bridge_learnx_to_code(config, action_id, template_args, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_args, paren)
@automation.register_action(
automation.register_apply_action(
"rf_bridge.start_advanced_sniffing",
RFBridgeStartAdvancedSniffingAction,
RFBRIDGE_ID_SCHEMA,
synchronous=True,
automation.ApplyCall("start_advanced_sniffing()"),
)
async def rf_bridge_start_advanced_sniffing_to_code(
config, action_id, template_args, args
):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_args, paren)
@automation.register_action(
automation.register_apply_action(
"rf_bridge.stop_advanced_sniffing",
RFBridgeStopAdvancedSniffingAction,
RFBRIDGE_ID_SCHEMA,
synchronous=True,
automation.ApplyCall("stop_advanced_sniffing()"),
)
async def rf_bridge_stop_advanced_sniffing_to_code(
config, action_id, template_args, args
):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_args, paren)
@automation.register_action(
automation.register_apply_action(
"rf_bridge.start_bucket_sniffing",
RFBridgeStartBucketSniffingAction,
RFBRIDGE_ID_SCHEMA,
synchronous=True,
automation.ApplyCall("start_bucket_sniffing()"),
)
async def rf_bridge_start_bucket_sniffing_to_code(
config, action_id, template_args, args
):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_args, paren)
RFBRIDGE_SEND_ADVANCED_CODE_SCHEMA = cv.Schema(
@@ -190,22 +141,18 @@ RFBRIDGE_SEND_ADVANCED_CODE_SCHEMA = cv.Schema(
)
@automation.register_action(
automation.register_apply_action(
"rf_bridge.send_advanced_code",
RFBridgeSendAdvancedCodeAction,
RFBRIDGE_SEND_ADVANCED_CODE_SCHEMA,
synchronous=True,
automation.ApplyCall(
"send_advanced_code(rf_bridge::RFBridgeAdvancedData{{.length = {}, .protocol = {}, .code = {}}})",
(
(CONF_LENGTH, cg.uint8),
(CONF_PROTOCOL, cg.uint8),
(CONF_CODE, cg.std_string),
),
),
)
async def rf_bridge_send_advanced_code_to_code(config, action_id, template_args, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_args, paren)
template_ = await cg.templatable(config[CONF_LENGTH], args, cg.uint8)
cg.add(var.set_length(template_))
template_ = await cg.templatable(config[CONF_PROTOCOL], args, cg.uint8)
cg.add(var.set_protocol(template_))
template_ = await cg.templatable(config[CONF_CODE], args, cg.std_string)
cg.add(var.set_code(template_))
return var
RFBRIDGE_SEND_RAW_SCHEMA = cv.Schema(
@@ -216,18 +163,11 @@ RFBRIDGE_SEND_RAW_SCHEMA = cv.Schema(
)
@automation.register_action(
automation.register_apply_action(
"rf_bridge.send_raw",
RFBridgeSendRawAction,
RFBRIDGE_SEND_RAW_SCHEMA,
synchronous=True,
automation.ApplyField(CONF_RAW, "send_raw", cg.std_string),
)
async def rf_bridge_send_raw_to_code(config, action_id, template_args, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_args, paren)
template_ = await cg.templatable(config[CONF_RAW], args, cg.std_string)
cg.add(var.set_raw(template_))
return var
RFBRIDGE_BEEP_SCHEMA = cv.Schema(
@@ -238,12 +178,8 @@ RFBRIDGE_BEEP_SCHEMA = cv.Schema(
)
@automation.register_action(
"rf_bridge.beep", RFBridgeBeepAction, RFBRIDGE_BEEP_SCHEMA, synchronous=True
automation.register_apply_action(
"rf_bridge.beep",
RFBRIDGE_BEEP_SCHEMA,
automation.ApplyField(CONF_DURATION, "beep", cg.uint16),
)
async def rf_bridge_beep_to_code(config, action_id, template_args, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_args, paren)
template_ = await cg.templatable(config[CONF_DURATION], args, cg.uint16)
cg.add(var.set_duration(template_))
return var
-103
View File
@@ -5,7 +5,6 @@
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/automation.h"
namespace esphome::rf_bridge {
@@ -89,106 +88,4 @@ class RFBridgeComponent final : public uart::UARTDevice, public Component {
CallbackManager<void(RFBridgeAdvancedData)> advanced_data_callback_;
};
template<typename... Ts> class RFBridgeSendCodeAction final : public Action<Ts...> {
public:
RFBridgeSendCodeAction(RFBridgeComponent *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(uint16_t, sync)
TEMPLATABLE_VALUE(uint16_t, low)
TEMPLATABLE_VALUE(uint16_t, high)
TEMPLATABLE_VALUE(uint32_t, code)
void play(const Ts &...x) {
RFBridgeData data{};
data.sync = this->sync_.value(x...);
data.low = this->low_.value(x...);
data.high = this->high_.value(x...);
data.code = this->code_.value(x...);
this->parent_->send_code(data);
}
protected:
RFBridgeComponent *parent_;
};
template<typename... Ts> class RFBridgeSendAdvancedCodeAction final : public Action<Ts...> {
public:
RFBridgeSendAdvancedCodeAction(RFBridgeComponent *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(uint8_t, length)
TEMPLATABLE_VALUE(uint8_t, protocol)
TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) {
RFBridgeAdvancedData data{};
data.length = this->length_.value(x...);
data.protocol = this->protocol_.value(x...);
data.code = this->code_.value(x...);
this->parent_->send_advanced_code(data);
}
protected:
RFBridgeComponent *parent_;
};
template<typename... Ts> class RFBridgeLearnAction final : public Action<Ts...> {
public:
RFBridgeLearnAction(RFBridgeComponent *parent) : parent_(parent) {}
void play(const Ts &...x) { this->parent_->learn(); }
protected:
RFBridgeComponent *parent_;
};
template<typename... Ts> class RFBridgeStartAdvancedSniffingAction final : public Action<Ts...> {
public:
RFBridgeStartAdvancedSniffingAction(RFBridgeComponent *parent) : parent_(parent) {}
void play(const Ts &...x) { this->parent_->start_advanced_sniffing(); }
protected:
RFBridgeComponent *parent_;
};
template<typename... Ts> class RFBridgeStopAdvancedSniffingAction final : public Action<Ts...> {
public:
RFBridgeStopAdvancedSniffingAction(RFBridgeComponent *parent) : parent_(parent) {}
void play(const Ts &...x) { this->parent_->stop_advanced_sniffing(); }
protected:
RFBridgeComponent *parent_;
};
template<typename... Ts> class RFBridgeStartBucketSniffingAction final : public Action<Ts...> {
public:
RFBridgeStartBucketSniffingAction(RFBridgeComponent *parent) : parent_(parent) {}
void play(const Ts &...x) { this->parent_->start_bucket_sniffing(); }
protected:
RFBridgeComponent *parent_;
};
template<typename... Ts> class RFBridgeSendRawAction final : public Action<Ts...> {
public:
RFBridgeSendRawAction(RFBridgeComponent *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(std::string, raw)
void play(const Ts &...x) { this->parent_->send_raw(this->raw_.value(x...)); }
protected:
RFBridgeComponent *parent_;
};
template<typename... Ts> class RFBridgeBeepAction final : public Action<Ts...> {
public:
RFBridgeBeepAction(RFBridgeComponent *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(uint16_t, duration)
void play(const Ts &...x) { this->parent_->beep(this->duration_.value(x...)); }
protected:
RFBridgeComponent *parent_;
};
} // namespace esphome::rf_bridge
+5 -41
View File
@@ -6,8 +6,6 @@ from esphome.components.output import FloatOutput
from esphome.components.speaker import Speaker
import esphome.config_validation as cv
from esphome.const import CONF_GAIN, CONF_ID, CONF_OUTPUT, CONF_PLATFORM, CONF_SPEAKER
from esphome.core import ID
from esphome.cpp_generator import MockObj, TemplateArgsType
import esphome.final_validate as fv
from esphome.types import ConfigType
@@ -20,8 +18,6 @@ CONF_ON_FINISHED_PLAYBACK = "on_finished_playback"
rtttl_ns = cg.esphome_ns.namespace("rtttl")
Rtttl = rtttl_ns.class_("Rtttl", cg.Component)
PlayAction = rtttl_ns.class_("PlayAction", automation.Action)
StopAction = rtttl_ns.class_("StopAction", automation.Action)
IsPlayingCondition = rtttl_ns.class_("IsPlayingCondition", automation.Condition)
MULTI_CONF = True
@@ -101,9 +97,8 @@ async def to_code(config: ConfigType) -> None:
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
@automation.register_action(
automation.register_apply_action(
"rtttl.play",
PlayAction,
cv.maybe_simple_value(
{
cv.GenerateID(CONF_ID): cv.use_id(Rtttl),
@@ -111,43 +106,21 @@ async def to_code(config: ConfigType) -> None:
},
key=CONF_RTTTL,
),
synchronous=True,
automation.ApplyField(CONF_RTTTL, "play", cg.std_string),
)
async def rtttl_play_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_RTTTL], args, cg.std_string)
cg.add(var.set_value(template_))
return var
@automation.register_action(
automation.register_apply_action(
"rtttl.stop",
StopAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(Rtttl),
}
),
synchronous=True,
automation.ApplyCall("stop()"),
)
async def rtttl_stop_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
@automation.register_condition(
automation.register_parented_condition(
"rtttl.is_playing",
IsPlayingCondition,
cv.Schema(
@@ -156,12 +129,3 @@ async def rtttl_stop_to_code(
}
),
)
async def rtttl_is_playing_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(condition_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
-16
View File
@@ -116,22 +116,6 @@ class Rtttl final : public Component {
#endif
};
template<typename... Ts> class PlayAction final : public Action<Ts...> {
public:
PlayAction(Rtttl *rtttl) : rtttl_(rtttl) {}
TEMPLATABLE_VALUE(std::string, value)
void play(const Ts &...x) override { this->rtttl_->play(this->value_.value(x...)); }
protected:
Rtttl *rtttl_;
};
template<typename... Ts> class StopAction final : public Action<Ts...>, public Parented<Rtttl> {
public:
void play(const Ts &...x) override { this->parent_->stop(); }
};
template<typename... Ts> class IsPlayingCondition final : public Condition<Ts...>, public Parented<Rtttl> {
public:
bool check(const Ts &...x) override { return this->parent_->is_playing(); }
+128
View File
@@ -0,0 +1,128 @@
#include "rx8025t.h"
#include "esphome/core/log.h"
// Datasheet:
// - https://support.epson.biz/td/api/doc_check.php?dl=app_RX8025T
namespace esphome::rx8025t {
static constexpr uint8_t RX8025T_REG_SEC = 0x00;
static constexpr uint8_t RX8025T_REG_FLAG = 0x0E;
static constexpr uint8_t RX8025T_FLAG_VDET = 0x01;
static constexpr uint8_t RX8025T_FLAG_VLF = 0x02;
static const char *const TAG = "rx8025t";
constexpr uint8_t bcd2dec(uint8_t val) { return (val >> 4) * 10 + (val & 0x0f); }
constexpr uint8_t dec2bcd(uint8_t val) { return ((val / 10) << 4) + (val % 10); }
void RX8025TComponent::setup() {
uint8_t flags;
if (!this->read_flags_(&flags)) {
this->mark_failed();
return;
}
if (flags & RX8025T_FLAG_VLF) {
ESP_LOGW(TAG, "VLF flag is set - Loss of oscillator detected. Time may be invalid.");
}
}
void RX8025TComponent::update() { this->read_time(); }
void RX8025TComponent::dump_config() {
ESP_LOGCONFIG(TAG, "RX8025T:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
time::RealTimeClock::dump_config();
}
bool RX8025TComponent::read_flags_(uint8_t *flags) {
if (!this->read_byte(RX8025T_REG_FLAG, flags)) {
ESP_LOGE(TAG, "Can't read flag register.");
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return false;
}
return true;
}
void RX8025TComponent::read_time() {
uint8_t flags;
if (!this->read_flags_(&flags)) {
return;
}
uint8_t date[7];
if (!this->read_bytes(RX8025T_REG_SEC, date, sizeof(date))) {
ESP_LOGE(TAG, "Can't read I2C data.");
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
this->status_clear_warning();
if (flags & RX8025T_FLAG_VLF) {
ESP_LOGW(TAG, "VLF flag is set - time data may be invalid, not syncing to system clock.");
return;
}
ESPTime rtc_time{
.second = bcd2dec(date[0] & 0x7f),
.minute = bcd2dec(date[1] & 0x7f),
.hour = bcd2dec(date[2] & 0x3f),
.day_of_week = static_cast<uint8_t>((date[3] & 0x7f) ? __builtin_ctz(date[3] & 0x7f) + 1 : 1),
.day_of_month = bcd2dec(date[4] & 0x3f),
.day_of_year = 1,
.month = bcd2dec(date[5] & 0x1f),
.year = static_cast<uint16_t>(bcd2dec(date[6]) + 2000),
.is_dst = false,
.timestamp = 0,
};
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}
ESP_LOGD(TAG, "Read UTC time: %04d-%02d-%02d %02d:%02d:%02d VDET:%s", rtc_time.year, rtc_time.month,
rtc_time.day_of_month, rtc_time.hour, rtc_time.minute, rtc_time.second, ONOFF(flags & RX8025T_FLAG_VDET));
this->synchronize_epoch_(rtc_time.timestamp);
}
void RX8025TComponent::write_time() {
auto now = this->utcnow();
if (!now.is_valid()) {
ESP_LOGE(TAG, "Invalid system time, not syncing to RTC.");
return;
}
uint8_t buff[7];
buff[0] = dec2bcd(now.second);
buff[1] = dec2bcd(now.minute);
buff[2] = dec2bcd(now.hour);
buff[3] = 1 << (now.day_of_week - 1);
buff[4] = dec2bcd(now.day_of_month);
buff[5] = dec2bcd(now.month);
buff[6] = dec2bcd(now.year % 100);
if (!this->write_bytes(RX8025T_REG_SEC, buff, sizeof(buff))) {
ESP_LOGE(TAG, "Can't write I2C data.");
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
ESP_LOGD(TAG, "Wrote UTC time: %04d-%02d-%02d %02d:%02d:%02d", now.year, now.month, now.day_of_month, now.hour,
now.minute, now.second);
// Clear VLF and VDET flags via read-modify-write of flag register
uint8_t flags;
if (!this->read_flags_(&flags)) {
return;
}
flags &= ~(RX8025T_FLAG_VLF | RX8025T_FLAG_VDET);
if (!this->write_byte(RX8025T_REG_FLAG, flags)) {
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
this->status_clear_warning();
}
} // namespace esphome::rx8025t
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/time/real_time_clock.h"
namespace esphome::rx8025t {
class RX8025TComponent : public time::RealTimeClock, public i2c::I2CDevice {
public:
void setup() override;
void update() override;
void dump_config() override;
void read_time();
void write_time();
protected:
bool read_flags_(uint8_t *flags);
};
} // namespace esphome::rx8025t
+42
View File
@@ -0,0 +1,42 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import i2c, time
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
CODEOWNERS = ["@remcom"]
DEPENDENCIES = ["i2c"]
rx8025t_ns = cg.esphome_ns.namespace("rx8025t")
RX8025TComponent = rx8025t_ns.class_(
"RX8025TComponent", time.RealTimeClock, i2c.I2CDevice
)
CONFIG_SCHEMA = time.TIME_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(RX8025TComponent),
}
).extend(i2c.i2c_device_schema(0x32))
for _name, _call in (
("rx8025t.write_time", "write_time()"),
("rx8025t.read_time", "read_time()"),
):
automation.register_apply_action(
_name,
automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(RX8025TComponent),
}
),
automation.ApplyCall(_call),
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
await time.register_time(var, config)
+1 -1
View File
@@ -211,7 +211,7 @@ bool Snapshot::take_snapshot(const char *filename) {
exact = !name.empty();
if (name_changed) {
ESP_LOGW(TAG, "Requested snapshot name '%s' is not an acceptable file name, using '%s' instead", filename,
name.empty() ? "a name made from the time" : name.c_str());
name.empty() ? LOG_STR_LITERAL("a name made from the time") : name.c_str());
}
}
if (name.empty()) {
+5 -1
View File
@@ -185,7 +185,11 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_
return 0;
}
#else
server->sin_addr.s_addr = inet_addr(ip_address);
// inet_aton() reports failure separately from the address, so 255.255.255.255 needs no special case
if (inet_aton(ip_address, &server->sin_addr) == 0) {
errno = EINVAL;
return 0;
}
#endif
server->sin_port = htons(port);
return sizeof(sockaddr_in);
+11 -44
View File
@@ -18,21 +18,6 @@ Speaker = speaker_ns.class_("Speaker")
PlayAction = speaker_ns.class_(
"PlayAction", automation.Action, cg.Parented.template(Speaker)
)
StopAction = speaker_ns.class_(
"StopAction", automation.Action, cg.Parented.template(Speaker)
)
FinishAction = speaker_ns.class_(
"FinishAction", automation.Action, cg.Parented.template(Speaker)
)
VolumeSetAction = speaker_ns.class_(
"VolumeSetAction", automation.Action, cg.Parented.template(Speaker)
)
MuteOnAction = speaker_ns.class_(
"MuteOnAction", automation.Action, cg.Parented.template(Speaker)
)
MuteOffAction = speaker_ns.class_(
"MuteOffAction", automation.Action, cg.Parented.template(Speaker)
)
IsPlayingCondition = speaker_ns.class_("IsPlayingCondition", automation.Condition)
@@ -96,12 +81,15 @@ async def speaker_play_action(config, action_id, template_arg, args):
return var
automation.register_action(
"speaker.stop", StopAction, SPEAKER_AUTOMATION_SCHEMA, synchronous=True
)(speaker_action)
automation.register_action(
"speaker.finish", FinishAction, SPEAKER_AUTOMATION_SCHEMA, synchronous=True
)(speaker_action)
for _name, _call in (
("speaker.stop", "stop()"),
("speaker.finish", "finish()"),
("speaker.mute_on", "set_mute_state(true)"),
("speaker.mute_off", "set_mute_state(false)"),
):
automation.register_apply_action(
_name, SPEAKER_AUTOMATION_SCHEMA, automation.ApplyCall(_call)
)
automation.register_condition(
"speaker.is_playing", IsPlayingCondition, SPEAKER_AUTOMATION_SCHEMA
@@ -112,9 +100,8 @@ automation.register_condition(
)(speaker_action)
@automation.register_action(
automation.register_apply_action(
"speaker.volume_set",
VolumeSetAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(Speaker),
@@ -122,28 +109,8 @@ automation.register_condition(
},
key=CONF_VOLUME,
),
synchronous=True,
automation.ApplyField(CONF_VOLUME, "set_volume", cg.float_),
)
async def speaker_volume_set_action(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
volume = await cg.templatable(config[CONF_VOLUME], args, cg.float_)
cg.add(var.set_volume(volume))
return var
@automation.register_action(
"speaker.mute_off",
MuteOffAction,
SPEAKER_AUTOMATION_SCHEMA,
synchronous=True,
)
@automation.register_action(
"speaker.mute_on", MuteOnAction, SPEAKER_AUTOMATION_SCHEMA, synchronous=True
)
async def speaker_mute_action_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)
@coroutine_with_priority(CoroPriority.CORE)
-35
View File
@@ -38,41 +38,6 @@ template<typename... Ts> class PlayAction final : public Action<Ts...>, public P
} data_;
};
template<typename... Ts> class VolumeSetAction final : public Action<Ts...>, public Parented<Speaker> {
TEMPLATABLE_VALUE(float, volume)
void play(const Ts &...x) override { this->parent_->set_volume(this->volume_.value(x...)); }
};
template<typename... Ts> class MuteOnAction final : public Action<Ts...> {
public:
explicit MuteOnAction(Speaker *speaker) : speaker_(speaker) {}
void play(const Ts &...x) override { this->speaker_->set_mute_state(true); }
protected:
Speaker *speaker_;
};
template<typename... Ts> class MuteOffAction final : public Action<Ts...> {
public:
explicit MuteOffAction(Speaker *speaker) : speaker_(speaker) {}
void play(const Ts &...x) override { this->speaker_->set_mute_state(false); }
protected:
Speaker *speaker_;
};
template<typename... Ts> class StopAction final : public Action<Ts...>, public Parented<Speaker> {
public:
void play(const Ts &...x) override { this->parent_->stop(); }
};
template<typename... Ts> class FinishAction final : public Action<Ts...>, public Parented<Speaker> {
public:
void play(const Ts &...x) override { this->parent_->finish(); }
};
template<typename... Ts> class IsPlayingCondition final : public Condition<Ts...>, public Parented<Speaker> {
public:
bool check(const Ts &...x) override { return this->parent_->is_running(); }
@@ -22,7 +22,6 @@ from esphome.const import (
CONF_BUFFER_SIZE,
CONF_FILES,
CONF_FORMAT,
CONF_ID,
CONF_NUM_CHANNELS,
CONF_ON_TURN_OFF,
CONF_ON_TURN_ON,
@@ -63,11 +62,6 @@ AUDIO_PIPELINE_TYPE_ENUM = {
"ANNOUNCEMENT": AudioPipelineType.ANNOUNCEMENT,
}
PlayOnDeviceMediaAction = speaker_ns.class_(
"PlayOnDeviceMediaAction",
automation.Action,
cg.Parented.template(SpeakerMediaPlayer),
)
StopStreamAction = speaker_ns.class_(
"StopStreamAction", automation.Action, cg.Parented.template(SpeakerMediaPlayer)
)
@@ -239,9 +233,8 @@ async def to_code(config):
audio_file.generate_audio_file_code(file_config)
@automation.register_action(
automation.register_apply_action(
"media_player.speaker.play_on_device_media_file",
PlayOnDeviceMediaAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(SpeakerMediaPlayer),
@@ -251,17 +244,18 @@ async def to_code(config):
},
key=CONF_MEDIA_FILE,
),
synchronous=True,
automation.ApplyCall(
"play_file({}, {}, {})",
(
# The file is a use_id of a global AudioFile pointer; global-scope qualified
# so a trigger arg with the same name cannot shadow it.
(
CONF_MEDIA_FILE,
audio.AudioFile.operator("ptr"),
lambda _, value: f"::{value}",
),
(CONF_ANNOUNCEMENT, cg.bool_),
(CONF_ENQUEUE, cg.bool_),
),
),
)
async def play_on_device_media_media_action(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
media_file = await cg.get_variable(config[CONF_MEDIA_FILE])
announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_)
enqueue = await cg.templatable(config[CONF_ENQUEUE], args, cg.bool_)
template_ = await cg.templatable(media_file, args, audio.AudioFile.operator("ptr"))
cg.add(var.set_audio_file(template_))
cg.add(var.set_announcement(announcement))
cg.add(var.set_enqueue(enqueue))
return var
@@ -1,25 +0,0 @@
#pragma once
#include "speaker_media_player.h"
#ifdef USE_ESP32
#include "esphome/components/audio/audio.h"
#include "esphome/core/automation.h"
namespace esphome::speaker {
template<typename... Ts>
class PlayOnDeviceMediaAction final : public Action<Ts...>, public Parented<SpeakerMediaPlayer> {
TEMPLATABLE_VALUE(audio::AudioFile *, audio_file)
TEMPLATABLE_VALUE(bool, announcement)
TEMPLATABLE_VALUE(bool, enqueue)
void play(const Ts &...x) override {
this->parent_->play_file(this->audio_file_.value(x...), this->announcement_.value(x...),
this->enqueue_.value(x...));
}
};
} // namespace esphome::speaker
#endif
+12 -60
View File
@@ -66,14 +66,9 @@ SprinklerControllerSwitch = sprinkler_ns.class_(
"SprinklerControllerSwitch", switch.Switch, cg.Component
)
SetDividerAction = sprinkler_ns.class_("SetDividerAction", automation.Action)
SetMultiplierAction = sprinkler_ns.class_("SetMultiplierAction", automation.Action)
QueueValveAction = sprinkler_ns.class_("QueueValveAction", automation.Action)
ClearQueuedValvesAction = sprinkler_ns.class_(
"ClearQueuedValvesAction", automation.Action
)
SetRepeatAction = sprinkler_ns.class_("SetRepeatAction", automation.Action)
SetRunDurationAction = sprinkler_ns.class_("SetRunDurationAction", automation.Action)
StartFromQueueAction = sprinkler_ns.class_("StartFromQueueAction", automation.Action)
StartFullCycleAction = sprinkler_ns.class_("StartFullCycleAction", automation.Action)
StartSingleValveAction = sprinkler_ns.class_(
@@ -418,80 +413,37 @@ CONFIG_SCHEMA = cv.All(
)
@automation.register_action(
_VALVE_AND_DURATION = ((CONF_VALVE_NUMBER, cg.size_t), (CONF_RUN_DURATION, cg.uint32))
automation.register_apply_action(
"sprinkler.set_divider",
SetDividerAction,
SPRINKLER_ACTION_SET_DIVIDER_SCHEMA,
synchronous=True,
automation.ApplyField(CONF_DIVIDER, "set_divider", cg.uint32),
)
async def sprinkler_set_divider_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_DIVIDER], args, cg.uint32)
cg.add(var.set_divider(template_))
return var
@automation.register_action(
automation.register_apply_action(
"sprinkler.set_multiplier",
SetMultiplierAction,
SPRINKLER_ACTION_SET_MULTIPLIER_SCHEMA,
synchronous=True,
automation.ApplyField(CONF_MULTIPLIER, "set_multiplier", cg.float_),
)
async def sprinkler_set_multiplier_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_MULTIPLIER], args, cg.float_)
cg.add(var.set_multiplier(template_))
return var
@automation.register_action(
automation.register_apply_action(
"sprinkler.queue_valve",
QueueValveAction,
SPRINKLER_ACTION_QUEUE_VALVE_SCHEMA,
synchronous=True,
automation.ApplyCall("queue_valve({}, {})", _VALVE_AND_DURATION),
)
async def sprinkler_set_queued_valve_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_VALVE_NUMBER], args, cg.size_t)
cg.add(var.set_valve_number(template_))
template_ = await cg.templatable(config[CONF_RUN_DURATION], args, cg.uint32)
cg.add(var.set_valve_run_duration(template_))
return var
@automation.register_action(
automation.register_apply_action(
"sprinkler.set_repeat",
SetRepeatAction,
SPRINKLER_ACTION_REPEAT_SCHEMA,
synchronous=True,
automation.ApplyField(CONF_REPEAT, "set_repeat", cg.uint32),
)
async def sprinkler_set_repeat_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_REPEAT], args, cg.uint32)
cg.add(var.set_repeat(template_))
return var
@automation.register_action(
automation.register_apply_action(
"sprinkler.set_valve_run_duration",
SetRunDurationAction,
SPRINKLER_ACTION_SET_RUN_DURATION_SCHEMA,
synchronous=True,
automation.ApplyCall("set_valve_run_duration({}, {})", _VALVE_AND_DURATION),
)
async def sprinkler_set_valve_run_duration_to_code(
config, action_id, template_arg, args
):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_VALVE_NUMBER], args, cg.size_t)
cg.add(var.set_valve_number(template_))
template_ = await cg.templatable(config[CONF_RUN_DURATION], args, cg.uint32)
cg.add(var.set_valve_run_duration(template_))
return var
@automation.register_action(
-68
View File
@@ -6,46 +6,6 @@
namespace esphome::sprinkler {
template<typename... Ts> class SetDividerAction final : public Action<Ts...> {
public:
explicit SetDividerAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {}
TEMPLATABLE_VALUE(uint32_t, divider)
void play(const Ts &...x) override { this->sprinkler_->set_divider(this->divider_.optional_value(x...)); }
protected:
Sprinkler *sprinkler_;
};
template<typename... Ts> class SetMultiplierAction final : public Action<Ts...> {
public:
explicit SetMultiplierAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {}
TEMPLATABLE_VALUE(float, multiplier)
void play(const Ts &...x) override { this->sprinkler_->set_multiplier(this->multiplier_.optional_value(x...)); }
protected:
Sprinkler *sprinkler_;
};
template<typename... Ts> class QueueValveAction final : public Action<Ts...> {
public:
explicit QueueValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {}
TEMPLATABLE_VALUE(size_t, valve_number)
TEMPLATABLE_VALUE(uint32_t, valve_run_duration)
void play(const Ts &...x) override {
this->sprinkler_->queue_valve(this->valve_number_.optional_value(x...),
this->valve_run_duration_.optional_value(x...));
}
protected:
Sprinkler *sprinkler_;
};
template<typename... Ts> class ClearQueuedValvesAction final : public Action<Ts...> {
public:
explicit ClearQueuedValvesAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {}
@@ -56,34 +16,6 @@ template<typename... Ts> class ClearQueuedValvesAction final : public Action<Ts.
Sprinkler *sprinkler_;
};
template<typename... Ts> class SetRepeatAction final : public Action<Ts...> {
public:
explicit SetRepeatAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {}
TEMPLATABLE_VALUE(uint32_t, repeat)
void play(const Ts &...x) override { this->sprinkler_->set_repeat(this->repeat_.optional_value(x...)); }
protected:
Sprinkler *sprinkler_;
};
template<typename... Ts> class SetRunDurationAction final : public Action<Ts...> {
public:
explicit SetRunDurationAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {}
TEMPLATABLE_VALUE(size_t, valve_number)
TEMPLATABLE_VALUE(uint32_t, valve_run_duration)
void play(const Ts &...x) override {
this->sprinkler_->set_valve_run_duration(this->valve_number_.optional_value(x...),
this->valve_run_duration_.optional_value(x...));
}
protected:
Sprinkler *sprinkler_;
};
template<typename... Ts> class StartFromQueueAction final : public Action<Ts...> {
public:
explicit StartFromQueueAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {}
+12 -28
View File
@@ -54,12 +54,6 @@ RESTORE_MODES = {
}
ControlAction = switch_ns.class_("ControlAction", automation.Action)
ToggleAction = switch_ns.class_("ToggleAction", automation.Action)
TurnOffAction = switch_ns.class_("TurnOffAction", automation.Action)
TurnOnAction = switch_ns.class_("TurnOnAction", automation.Action)
SwitchPublishAction = switch_ns.class_("SwitchPublishAction", automation.Action)
SwitchCondition = switch_ns.class_("SwitchCondition", Condition)
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True)
@@ -193,29 +187,19 @@ SWITCH_CONTROL_ACTION_SCHEMA = automation.maybe_simple_id(
)
@automation.register_action(
"switch.control", ControlAction, SWITCH_CONTROL_ACTION_SCHEMA, synchronous=True
automation.register_apply_action(
"switch.control",
SWITCH_CONTROL_ACTION_SCHEMA,
automation.ApplyField(CONF_STATE, "control", cg.bool_),
)
async def switch_control_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_)
cg.add(var.set_state(template_))
return var
@automation.register_action(
"switch.toggle", ToggleAction, SWITCH_ACTION_SCHEMA, synchronous=True
)
@automation.register_action(
"switch.turn_off", TurnOffAction, SWITCH_ACTION_SCHEMA, synchronous=True
)
@automation.register_action(
"switch.turn_on", TurnOnAction, SWITCH_ACTION_SCHEMA, synchronous=True
)
async def switch_toggle_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
for _name, _call in (
("switch.toggle", "toggle()"),
("switch.turn_off", "turn_off()"),
("switch.turn_on", "turn_on()"),
):
automation.register_apply_action(
_name, SWITCH_ACTION_SCHEMA, automation.ApplyCall(_call)
)
@automation.register_condition("switch.is_on", SwitchCondition, SWITCH_ACTION_SCHEMA)
-58
View File
@@ -6,53 +6,6 @@
namespace esphome::switch_ {
template<typename... Ts> class TurnOnAction final : public Action<Ts...> {
public:
explicit TurnOnAction(Switch *a_switch) : switch_(a_switch) {}
void play(const Ts &...x) override { this->switch_->turn_on(); }
protected:
Switch *switch_;
};
template<typename... Ts> class TurnOffAction final : public Action<Ts...> {
public:
explicit TurnOffAction(Switch *a_switch) : switch_(a_switch) {}
void play(const Ts &...x) override { this->switch_->turn_off(); }
protected:
Switch *switch_;
};
template<typename... Ts> class ToggleAction final : public Action<Ts...> {
public:
explicit ToggleAction(Switch *a_switch) : switch_(a_switch) {}
void play(const Ts &...x) override { this->switch_->toggle(); }
protected:
Switch *switch_;
};
template<typename... Ts> class ControlAction final : public Action<Ts...> {
public:
explicit ControlAction(Switch *a_switch) : switch_(a_switch) {}
TEMPLATABLE_VALUE(bool, state)
void play(const Ts &...x) override {
auto state = this->state_.optional_value(x...);
if (state.has_value()) {
this->switch_->control(*state);
}
}
protected:
Switch *switch_;
};
template<typename... Ts> class SwitchCondition final : public Condition<Ts...> {
public:
SwitchCondition(Switch *parent, bool state) : parent_(parent), state_(state) {}
@@ -92,15 +45,4 @@ class SwitchTurnOffTrigger final : public Trigger<> {
}
};
template<typename... Ts> class SwitchPublishAction final : public Action<Ts...> {
public:
SwitchPublishAction(Switch *a_switch) : switch_(a_switch) {}
TEMPLATABLE_VALUE(bool, state)
void play(const Ts &...x) override { this->switch_->publish_state(this->state_.value(x...)); }
protected:
Switch *switch_;
};
} // namespace esphome::switch_
+11 -1
View File
@@ -4,7 +4,7 @@ from esphome.components.logger import LOG_LEVELS, is_log_level, request_log_list
from esphome.components.time import RealTimeClock
from esphome.components.udp import CONF_UDP_ID
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_LEVEL, CONF_PORT, CONF_TIME_ID
from esphome.const import CONF_FORMAT, CONF_ID, CONF_LEVEL, CONF_PORT, CONF_TIME_ID
from esphome.cpp_types import Component, Parented
from esphome.types import ConfigType
@@ -14,6 +14,12 @@ DEPENDENCIES = ["udp", "logger", "time"]
syslog_ns = cg.esphome_ns.namespace("syslog")
Syslog = syslog_ns.class_("Syslog", Component, Parented.template(udp.UDPComponent))
SyslogFormat = syslog_ns.enum("SyslogFormat")
SYSLOG_FORMATS = {
"RFC3164": SyslogFormat.SYSLOG_FORMAT_RFC3164,
"RFC5424": SyslogFormat.SYSLOG_FORMAT_RFC5424,
}
CONF_STRIP = "strip"
CONF_FACILITY = "facility"
@@ -25,6 +31,9 @@ CONFIG_SCHEMA = udp.UDP_SCHEMA.extend(
cv.Optional(CONF_LEVEL, default="DEBUG"): is_log_level,
cv.Optional(CONF_STRIP, default=True): cv.boolean,
cv.Optional(CONF_FACILITY, default=16): cv.int_range(0, 23),
cv.Optional(CONF_FORMAT, default="RFC3164"): cv.enum(
SYSLOG_FORMATS, upper=True
),
}
)
@@ -41,3 +50,4 @@ async def to_code(config: ConfigType) -> None:
await cg.register_parented(var, parent)
cg.add(var.set_strip(config[CONF_STRIP]))
cg.add(var.set_facility(config[CONF_FACILITY]))
cg.add(var.set_format(config[CONF_FORMAT]))
+24 -14
View File
@@ -49,27 +49,37 @@ void Syslog::log_(const int level, const char *tag, const char *message, size_t
// Build syslog packet on stack (508 bytes chosen as practical limit for syslog over UDP)
char packet[508];
size_t offset = 0;
size_t remaining = sizeof(packet);
// Write PRI - abort if this fails as packet would be malformed
offset = buf_append_printf(packet, sizeof(packet), 0, "<%d>", pri);
if (offset == 0) {
return; // PRI always produces at least "<0>" (3 chars), so 0 means error
}
remaining -= offset;
// Write timestamp directly into packet (RFC 5424: use "-" if time not valid or strftime fails)
auto now = this->time_->now();
size_t ts_written = now.is_valid() ? now.strftime(packet + offset, remaining, "%b %e %H:%M:%S") : 0;
if (ts_written > 0) {
offset += ts_written;
} else if (remaining > 0) {
packet[offset++] = '-';
}
if (this->format_ == SYSLOG_FORMAT_RFC5424) {
offset = buf_append_str(packet, sizeof(packet), offset, "1 ");
// Write hostname, tag, and message
offset = buf_append_printf(packet, sizeof(packet), offset, " %s %s: %.*s", App.get_name().c_str(), tag, (int) len,
message);
char timestamp[32];
size_t timestamp_len = now.is_valid() ? now.strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S%z") : 0;
if (timestamp_len == 24) {
// ESPTime formats the numeric offset as +HHMM. RFC 3339 requires +HH:MM.
timestamp[25] = '\0';
timestamp[24] = timestamp[23];
timestamp[23] = timestamp[22];
timestamp[22] = ':';
offset = buf_append_printf(packet, sizeof(packet), offset, "%s", timestamp);
} else {
offset = buf_append_str(packet, sizeof(packet), offset, "-");
}
offset = buf_append_printf(packet, sizeof(packet), offset, " %s %s - - - %.*s", App.get_name().c_str(), tag,
(int) len, message);
} else {
// RFC 3164 has no NILVALUE. If the clock is invalid, omit TIMESTAMP so a relay can add it.
if (now.is_valid()) {
offset += now.strftime(packet + offset, sizeof(packet) - offset, "%b %e %H:%M:%S ");
}
offset = buf_append_printf(packet, sizeof(packet), offset, "%s %s: %.*s", App.get_name().c_str(), tag, (int) len,
message);
}
// Clamp to exclude null terminator position if buffer was filled
if (offset >= sizeof(packet)) {
offset = sizeof(packet) - 1;
@@ -7,6 +7,11 @@
#ifdef USE_NETWORK
namespace esphome::syslog {
enum SyslogFormat : uint8_t {
SYSLOG_FORMAT_RFC3164,
SYSLOG_FORMAT_RFC5424,
};
class Syslog final : public Component, public Parented<udp::UDPComponent> {
public:
Syslog(int level, time::RealTimeClock *time) : log_level_(level), time_(time) {}
@@ -14,6 +19,7 @@ class Syslog final : public Component, public Parented<udp::UDPComponent> {
void on_log(uint8_t level, const char *tag, const char *message, size_t message_len);
void set_strip(bool strip) { this->strip_ = strip; }
void set_facility(int facility) { this->facility_ = facility; }
void set_format(SyslogFormat format) { this->format_ = format; }
protected:
int log_level_;
@@ -21,6 +27,7 @@ class Syslog final : public Component, public Parented<udp::UDPComponent> {
time::RealTimeClock *time_;
bool strip_{true};
int facility_{16};
SyslogFormat format_{SYSLOG_FORMAT_RFC3164};
};
} // namespace esphome::syslog
#endif
+25 -51
View File
@@ -28,8 +28,6 @@ from esphome.const import (
CONF_TARGET_TEMPERATURE_HIGH,
CONF_TARGET_TEMPERATURE_LOW,
)
from esphome.core import ID
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
from .. import template_ns
@@ -53,11 +51,6 @@ CONF_SET_PRESET_ACTION = "set_preset_action"
CONF_SET_CUSTOM_PRESET_ACTION = "set_custom_preset_action"
TemplateClimate = template_ns.class_("TemplateClimate", climate.Climate, cg.Component)
TemplateClimatePublishAction = template_ns.class_(
"TemplateClimatePublishAction",
automation.Action,
cg.Parented.template(TemplateClimate),
)
TemplateClimateRestoreMode = template_ns.enum(
"TemplateClimateRestoreMode", is_class=True
@@ -416,50 +409,31 @@ CLIMATE_TEMPLATE_PUBLISH_ACTION_SCHEMA = cv.All(
)
@automation.register_action(
automation.register_apply_action(
"climate.template.publish",
TemplateClimatePublishAction,
CLIMATE_TEMPLATE_PUBLISH_ACTION_SCHEMA,
synchronous=True,
automation.ApplyField(
CONF_CURRENT_TEMPERATURE, "current_temperature = {}", cg.float_
),
automation.ApplyField(CONF_CURRENT_HUMIDITY, "current_humidity = {}", cg.float_),
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_MODE, "set_mode", climate.ClimateMode),
automation.ApplyField(CONF_ACTION, "action = {}", climate.ClimateAction),
automation.ApplyField(CONF_FAN_MODE, "set_fan_mode", climate.ClimateFanMode),
automation.ApplyField(
CONF_CUSTOM_FAN_MODE, "set_custom_fan_mode(StringRef({}))", cg.std_string
),
automation.ApplyField(CONF_SWING_MODE, "set_swing_mode", climate.ClimateSwingMode),
automation.ApplyField(CONF_PRESET, "set_preset", climate.ClimatePreset),
automation.ApplyField(
CONF_CUSTOM_PRESET, "set_custom_preset(StringRef({}))", cg.std_string
),
automation.ApplyCall("publish_state()"),
)
async def climate_template_publish_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])
if (v := config.get(CONF_CURRENT_TEMPERATURE)) is not None:
cg.add(var.set_current_temperature(await cg.templatable(v, args, cg.float_)))
if (v := config.get(CONF_CURRENT_HUMIDITY)) is not None:
cg.add(var.set_current_humidity(await cg.templatable(v, args, cg.float_)))
if (v := config.get(CONF_TARGET_TEMPERATURE)) is not None:
cg.add(var.set_target_temperature(await cg.templatable(v, args, cg.float_)))
if (v := config.get(CONF_TARGET_TEMPERATURE_LOW)) is not None:
cg.add(var.set_target_temperature_low(await cg.templatable(v, args, cg.float_)))
if (v := config.get(CONF_TARGET_TEMPERATURE_HIGH)) is not None:
cg.add(
var.set_target_temperature_high(await cg.templatable(v, args, cg.float_))
)
if (v := config.get(CONF_TARGET_HUMIDITY)) is not None:
cg.add(var.set_target_humidity(await cg.templatable(v, args, cg.float_)))
if (v := config.get(CONF_MODE)) is not None:
cg.add(var.set_mode(await cg.templatable(v, args, climate.ClimateMode)))
if (v := config.get(CONF_ACTION)) is not None:
cg.add(var.set_action(await cg.templatable(v, args, climate.ClimateAction)))
if (v := config.get(CONF_FAN_MODE)) is not None:
cg.add(var.set_fan_mode(await cg.templatable(v, args, climate.ClimateFanMode)))
if (v := config.get(CONF_CUSTOM_FAN_MODE)) is not None:
cg.add(var.set_custom_fan_mode(await cg.templatable(v, args, cg.std_string)))
if (v := config.get(CONF_SWING_MODE)) is not None:
cg.add(
var.set_swing_mode(await cg.templatable(v, args, climate.ClimateSwingMode))
)
if (v := config.get(CONF_PRESET)) is not None:
cg.add(var.set_preset(await cg.templatable(v, args, climate.ClimatePreset)))
if (v := config.get(CONF_CUSTOM_PRESET)) is not None:
cg.add(var.set_custom_preset(await cg.templatable(v, args, cg.std_string)))
return var
@@ -1,57 +0,0 @@
#pragma once
#include "template_climate.h"
#include "esphome/core/automation.h"
namespace esphome::template_ {
template<typename... Ts>
class TemplateClimatePublishAction final : public Action<Ts...>, public Parented<TemplateClimate> {
public:
TEMPLATABLE_VALUE(float, current_temperature)
TEMPLATABLE_VALUE(float, current_humidity)
TEMPLATABLE_VALUE(float, target_temperature)
TEMPLATABLE_VALUE(float, target_temperature_low)
TEMPLATABLE_VALUE(float, target_temperature_high)
TEMPLATABLE_VALUE(float, target_humidity)
TEMPLATABLE_VALUE(climate::ClimateMode, mode)
TEMPLATABLE_VALUE(climate::ClimateAction, action)
TEMPLATABLE_VALUE(climate::ClimateFanMode, fan_mode)
TEMPLATABLE_VALUE(std::string, custom_fan_mode)
TEMPLATABLE_VALUE(climate::ClimateSwingMode, swing_mode)
TEMPLATABLE_VALUE(climate::ClimatePreset, preset)
TEMPLATABLE_VALUE(std::string, custom_preset)
void play(const Ts &...x) override {
if (this->current_temperature_.has_value())
this->parent_->current_temperature = this->current_temperature_.value(x...);
if (this->current_humidity_.has_value())
this->parent_->current_humidity = this->current_humidity_.value(x...);
if (this->target_temperature_.has_value())
this->parent_->set_target_temperature(this->target_temperature_.value(x...));
if (this->target_temperature_low_.has_value())
this->parent_->set_target_temperature_low(this->target_temperature_low_.value(x...));
if (this->target_temperature_high_.has_value())
this->parent_->set_target_temperature_high(this->target_temperature_high_.value(x...));
if (this->target_humidity_.has_value())
this->parent_->set_target_humidity(this->target_humidity_.value(x...));
if (this->mode_.has_value())
this->parent_->set_mode(this->mode_.value(x...));
if (this->action_.has_value())
this->parent_->action = this->action_.value(x...);
if (this->fan_mode_.has_value())
this->parent_->set_fan_mode(this->fan_mode_.value(x...));
if (this->custom_fan_mode_.has_value())
this->parent_->set_custom_fan_mode(StringRef(this->custom_fan_mode_.value(x...)));
if (this->swing_mode_.has_value())
this->parent_->set_swing_mode(this->swing_mode_.value(x...));
if (this->preset_.has_value())
this->parent_->set_preset(this->preset_.value(x...));
if (this->custom_preset_.has_value())
this->parent_->set_custom_preset(StringRef(this->custom_preset_.value(x...)));
this->parent_->publish_state();
}
};
} // namespace esphome::template_
@@ -50,8 +50,7 @@ class TemplateClimate final : public climate::Climate, public Component {
Trigger<climate::ClimatePreset> *get_set_preset_trigger() { return &this->set_preset_trigger_; }
Trigger<StringRef> *get_set_custom_preset_trigger() { return &this->set_custom_preset_trigger_; }
// Used by TemplateClimatePublishAction, which is not a Climate subclass and so cannot reach the
// protected setters, and by codegen to apply `initial_state:` before setup() runs.
// Public so the climate.template.publish action and the initial_state codegen can set them.
void set_target_temperature(float value) { this->target_temperature = value; }
void set_target_temperature_low(float value) { this->target_temperature_low = value; }
void set_target_temperature_high(float value) { this->target_temperature_high = value; }
+8 -31
View File
@@ -20,9 +20,6 @@ from esphome.const import (
CONF_TILT_ACTION,
CONF_TILT_LAMBDA,
)
from esphome.core import ID
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType, TemplateArgsType
from .. import template_ns
@@ -120,17 +117,8 @@ async def to_code(config):
# CONF_STATE and CONF_POSITION are cv.Exclusive in the schema, so at most
# one is present and both map to the position field.
_COVER_PUBLISH_FIELDS: tuple[cover.ApplyField, ...] = (
cover.ApplyField(CONF_STATE, "position", cg.float_),
cover.ApplyField(CONF_POSITION, "position", cg.float_),
cover.ApplyField(CONF_TILT, "tilt", cg.float_),
cover.ApplyField(CONF_CURRENT_OPERATION, "current_operation", cover.CoverOperation),
)
@automation.register_action(
automation.register_apply_action(
"cover.template.publish",
cover.CoverPublishAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(cover.Cover),
@@ -142,22 +130,11 @@ _COVER_PUBLISH_FIELDS: tuple[cover.ApplyField, ...] = (
cv.Optional(CONF_TILT): cv.templatable(cv.zero_to_one_float),
}
),
synchronous=True,
automation.ApplyField(CONF_STATE, "position = {}", cg.float_),
automation.ApplyField(CONF_POSITION, "position = {}", cg.float_),
automation.ApplyField(CONF_TILT, "tilt = {}", cg.float_),
automation.ApplyField(
CONF_CURRENT_OPERATION, "current_operation = {}", cover.CoverOperation
),
automation.ApplyCall("publish_state()"),
)
async def cover_template_publish_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
# Mutates Cover fields directly (no CoverCall) since publish is a state
# push, not a control request.
return await cover.build_apply_lambda_action(
config=config,
action_id=action_id,
template_arg=template_arg,
args=args,
fields=_COVER_PUBLISH_FIELDS,
prefix_args=[(cover.Cover.operator("ptr"), "cover")],
statement_fn=lambda field, expr: f"cover->{field} = {expr};",
)
@@ -79,20 +79,13 @@ async def to_code(config):
cg.add(var.set_assumed_state(True))
@automation.register_action(
automation.register_apply_action(
"switch.template.publish",
switch.SwitchPublishAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(switch.Switch),
cv.Required(CONF_STATE): cv.templatable(cv.boolean),
}
),
synchronous=True,
automation.ApplyField(CONF_STATE, "publish_state", cg.bool_),
)
async def switch_template_publish_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_)
cg.add(var.set_state(template_))
return var
+11 -50
View File
@@ -21,14 +21,14 @@ from esphome.const import (
DEVICE_CLASS_GAS,
DEVICE_CLASS_WATER,
)
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_device_class,
setup_entity,
)
from esphome.cpp_generator import LambdaExpression, MockObjClass
from esphome.cpp_generator import MockObjClass
IS_PLATFORM_COMPONENT = True
@@ -43,7 +43,6 @@ DEVICE_CLASSES = [
valve_ns = cg.esphome_ns.namespace("valve")
Valve = valve_ns.class_("Valve", cg.EntityBase)
ValveCall = valve_ns.class_("ValveCall")
VALVE_OPEN = valve_ns.VALVE_OPEN
VALVE_CLOSED = valve_ns.VALVE_CLOSED
@@ -67,7 +66,6 @@ OpenAction = valve_ns.class_("OpenAction", automation.Action)
CloseAction = valve_ns.class_("CloseAction", automation.Action)
StopAction = valve_ns.class_("StopAction", automation.Action)
ToggleAction = valve_ns.class_("ToggleAction", automation.Action)
ControlAction = valve_ns.class_("ControlAction", automation.Action)
ValvePublishAction = valve_ns.class_("ValvePublishAction", automation.Action)
ValveIsOpenCondition = valve_ns.class_("ValveIsOpenCondition", Condition)
ValveIsClosedCondition = valve_ns.class_("ValveIsClosedCondition", Condition)
@@ -226,53 +224,16 @@ VALVE_CONTROL_ACTION_SCHEMA = cv.Schema(
)
@automation.register_action(
"valve.control", ControlAction, VALVE_CONTROL_ACTION_SCHEMA, synchronous=True
# CONF_STATE and CONF_POSITION are cv.Exclusive in the schema, so at most
# one is present and both dispatch to set_position.
automation.register_apply_action(
"valve.control",
VALVE_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_),
call="make_call",
)
async def valve_control_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
# All configured fields are folded into a single stateless lambda whose
# constants live in flash; the action stores only a function pointer.
# CONF_STATE and CONF_POSITION are cv.Exclusive in the schema, so at most
# one is present and both dispatch to set_position.
FIELDS = (
(CONF_STOP, "set_stop", cg.bool_),
(CONF_STATE, "set_position", cg.float_),
(CONF_POSITION, "set_position", cg.float_),
)
# Normalize trigger args to `const std::remove_cvref_t<T> &` so the
# apply lambda and any inner field lambdas (generated below via
# `process_lambda`) share one parameter spelling that's well-formed for
# any T (value, ref, or const-ref). Matches 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}));")
else:
body_lines.append(f"call.{setter}({cg.safe_exp(value)});")
apply_args = [
(ValveCall.operator("ref"), "call"),
*normalized_args,
]
apply_lambda = LambdaExpression(
["\n".join(body_lines)],
apply_args,
capture="",
return_type=cg.void,
)
return cg.new_Pvariable(action_id, template_arg, paren, apply_lambda)
@coroutine_with_priority(CoroPriority.CORE)
-28
View File
@@ -46,34 +46,6 @@ template<typename... Ts> class ToggleAction final : public Action<Ts...> {
Valve *valve_;
};
// 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. `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 (*)(ValveCall &, const std::remove_cvref_t<Ts> &...);
ControlAction(Valve *valve, ApplyFn apply) : valve_(valve), apply_(apply) {}
void play(const Ts &...x) override {
auto call = this->valve_->make_call();
this->apply_(call, x...);
call.perform();
}
protected:
Valve *valve_;
ApplyFn apply_;
};
template<typename... Ts> class ValveIsOpenCondition final : public Condition<Ts...> {
public:
ValveIsOpenCondition(Valve *valve) : valve_(valve) {}
+4 -4
View File
@@ -200,10 +200,10 @@ void DeferredUpdateEventSource::process_deferred_queue_() {
deferred_queue_.erase(deferred_queue_.begin());
this->consecutive_send_failures_ = 0; // Reset failure count on successful send
} else {
// NOTE: Similar logic exists in web_server_idf/web_server_idf.cpp in AsyncEventSourceResponse::process_buffer_()
// The implementations differ due to platform-specific APIs (DISCARDED vs HTTPD_SOCK_ERR_TIMEOUT, close() vs
// fd_.store(0)), but the failure counting and timeout logic should be kept in sync. If you change this logic,
// also update the ESP-IDF implementation.
// NOTE: Similar logic exists in web_server_idf/web_server_idf.cpp in AsyncEventSourceResponse::process_buffer_().
// The close mechanisms are platform-specific (this path calls close() directly; the IDF path is time-based and
// closes through HTTPD to preserve session ownership), but both drop a client after roughly 20 seconds without
// send progress. Keep that stall policy in sync when changing either side.
this->consecutive_send_failures_++;
if (this->consecutive_send_failures_ >= MAX_CONSECUTIVE_SEND_FAILURES) {
// Too many failures, connection is likely dead
@@ -6,6 +6,7 @@
#include <cctype>
#include <cinttypes>
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -728,7 +729,7 @@ bool AsyncEventSource::loop() {
for (size_t i = 0; i < this->sessions_.size();) {
auto *ses = this->sessions_[i];
// If the session has a dead socket (marked by destroy callback)
if (ses->fd_.load() == 0) {
if (ses->safe_to_delete_()) {
// destroy() already logged the close with the fd; don't double-log here.
delete ses; // NOLINT(cppcoreguidelines-owning-memory)
// Remove by swapping with last element (O(1) removal, order doesn't matter for sessions)
@@ -751,7 +752,7 @@ void AsyncEventSource::adopt_pending_sessions_main_loop_() {
}
for (auto *rsp : incoming) {
// Already disconnected? Drop it; skip on_connect_/session start on a dead session.
if (rsp->fd_.load() == 0) {
if (rsp->safe_to_delete_()) {
delete rsp; // NOLINT(cppcoreguidelines-owning-memory)
continue;
}
@@ -865,10 +866,16 @@ void AsyncEventSourceResponse::deq_push_back_with_dedup_(void *source, message_g
}
void AsyncEventSourceResponse::process_deferred_queue_() {
if (this->close_requested_) {
return;
}
while (!deferred_queue_.empty()) {
DeferredEvent &de = deferred_queue_.front();
auto message = de.message_generator_(web_server_, de.source_);
if (this->try_send_nodefer(message.c_str(), message.size(), "state")) {
if (this->close_requested_ || deferred_queue_.empty()) {
return;
}
// O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
deferred_queue_.erase(deferred_queue_.begin());
} else {
@@ -877,8 +884,64 @@ void AsyncEventSourceResponse::process_deferred_queue_() {
}
}
void AsyncEventSourceResponse::request_close_() {
if (!this->close_requested_) {
this->close_requested_ = true;
this->deferred_queue_.clear();
this->event_buffer_.clear();
this->event_bytes_sent_ = 0;
this->next_close_attempt_ms_ = App.get_loop_component_start_time();
}
this->process_close_();
}
void AsyncEventSourceResponse::process_close_() {
if (!this->close_requested_ || this->close_work_queued_.load(std::memory_order_acquire)) {
return;
}
const int fd = this->fd_.load();
if (fd == 0) {
return;
}
const uint32_t now = App.get_loop_component_start_time();
if (static_cast<int32_t>(now - this->next_close_attempt_ms_) < 0) {
return;
}
// Queue an identity-checked shutdown on the HTTPD task. The public
// httpd_sess_trigger_close() queues only a reusable fd/session slot and can
// therefore close a new client if the original peer disconnects meanwhile.
this->close_work_queued_.store(true, std::memory_order_release);
const esp_err_t err = httpd_queue_work(this->hd_, &AsyncEventSourceResponse::close_session_work, this);
this->next_close_attempt_ms_ = now + (err == ESP_OK ? CLOSE_CONFIRM_INTERVAL_MS : CLOSE_RETRY_INTERVAL_MS);
if (err == ESP_OK) {
return;
}
this->close_work_queued_.store(false, std::memory_order_release);
if (!this->close_retry_warning_logged_) {
ESP_LOGW(TAG, "Failed to queue EventSource close (%s); retrying", esp_err_to_name(err));
this->close_retry_warning_logged_ = true;
}
}
void AsyncEventSourceResponse::close_session_work(void *arg) {
auto *response = static_cast<AsyncEventSourceResponse *>(arg);
const int fd = response->fd_.load();
if (fd != 0 && httpd_sess_get_ctx(response->hd_, fd) == response) {
// The HTTPD task remains the session owner. Shutting the socket down makes
// its next select/recv path delete the session and invoke destroy().
shutdown(fd, SHUT_RDWR);
}
// Release self only after the HTTPD-task callback has finished every access.
response->close_work_queued_.store(false, std::memory_order_release);
}
void AsyncEventSourceResponse::process_buffer_() {
if (event_buffer_.empty()) {
if (this->close_requested_ || event_buffer_.empty()) {
return;
}
if (event_bytes_sent_ == event_buffer_.size()) {
@@ -892,32 +955,33 @@ void AsyncEventSourceResponse::process_buffer_() {
httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_, remaining, 0);
if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT) {
// EAGAIN/EWOULDBLOCK - socket buffer full, try again later
// NOTE: Similar logic exists in web_server/web_server.cpp in DeferredUpdateEventSource::process_deferred_queue_()
// The implementations differ due to platform-specific APIs (HTTPD_SOCK_ERR_TIMEOUT vs DISCARDED, fd_.store(0) vs
// close()), but the failure counting and timeout logic should be kept in sync. If you change this logic, also
// update the Arduino implementation.
this->consecutive_send_failures_++;
if (this->consecutive_send_failures_ >= MAX_CONSECUTIVE_SEND_FAILURES) {
// Too many failures, connection is likely dead
ESP_LOGW(TAG, "Closing stuck EventSource connection after %" PRIu16 " failed sends",
this->consecutive_send_failures_);
this->fd_.store(0); // Mark for cleanup
this->deferred_queue_.clear();
// NOTE: Similar logic exists in web_server/web_server.cpp in DeferredUpdateEventSource::process_deferred_queue_().
// The IDF path is intentionally time-based and closes through HTTPD to preserve session ownership.
const uint32_t now = App.get_loop_component_start_time();
if (this->send_failure_started_ms_ == 0) {
this->send_failure_started_ms_ = now != 0 ? now : 1; // Reserve zero for no stall.
}
if (static_cast<int32_t>(now - (this->send_failure_started_ms_ + SEND_STALL_TIMEOUT_MS)) >= 0) {
ESP_LOGW(TAG, "Closing stuck EventSource connection after %" PRIu32 " ms without send progress",
now - this->send_failure_started_ms_);
this->request_close_();
}
return;
}
if (bytes_sent == HTTPD_SOCK_ERR_FAIL) {
// Real socket error - connection will be closed by httpd and destroy callback will be called
// Low-level asynchronous sends do not make HTTPD close the session automatically.
this->request_close_();
return;
}
if (bytes_sent <= 0) {
// Unexpected error or zero bytes sent
ESP_LOGW(TAG, "Unexpected send result: %d", bytes_sent);
this->request_close_();
return;
}
// Successful send - reset failure counter
this->consecutive_send_failures_ = 0;
// Successful send - reset stall tracking
this->send_failure_started_ms_ = 0;
event_bytes_sent_ += bytes_sent;
// Log partial sends for debugging
@@ -933,20 +997,26 @@ void AsyncEventSourceResponse::process_buffer_() {
}
void AsyncEventSourceResponse::loop() {
if (this->close_requested_) {
this->process_close_();
return;
}
process_buffer_();
process_deferred_queue_();
if (this->close_requested_)
return;
// One step per loop; refusals retry next pass
this->entities_iterator_.try_advance(1);
}
bool AsyncEventSourceResponse::try_send_nodefer(const char *message, size_t message_len, const char *event, uint32_t id,
uint32_t reconnect) {
if (this->fd_.load() == 0) {
if (this->fd_.load() == 0 || this->close_requested_) {
return false;
}
process_buffer_();
if (!event_buffer_.empty()) {
if (this->close_requested_ || !event_buffer_.empty()) {
// there is still pending event data to send first
return false;
}
@@ -1098,6 +1168,10 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e
process_buffer_();
process_deferred_queue_();
if (this->close_requested_) {
return;
}
if (!event_buffer_.empty() || !deferred_queue_.empty()) {
// outgoing event buffer or deferred queue still not empty which means downstream tcp send buffer full, no point
// trying to send first
@@ -301,6 +301,14 @@ class AsyncEventSourceResponse {
void deq_push_back_with_dedup_(void *source, message_generator_t *message_generator);
void process_deferred_queue_();
void process_buffer_();
void request_close_();
void process_close_();
static void close_session_work(void *arg);
// Deletable only after destroy() zeroed fd_ and no queued HTTPD close work still references this object.
bool safe_to_delete_() const {
return this->fd_.load() == 0 && !this->close_work_queued_.load(std::memory_order_acquire);
}
static void destroy(void *p);
AsyncEventSource *server_;
@@ -311,8 +319,16 @@ class AsyncEventSourceResponse {
esphome::web_server::ListEntitiesIterator entities_iterator_;
std::string event_buffer_;
size_t event_bytes_sent_;
uint16_t consecutive_send_failures_{0};
static constexpr uint16_t MAX_CONSECUTIVE_SEND_FAILURES = 2500; // ~20 seconds at 125Hz loop rate
uint32_t send_failure_started_ms_{0}; // Zero means no send stall in progress.
uint32_t next_close_attempt_ms_{0};
// Main-loop only; the HTTPD task never reads or writes this flag.
bool close_requested_{false};
bool close_retry_warning_logged_{false};
// Set on the main loop before queueing close work, cleared by the HTTPD-task callback when done.
std::atomic<bool> close_work_queued_{false};
static constexpr uint32_t SEND_STALL_TIMEOUT_MS = 20000;
static constexpr uint32_t CLOSE_RETRY_INTERVAL_MS = 250;
static constexpr uint32_t CLOSE_CONFIRM_INTERVAL_MS = 1000;
};
using AsyncEventSourceClient = AsyncEventSourceResponse;
+4 -1
View File
@@ -176,7 +176,10 @@ def zephyr_to_code(config: ConfigType) -> None:
@coroutine_with_priority(CoroPriority.FINAL)
async def _cdc_acm_to_code(config: ConfigType) -> None:
if "CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT" in zephyr_data()[KEY_PRJ_CONF][""]:
need_cdc_cb = zephyr_data()[KEY_PRJ_CONF][""].get(
"CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT", (False,)
)[0]
if need_cdc_cb:
var = cg.new_Pvariable(config[CONF_CDC_ACM])
await cg.register_component(var, {})
@@ -10,6 +10,7 @@ from esphome.components.esp32 import (
add_idf_sdkconfig_option,
add_partition,
include_builtin_idf_component,
require_mbedtls_tls_extras,
require_vfs_select,
)
import esphome.config_validation as cv
@@ -289,6 +290,12 @@ async def esp32_to_code(config: ConfigType) -> "MockObj":
ref="2.0.4",
)
# Zigbee's crypto platform uses AES-CCM and deterministic ECDSA directly.
# Keep the esp32 component from trimming them out of mbedTLS.
require_mbedtls_tls_extras(
("CONFIG_MBEDTLS_CCM_C", "CONFIG_MBEDTLS_ECDSA_DETERMINISTIC")
)
if CONF_WIFI in CORE.config:
# zigbee_esp32.cpp uses esp_coexist.h when WiFi is present
include_builtin_idf_component("esp_coex")
+1
View File
@@ -2354,6 +2354,7 @@ def maybe_simple_value(*validators, **kwargs):
return validator(value)
return validator({key: value})
validate.inner_schema = validator
return validate
+2 -7
View File
@@ -317,13 +317,8 @@ template<typename... X> class TemplatableValue<std::string, X...> {
case STATIC_STRING:
return std::string(this->static_str_);
#ifdef USE_ESP8266
case FLASH_STRING: {
// PROGMEM pointer — must use _P functions to access on ESP8266
size_t len = strlen_P(this->static_str_);
std::string result(len, '\0');
memcpy_P(result.data(), this->static_str_, len);
return result;
}
case FLASH_STRING:
return progmem_string(reinterpret_cast<ProgmemStr>(this->static_str_));
#endif
case NONE:
default:
+15
View File
@@ -11,6 +11,7 @@
#include <array>
#include <list>
#include <type_traits>
#include <vector>
namespace esphome {
@@ -252,6 +253,20 @@ template<typename... Ts> class StatelessLambdaAction : public Action<Ts...> {
void (*f_)(Ts...);
};
/// Runs one codegen-generated function that has the parent and every field baked in, so the
/// action holds one pointer. Args pass by const reference so a std::string arg is never copied;
/// StatelessLambdaAction keeps by-value parameters because user `lambda:` code owns them.
template<typename... Ts> class ApplyAction final : public Action<Ts...> {
public:
using ApplyFn = void (*)(const std::remove_cvref_t<Ts> &...);
explicit ApplyAction(ApplyFn apply) : apply_(apply) {}
void play(const Ts &...x) override { this->apply_(x...); }
protected:
ApplyFn apply_;
};
/// Simple continuation action that calls play_next_ on a parent action.
/// Used internally by IfAction, WhileAction, RepeatAction, etc. to chain actions.
/// Memory: 4-8 bytes (parent pointer) vs 40 bytes (LambdaAction with std::function).
-1
View File
@@ -283,7 +283,6 @@
#define USE_RUNTIME_STATS
#define USE_OTA
#define USE_OTA_ENCRYPTION
#define USE_OTA_ENCRYPTION_FROM_API
#define USE_OTA_ENCRYPTION_PROVISIONED
#define USE_OTA_ENCRYPTION_REQUIRED
#define USE_OTA_PASSWORD
+9
View File
@@ -20,6 +20,15 @@
namespace esphome {
#ifdef USE_ESP8266
std::string progmem_string(ProgmemStr str) {
auto *src = reinterpret_cast<PGM_P>(str);
std::string result(strlen_P(src), '\0');
memcpy_P(result.data(), src, result.size());
return result;
}
#endif
static const char *const TAG = "helpers";
__attribute__((noinline, cold)) void *callback_manager_grow(void *data, uint16_t size, uint16_t &capacity,
+8
View File
@@ -4,6 +4,7 @@
#include <cstddef>
#include <cstdint>
#include <new>
#include <string>
#include "esphome/core/hal.h" // For PROGMEM definition
@@ -54,6 +55,13 @@ using ProgmemStr = const char *;
namespace esphome {
/// Copies a string stored with ESPHOME_F into a std::string.
#ifdef USE_ESP8266
std::string progmem_string(ProgmemStr str);
#else
inline std::string progmem_string(ProgmemStr str) { return std::string(str); }
#endif
/// Helper for C++20 string literal template arguments
template<size_t N> struct FixedString {
char data[N]{};

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