mirror of
https://github.com/esphome/esphome.git
synced 2026-10-15 10:42:52 +00:00
Merge remote-tracking branch 'origin/dev' into esp8266-entity-names-flash
# Conflicts: # esphome/components/current_based/current_based_cover.cpp
This commit is contained in:
+12
-16
@@ -1422,31 +1422,27 @@ jobs:
|
||||
echo "::warning::ci_memory_impact_extract.py not found on target branch, skipping memory impact analysis"
|
||||
fi
|
||||
|
||||
# Check if test files exist on the target branch for the requested
|
||||
# components and platform. When a PR adds new test files for a platform,
|
||||
# the target branch won't have them yet, so skip instead of failing.
|
||||
# This check must be done here (not in determine-jobs.py) because
|
||||
# determine-jobs runs on the PR branch and cannot see what the target
|
||||
# branch has.
|
||||
# The build below uses --base-only, so skip unless the target branch has
|
||||
# test.<platform>.yaml for every requested component (test-*.yaml
|
||||
# variants alone compile nothing, and a partial set would compare a
|
||||
# smaller target build against the PR build). Checked here, not in
|
||||
# determine-jobs.py, because determine-jobs runs on the PR branch and
|
||||
# cannot see the target branch.
|
||||
- name: Check for test files on target branch
|
||||
id: check-tests
|
||||
if: steps.check-script.outputs.skip != 'true'
|
||||
run: |
|
||||
components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}'
|
||||
platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}"
|
||||
found=false
|
||||
missing=""
|
||||
for component in $(echo "$components" | jq -r '.[]'); do
|
||||
# Check for test files matching the platform (test.platform.yaml or test-*.platform.yaml)
|
||||
for f in tests/components/${component}/test*.${platform}.yaml; do
|
||||
if [ -f "$f" ]; then
|
||||
found=true
|
||||
break 2
|
||||
fi
|
||||
done
|
||||
if [ ! -f "tests/components/${component}/test.${platform}.yaml" ]; then
|
||||
missing="${missing} ${component}"
|
||||
fi
|
||||
done
|
||||
if [ "$found" = false ]; then
|
||||
if [ -n "$missing" ]; then
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
echo "::warning::No test files found on target branch for platform ${platform}, skipping memory impact analysis"
|
||||
echo "::warning::No test.${platform}.yaml on target branch for${missing}, skipping memory impact analysis"
|
||||
else
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
@@ -177,6 +177,13 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
Pick the primitive by cadence: under 250 ms use a gated `loop()`; 500 ms and above use
|
||||
`set_interval`. Full reasoning, including why `set_interval` costs more below 500 ms:
|
||||
https://developers.esphome.io/architecture/components/advanced/#quick-rule-of-thumb
|
||||
* **Scheduler ids:** name a timer only when it must be cancelled or replaced, and use a
|
||||
`static constexpr uint32_t` id, never a string. Ids are per component instance and cannot clash with other
|
||||
components, so number them from 0 and keep all of a component's ids together in one place.
|
||||
```cpp
|
||||
static constexpr uint32_t READ_TIMEOUT_ID = 0;
|
||||
this->set_timeout(READ_TIMEOUT_ID, 50, [this]() { this->read_(); });
|
||||
```
|
||||
* **Don't override a default with the same value:** if a base class method already returns what you
|
||||
want, do not override it. `Component::get_setup_priority()` returns `setup_priority::DATA`, so a
|
||||
component that wants `DATA` should simply leave it alone.
|
||||
@@ -389,6 +396,25 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
)
|
||||
```
|
||||
|
||||
When the callback's parameters differ from the automation's arguments, or the trigger should only
|
||||
fire for some values, pass `params`, `forward` and `when`; the helper then generates a capture-less
|
||||
lambda (stored inline, nothing allocated). Name the parent with `automation.parent_ref(var)` and build
|
||||
`forward` from `MockObj` calls, not f-strings of C++:
|
||||
```python
|
||||
parent = automation.parent_ref(var)
|
||||
index = cg.RawExpression("index")
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_on_state_callback",
|
||||
[(cg.StringRef, "x"), (cg.size_t, "i")],
|
||||
conf,
|
||||
params=[(cg.size_t, "index")],
|
||||
forward=[cg.StringRef(parent.option_at(index)), index],
|
||||
)
|
||||
```
|
||||
`build_parent_callback_automation`, `build_trigger_callback`, `build_callback_automations` and
|
||||
`build_trigger_automations` cover the other shapes; see their docstrings in `esphome/automation.py`.
|
||||
|
||||
**C++ -- no trigger class needed.** The callback registration method must be templatized to accept both `std::function` and lightweight forwarder structs (which avoid heap allocation):
|
||||
```cpp
|
||||
class MyComponent : public Component {
|
||||
|
||||
@@ -49,9 +49,9 @@ class FrameworkRelease(NamedTuple):
|
||||
# Arduino core version -> its build in esphome-libs/arduino-esp8266
|
||||
FRAMEWORK_RELEASES: dict[Version, FrameworkRelease] = {
|
||||
Version(3, 1, 2): FrameworkRelease(
|
||||
"3.1.2-esphome.2",
|
||||
"9377d63f569c524823199b1897ed7fde2063e50e2676dd519a0d758ab73678f7",
|
||||
37192128,
|
||||
"3.1.2-esphome.3",
|
||||
"3af533a85163804e164500a5b254dc23b922080e25ecb7dc37b3dd8ac84651db",
|
||||
37185650,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
+160
-81
@@ -1,4 +1,4 @@
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Sequence
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
import string
|
||||
@@ -23,7 +23,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, ID, EsphomeError, HexInt, Lambda
|
||||
from esphome.cpp_generator import (
|
||||
FlashStringLiteral,
|
||||
Expression,
|
||||
LambdaExpression,
|
||||
MockObj,
|
||||
MockObjClass,
|
||||
@@ -245,9 +245,7 @@ ApplyCondition = cg.esphome_ns.class_("ApplyCondition", Condition)
|
||||
|
||||
def flash_string(config: ConfigType, value: str) -> str:
|
||||
"""Default renderer for ``std::string`` constants; copies the literal out of flash on ESP8266."""
|
||||
if CORE.is_esp8266:
|
||||
return f"progmem_string({FlashStringLiteral(value)})"
|
||||
return str(cg.safe_exp(value))
|
||||
return str(cg.progmem_string(value))
|
||||
|
||||
|
||||
def literal_with_length(config: ConfigType, value: str) -> str:
|
||||
@@ -259,6 +257,13 @@ def literal_with_length(config: ConfigType, value: str) -> str:
|
||||
return f"{cg.safe_exp(value)}, {len(value.encode('utf-8'))}"
|
||||
|
||||
|
||||
def string_ref_literal(config: ConfigType, value: str) -> str:
|
||||
"""Renderer for a ``StringRef`` comparison: a flash literal on ESP8266, else ``StringRef(literal, length)``."""
|
||||
if CORE.is_esp8266:
|
||||
return str(cg.FlashStringLiteral(value))
|
||||
return f"StringRef({literal_with_length(config, value)})"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ApplyCall:
|
||||
"""One statement from config keys, e.g. ``"set_range({}, {})"`` with ``((CONF_LOW, cg.float_), ...)``.
|
||||
@@ -365,9 +370,16 @@ def _check_key_in_schema(
|
||||
schema = schema.schema[markers[part]]
|
||||
|
||||
|
||||
def parent_ref(var: MockObj) -> MockObj:
|
||||
"""``var`` named from global scope, so a trigger argument cannot shadow it.
|
||||
|
||||
Also how a generated callback names its Automation.
|
||||
"""
|
||||
return MockObj(f"::{var}", "->")
|
||||
|
||||
|
||||
async def _apply_parent(config: ConfigType, id_key: str = CONF_ID) -> str:
|
||||
# Global-scope qualified so a trigger arg named like the id cannot shadow it.
|
||||
return f"::{await cg.get_variable(config[id_key])}"
|
||||
return str(parent_ref(await cg.get_variable(config[id_key])))
|
||||
|
||||
|
||||
def _apply_lambda_args(args: TemplateArgsType) -> TemplateArgsType:
|
||||
@@ -399,7 +411,7 @@ async def _render_values(
|
||||
members: list[tuple[Any, Any, Any]],
|
||||
values: list[Any],
|
||||
config: ConfigType,
|
||||
parent: str,
|
||||
parent: str | None,
|
||||
lambda_args: TemplateArgsType,
|
||||
compare: bool = False,
|
||||
) -> list[str]:
|
||||
@@ -477,7 +489,7 @@ def register_apply_action(
|
||||
statements: list[str] = []
|
||||
for target, members in statements_spec:
|
||||
values = _apply_values(config, members)
|
||||
if members and all(value is None for value in values):
|
||||
if not _apply_call_active(members, values):
|
||||
continue
|
||||
exprs = await _render_values(
|
||||
name, target, members, values, config, parent, lambda_args
|
||||
@@ -496,6 +508,27 @@ def register_apply_action(
|
||||
register_action(name, ApplyAction, schema, synchronous=True)(builder)
|
||||
|
||||
|
||||
def _apply_call_active(members: list[tuple[Any, Any, Any]], values: list[Any]) -> bool:
|
||||
"""An ``ApplyCall`` is emitted unless it has keys and none of them is set."""
|
||||
return not members or any(value is not None for value in values)
|
||||
|
||||
|
||||
async def _render_check(
|
||||
name: str,
|
||||
target: str,
|
||||
members: list[tuple[Any, Any, Any]],
|
||||
values: list[Any],
|
||||
config: ConfigType,
|
||||
parent: str | None,
|
||||
lambda_args: TemplateArgsType,
|
||||
) -> str:
|
||||
"""Render a boolean check with ``values`` compared against config."""
|
||||
exprs = await _render_values(
|
||||
name, target, members, values, config, parent, lambda_args, compare=True
|
||||
)
|
||||
return target.format(*exprs)
|
||||
|
||||
|
||||
def register_apply_condition(
|
||||
name: str, schema: cv.Schema, check: str | ApplyCall, id_key: str = CONF_ID
|
||||
) -> None:
|
||||
@@ -521,22 +554,16 @@ def register_apply_condition(
|
||||
) -> MockObj:
|
||||
parent = await _apply_parent(config, id_key)
|
||||
lambda_args = _apply_lambda_args(args)
|
||||
exprs = await _render_values(
|
||||
name,
|
||||
call.target,
|
||||
members,
|
||||
_apply_values(config, members),
|
||||
config,
|
||||
parent,
|
||||
lambda_args,
|
||||
compare=True,
|
||||
values = _apply_values(config, members)
|
||||
check = await _render_check(
|
||||
name, call.target, members, values, config, parent, lambda_args
|
||||
)
|
||||
return _apply_function(
|
||||
condition_id,
|
||||
cg.bool_,
|
||||
template_arg,
|
||||
lambda_args,
|
||||
[f"return {parent}->{call.target.format(*exprs)};"],
|
||||
[f"return {parent}->{check};"],
|
||||
)
|
||||
|
||||
register_condition(name, ApplyCondition, schema)(builder)
|
||||
@@ -672,52 +699,20 @@ XorCondition = cg.esphome_ns.class_("XorCondition", Condition)
|
||||
|
||||
|
||||
@register_condition("and", AndCondition, validate_condition_list)
|
||||
async def and_condition_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
conditions = await build_condition_list(config, template_arg, args)
|
||||
return cg.new_Pvariable(
|
||||
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
|
||||
)
|
||||
|
||||
|
||||
@register_condition("or", OrCondition, validate_condition_list)
|
||||
async def or_condition_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
conditions = await build_condition_list(config, template_arg, args)
|
||||
return cg.new_Pvariable(
|
||||
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
|
||||
)
|
||||
|
||||
|
||||
@register_condition("all", AndCondition, validate_condition_list)
|
||||
async def all_condition_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
conditions = await build_condition_list(config, template_arg, args)
|
||||
return cg.new_Pvariable(
|
||||
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
|
||||
)
|
||||
|
||||
|
||||
@register_condition("or", OrCondition, validate_condition_list)
|
||||
@register_condition("any", OrCondition, validate_condition_list)
|
||||
async def any_condition_to_code(
|
||||
@register_condition("xor", XorCondition, validate_condition_list)
|
||||
async def condition_group_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
conditions = await build_condition_list(config, template_arg, args)
|
||||
# A group of one is that condition for and, or and xor; skip the wrapper.
|
||||
if len(conditions) == 1:
|
||||
return conditions[0]
|
||||
return cg.new_Pvariable(
|
||||
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
|
||||
)
|
||||
@@ -734,19 +729,6 @@ async def not_condition_to_code(
|
||||
return cg.new_Pvariable(condition_id, template_arg, condition)
|
||||
|
||||
|
||||
@register_condition("xor", XorCondition, validate_condition_list)
|
||||
async def xor_condition_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
conditions = await build_condition_list(config, template_arg, args)
|
||||
return cg.new_Pvariable(
|
||||
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
|
||||
)
|
||||
|
||||
|
||||
@register_condition("lambda", LambdaCondition, cv.returning_lambda)
|
||||
async def lambda_condition_to_code(
|
||||
config: ConfigType,
|
||||
@@ -1056,23 +1038,77 @@ def has_non_synchronous_actions(actions: ConfigType) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
async def build_automation(
|
||||
trigger: MockObj, args: TemplateArgsType, config: ConfigType
|
||||
async def _new_automation(
|
||||
args: TemplateArgsType, config: ConfigType, *ctor_args: MockObj
|
||||
) -> MockObj:
|
||||
arg_types = [arg[0] for arg in args]
|
||||
templ = cg.TemplateArguments(*arg_types)
|
||||
obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, trigger)
|
||||
"""Create the Automation for ``config`` with its actions."""
|
||||
templ = cg.TemplateArguments(*(arg[0] for arg in args))
|
||||
obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, *ctor_args)
|
||||
actions = await build_action_list(config[CONF_THEN], templ, args)
|
||||
cg.add(obj.add_actions(actions))
|
||||
return obj
|
||||
|
||||
|
||||
async def build_automation(
|
||||
trigger: MockObj, args: TemplateArgsType, config: ConfigType
|
||||
) -> MockObj:
|
||||
return await _new_automation(args, config, trigger)
|
||||
|
||||
|
||||
async def build_trigger_callback(
|
||||
args: TemplateArgsType,
|
||||
config: ConfigType,
|
||||
params: TemplateArgsType,
|
||||
forward: Sequence[str | Expression] | None = None,
|
||||
when: str | ApplyCall | None = None,
|
||||
) -> LambdaExpression:
|
||||
"""Build the Automation for ``config`` and return a stateless callback that triggers it.
|
||||
|
||||
``params`` are the parent callback's parameters, ``forward`` the expressions passed to
|
||||
``trigger()`` (default: the parameter names; write the parent as ``parent_ref(var)``),
|
||||
``when`` a filter the callback returns early on, skipped like any ``ApplyCall`` when none
|
||||
of its keys is set.
|
||||
"""
|
||||
members: list[tuple[Any, Any, Any]] = []
|
||||
if when is not None:
|
||||
call = when if isinstance(when, ApplyCall) else ApplyCall(when)
|
||||
members = call.members
|
||||
# A trigger callback has no parent for a str type to name.
|
||||
if any(isinstance(t, str) and "{parent}" in t for _, t, _ in members):
|
||||
raise ValueError(f"trigger filter {call.target!r}: a type names {{parent}}")
|
||||
obj = await _new_automation(args, config)
|
||||
lambda_args = _apply_lambda_args(params)
|
||||
statements: list[str] = []
|
||||
if when is not None:
|
||||
values = _apply_values(config, members)
|
||||
if _apply_call_active(members, values):
|
||||
check = await _render_check(
|
||||
"trigger filter",
|
||||
call.target,
|
||||
members,
|
||||
values,
|
||||
config,
|
||||
None,
|
||||
lambda_args,
|
||||
)
|
||||
statements.append(f"if (!({check}))\n return;")
|
||||
if forward is None:
|
||||
forward = [name for _, name in params]
|
||||
statements.append(f"{parent_ref(obj)}->trigger({', '.join(map(str, forward))});")
|
||||
return LambdaExpression(
|
||||
["\n".join(statements)], lambda_args, capture="", return_type=cg.void
|
||||
)
|
||||
|
||||
|
||||
async def build_callback_automation(
|
||||
parent: MockObj,
|
||||
callback_method: str,
|
||||
args: TemplateArgsType,
|
||||
config: ConfigType,
|
||||
forwarder: MockObj | MockObjClass | None = None,
|
||||
params: TemplateArgsType | None = None,
|
||||
forward: Sequence[str | Expression] | None = None,
|
||||
when: str | ApplyCall | None = None,
|
||||
) -> None:
|
||||
"""Build an Automation and register it as a callback on the parent.
|
||||
|
||||
@@ -1084,6 +1120,9 @@ async def build_callback_automation(
|
||||
pointer-sized (single Automation* field) to fit inline in Callback::ctx_
|
||||
and avoid heap allocation.
|
||||
|
||||
With ``params``, ``forward`` or ``when`` the callback is instead the stateless
|
||||
lambda of ``build_trigger_callback``; ``forwarder`` cannot be combined with them.
|
||||
|
||||
:param parent: The component object (e.g., button, sensor).
|
||||
:param callback_method: Name of the callback method (e.g., "add_on_press_callback").
|
||||
:param args: Automation template args as list of (type, name) tuples.
|
||||
@@ -1092,22 +1131,56 @@ async def build_callback_automation(
|
||||
TriggerForwarder<Ts...>. Pass any struct type whose aggregate init takes
|
||||
a single Automation pointer (e.g., TriggerOnTrueForwarder).
|
||||
"""
|
||||
arg_types = [arg[0] for arg in args]
|
||||
templ = cg.TemplateArguments(*arg_types)
|
||||
obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ)
|
||||
actions = await build_action_list(config[CONF_THEN], templ, args)
|
||||
cg.add(obj.add_actions(actions))
|
||||
if params is not None or forward is not None or when is not None:
|
||||
if forwarder is not None:
|
||||
raise ValueError(
|
||||
"forwarder cannot be combined with params, forward or when"
|
||||
)
|
||||
callback = await build_trigger_callback(
|
||||
args, config, args if params is None else params, forward, when
|
||||
)
|
||||
cg.add(getattr(parent, callback_method)(callback))
|
||||
return
|
||||
obj = await _new_automation(args, config)
|
||||
# Use template forwarder structs for deduplication. The compiler generates
|
||||
# one operator() per forwarder type; different automation pointers are just
|
||||
# data in the struct.
|
||||
if forwarder is None:
|
||||
forwarder = TriggerForwarder.template(templ)
|
||||
forwarder = TriggerForwarder.template(*(arg[0] for arg in args))
|
||||
# RawExpression for aggregate init — both forwarder and obj are codegen
|
||||
# MockObjs (not user input), and there's no Expression type for positional
|
||||
# aggregate initialization (StructInitializer uses named fields).
|
||||
cg.add(getattr(parent, callback_method)(cg.RawExpression(f"{forwarder}{{{obj}}}")))
|
||||
|
||||
|
||||
async def build_parent_callback_automation(
|
||||
parent: MockObj, callback_method: str, arg: tuple[Any, str], config: ConfigType
|
||||
) -> None:
|
||||
"""Register an Automation that receives ``parent`` from a callback that carries nothing.
|
||||
|
||||
``arg`` is the automation's ``(type, name)``, e.g. ``(Fan.operator("ptr"), "x")``.
|
||||
"""
|
||||
await build_callback_automation(
|
||||
parent, callback_method, [arg], config, params=[], forward=[parent_ref(parent)]
|
||||
)
|
||||
|
||||
|
||||
async def build_trigger_automations(
|
||||
parent: MockObj | None,
|
||||
config: ConfigType,
|
||||
entries: tuple[tuple[str, TemplateArgsType], ...],
|
||||
) -> None:
|
||||
"""Instantiate each entry's Trigger class, with ``parent`` when given, and build its automations.
|
||||
|
||||
``entries`` are ``(conf_key, args)`` pairs; the class comes from the entry's ``CONF_TRIGGER_ID``.
|
||||
"""
|
||||
ctor_args = () if parent is None else (parent,)
|
||||
for conf_key, args in entries:
|
||||
for conf in config.get(conf_key, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], *ctor_args)
|
||||
await build_automation(trigger, args, conf)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CallbackAutomation:
|
||||
"""A single callback automation entry for build_callback_automations."""
|
||||
@@ -1116,6 +1189,9 @@ class CallbackAutomation:
|
||||
callback_method: str
|
||||
args: TemplateArgsType = field(default_factory=list)
|
||||
forwarder: MockObj | MockObjClass | None = None
|
||||
params: TemplateArgsType | None = None
|
||||
forward: Sequence[str | Expression] | None = None
|
||||
when: str | ApplyCall | None = None
|
||||
|
||||
|
||||
async def build_callback_automations(
|
||||
@@ -1137,4 +1213,7 @@ async def build_callback_automations(
|
||||
entry.args,
|
||||
conf,
|
||||
forwarder=entry.forwarder,
|
||||
params=entry.params,
|
||||
forward=entry.forward,
|
||||
when=entry.when,
|
||||
)
|
||||
|
||||
+38
-11
@@ -237,6 +237,35 @@ def idf_macro_matches(idf_path: Path) -> bool:
|
||||
return live == _EXPECTED_MACRO
|
||||
|
||||
|
||||
def _cmake_args_for_write(minimal: bool) -> dict[str, str]:
|
||||
"""The cg.add_cmake_arg() values for one project write.
|
||||
|
||||
The discovery (minimal) write lets every built-in component register so
|
||||
the list it finds, cached per target, serves any exclusion set; the full
|
||||
write drops the excluded names from that list and passes all of them to
|
||||
IDF. Names outside the framework's components directory (managed
|
||||
components) stay excluded on the discovery write too, since the list
|
||||
only holds built-in ones. A checkout supplied through IDF_PATH caches
|
||||
nothing, so its discovery keeps every exclusion.
|
||||
"""
|
||||
from esphome.espidf.toolchain import _esphome_manages_idf, _get_idf_path
|
||||
|
||||
args = dict(CORE.cmake_args)
|
||||
if (
|
||||
not minimal
|
||||
or not _esphome_manages_idf()
|
||||
or not (excluded := args.get("EXCLUDE_COMPONENTS"))
|
||||
):
|
||||
return args
|
||||
root = _get_idf_path() / "components"
|
||||
outside = [name for name in excluded.split(";") if not (root / name).is_dir()]
|
||||
if outside:
|
||||
args["EXCLUDE_COMPONENTS"] = ";".join(outside)
|
||||
else:
|
||||
del args["EXCLUDE_COMPONENTS"]
|
||||
return args
|
||||
|
||||
|
||||
def get_project_cmakelists(
|
||||
minimal: bool = False, builtin_components: list[str] | None = None
|
||||
) -> str:
|
||||
@@ -300,11 +329,10 @@ def get_project_cmakelists(
|
||||
|
||||
# CMake variables registered via cg.add_cmake_arg(). Emitted before
|
||||
# include(project.cmake) so values like EXCLUDE_COMPONENTS are already
|
||||
# set when project.cmake seeds the component list, and on minimal
|
||||
# (discovery) writes too so excluded components never register.
|
||||
# set when project.cmake seeds the component list.
|
||||
cmake_args = "\n".join(
|
||||
f"set({name} {_cmake_quote(value)})"
|
||||
for name, value in sorted(CORE.cmake_args.items())
|
||||
for name, value in sorted(_cmake_args_for_write(minimal).items())
|
||||
)
|
||||
|
||||
# Per-project list exposed as a CMake variable so converted PIO libs
|
||||
@@ -327,10 +355,10 @@ def get_project_cmakelists(
|
||||
# component's REQUIRES including real IDF components). Referenced by
|
||||
# src/CMakeLists and by each converted PIO lib's CMakeLists. Skipped
|
||||
# on minimal writes because project_description.json may be stale.
|
||||
# Excluded components are dropped here as well: a stale
|
||||
# project_description.json from a build without exclusions may still
|
||||
# list them, and requiring an excluded component pulls it back into
|
||||
# the build (IDF requirement expansion overrides EXCLUDE_COMPONENTS).
|
||||
# Excluded components are dropped here as well: the discovery list
|
||||
# (cached per target) still holds them, and requiring an excluded
|
||||
# component pulls it back into the build (IDF requirement expansion
|
||||
# overrides EXCLUDE_COMPONENTS).
|
||||
# Derived from the EXCLUDE_COMPONENTS cmake arg emitted above so the
|
||||
# two can never disagree within one generated file.
|
||||
builtin_components_property = (
|
||||
@@ -554,10 +582,9 @@ def write_project(
|
||||
)
|
||||
|
||||
# Snapshot the exclusion set so has_outdated_files() can trigger a
|
||||
# discovery reconfigure when it changes. Excluded components never
|
||||
# register in project_description.json, so re-including one (e.g. a
|
||||
# config gains mqtt) requires a fresh discovery pass before the
|
||||
# ESPHOME_PROJECT_BUILTIN_COMPONENTS property can list it.
|
||||
# reconfigure when it changes. The ESPHOME_PROJECT_BUILTIN_COMPONENTS
|
||||
# property drops excluded names, so re-including one (e.g. a config
|
||||
# gains mqtt) needs a reconfigure to rebuild it from the discovery list.
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("exclude_components.esphomeinternal"),
|
||||
";".join(get_excluded_builtin_components()),
|
||||
|
||||
@@ -39,6 +39,7 @@ from esphome.cpp_generator import ( # noqa: F401
|
||||
new_variable,
|
||||
process_lambda,
|
||||
progmem_array,
|
||||
progmem_string,
|
||||
safe_exp,
|
||||
set_cpp_standard,
|
||||
shared_progmem_array,
|
||||
|
||||
@@ -10,6 +10,9 @@ namespace esphome::airthings_wave_base {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "airthings_wave_base");
|
||||
|
||||
static constexpr uint32_t BATTERY_READ_TIMEOUT_ID = 0;
|
||||
static constexpr uint32_t RESPONSE_TIMEOUT_ID = 1;
|
||||
|
||||
void AirthingsWaveBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
switch (event) {
|
||||
@@ -181,7 +184,7 @@ void AirthingsWaveBase::read_battery_(uint8_t *raw_value, uint16_t value_len) {
|
||||
// read the battery again at the configured update interval
|
||||
if (this->battery_update_interval_ != this->update_interval_) {
|
||||
this->read_battery_next_update_ = false;
|
||||
this->set_timeout("battery", this->battery_update_interval_,
|
||||
this->set_timeout(BATTERY_READ_TIMEOUT_ID, this->battery_update_interval_,
|
||||
[this]() { this->read_battery_next_update_ = true; });
|
||||
}
|
||||
}
|
||||
@@ -204,7 +207,7 @@ void AirthingsWaveBase::response_received_() {
|
||||
}
|
||||
|
||||
void AirthingsWaveBase::set_response_timeout_() {
|
||||
this->set_timeout("response_timeout", 30 * 1000, [this]() {
|
||||
this->set_timeout(RESPONSE_TIMEOUT_ID, 30 * 1000, [this]() {
|
||||
this->responses_pending_ = 1;
|
||||
this->response_received_();
|
||||
});
|
||||
|
||||
@@ -5,18 +5,20 @@ namespace esphome::aqi {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "aqi");
|
||||
|
||||
static constexpr uint32_t CALCULATE_DEFER_ID = 0;
|
||||
|
||||
void AQISensor::setup() {
|
||||
if (this->pm_2_5_sensor_ != nullptr) {
|
||||
this->pm_2_5_sensor_->add_on_state_callback([this](float value) {
|
||||
this->pm_2_5_value_ = value;
|
||||
// Defer calculation to avoid double-publishing if both sensors update in the same loop
|
||||
this->defer("update", [this]() { this->calculate_aqi_(); });
|
||||
this->defer(CALCULATE_DEFER_ID, [this]() { this->calculate_aqi_(); });
|
||||
});
|
||||
}
|
||||
if (this->pm_10_0_sensor_ != nullptr) {
|
||||
this->pm_10_0_sensor_->add_on_state_callback([this](float value) {
|
||||
this->pm_10_0_value_ = value;
|
||||
this->defer("update", [this]() { this->calculate_aqi_(); });
|
||||
this->defer(CALCULATE_DEFER_ID, [this]() { this->calculate_aqi_(); });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace esphome::bme280_base {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bme280.sensor");
|
||||
|
||||
static constexpr uint32_t DATA_TIMEOUT_ID = 0;
|
||||
|
||||
static const uint8_t BME280_REGISTER_DIG_T1 = 0x88;
|
||||
static const uint8_t BME280_REGISTER_DIG_T2 = 0x8A;
|
||||
static const uint8_t BME280_REGISTER_DIG_T3 = 0x8C;
|
||||
@@ -221,7 +223,7 @@ void BME280Component::update() {
|
||||
meas_time += 2.3f * oversampling_to_time(this->pressure_oversampling_) + 0.575f;
|
||||
meas_time += 2.3f * oversampling_to_time(this->humidity_oversampling_) + 0.575f;
|
||||
|
||||
this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() {
|
||||
this->set_timeout(DATA_TIMEOUT_ID, uint32_t(ceilf(meas_time)), [this]() {
|
||||
uint8_t data[8];
|
||||
if (!this->read_bytes(BME280_REGISTER_MEASUREMENTS, data, 8)) {
|
||||
ESP_LOGW(TAG, "Error reading registers");
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace esphome::bme680 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bme680.sensor");
|
||||
|
||||
static constexpr uint32_t DATA_TIMEOUT_ID = 0;
|
||||
|
||||
static const uint8_t BME680_REGISTER_COEFF1 = 0x89;
|
||||
static const uint8_t BME680_REGISTER_COEFF2 = 0xE1;
|
||||
|
||||
@@ -242,7 +244,7 @@ void BME680Component::update() {
|
||||
return;
|
||||
}
|
||||
|
||||
this->set_timeout("data", this->calc_meas_duration_(), [this]() { this->read_data_(); });
|
||||
this->set_timeout(DATA_TIMEOUT_ID, this->calc_meas_duration_(), [this]() { this->read_data_(); });
|
||||
}
|
||||
|
||||
uint8_t BME680Component::calc_heater_resistance_(uint16_t temperature) {
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace esphome::bme680_bsec {
|
||||
#ifdef USE_BSEC
|
||||
ESPHOME_LOG_TAG(TAG, "bme680_bsec.sensor");
|
||||
|
||||
static constexpr uint32_t READ_TIMEOUT_ID = 0;
|
||||
|
||||
static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"};
|
||||
|
||||
std::vector<BME680BSECComponent *>
|
||||
@@ -259,7 +261,7 @@ void BME680BSECComponent::run_() {
|
||||
this->snapshot_state_();
|
||||
|
||||
ESP_LOGV(TAG, "Queueing read in %ums", meas_dur);
|
||||
this->set_timeout("read", meas_dur, [this]() { this->read_(); });
|
||||
this->set_timeout(READ_TIMEOUT_ID, meas_dur, [this]() { this->read_(); });
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Measurement not required");
|
||||
this->read_();
|
||||
|
||||
@@ -15,6 +15,8 @@ namespace esphome::bme68x_bsec2 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bme68x_bsec2.sensor");
|
||||
|
||||
static constexpr uint32_t READ_TIMEOUT_ID = 0;
|
||||
|
||||
static constexpr const char *const IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"};
|
||||
|
||||
static bool is_no_new_data_warning(int8_t status) {
|
||||
@@ -285,11 +287,11 @@ void BME68xBSEC2Component::run_() {
|
||||
uint32_t meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_);
|
||||
ESP_LOGV(TAG, "Queueing read in %" PRIu32 "us", meas_dur);
|
||||
this->trigger_time_ns_ = curr_time_ns;
|
||||
this->set_timeout("read", meas_dur / 1000, [this]() { this->read_(this->trigger_time_ns_); });
|
||||
this->set_timeout(READ_TIMEOUT_ID, meas_dur / 1000, [this]() { this->read_(this->trigger_time_ns_); });
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Measurement not required, queueing immediate read");
|
||||
this->trigger_time_ns_ = curr_time_ns;
|
||||
this->set_timeout("read", 0, [this]() { this->read_(this->trigger_time_ns_); });
|
||||
this->set_timeout(READ_TIMEOUT_ID, 0, [this]() { this->read_(this->trigger_time_ns_); });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ namespace esphome::bmp085 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bmp085.sensor");
|
||||
|
||||
static constexpr uint32_t TEMPERATURE_TIMEOUT_ID = 0;
|
||||
static constexpr uint32_t PRESSURE_TIMEOUT_ID = 1;
|
||||
|
||||
static const uint8_t BMP085_ADDRESS = 0x77;
|
||||
static const uint8_t BMP085_REGISTER_AC1_H = 0xAA;
|
||||
static const uint8_t BMP085_REGISTER_CONTROL = 0xF4;
|
||||
@@ -16,7 +19,7 @@ void BMP085Component::update() {
|
||||
if (!this->set_mode_(BMP085_CONTROL_MODE_TEMPERATURE))
|
||||
return;
|
||||
|
||||
this->set_timeout("temperature", 5, [this]() { this->read_temperature_(); });
|
||||
this->set_timeout(TEMPERATURE_TIMEOUT_ID, 5, [this]() { this->read_temperature_(); });
|
||||
}
|
||||
void BMP085Component::setup() {
|
||||
uint8_t data[22];
|
||||
@@ -84,7 +87,7 @@ void BMP085Component::read_temperature_() {
|
||||
return;
|
||||
}
|
||||
|
||||
this->set_timeout("pressure", 26, [this]() { this->read_pressure_(); });
|
||||
this->set_timeout(PRESSURE_TIMEOUT_ID, 26, [this]() { this->read_pressure_(); });
|
||||
}
|
||||
void BMP085Component::read_pressure_() {
|
||||
uint8_t buffer[3];
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace esphome::bmp280_base {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bmp280.sensor");
|
||||
|
||||
static constexpr uint32_t DATA_TIMEOUT_ID = 0;
|
||||
|
||||
static const uint8_t BMP280_REGISTER_STATUS = 0xF3;
|
||||
static const uint8_t BMP280_REGISTER_CONTROL = 0xF4;
|
||||
static const uint8_t BMP280_REGISTER_CONFIG = 0xF5;
|
||||
@@ -166,7 +168,7 @@ void BMP280Component::update() {
|
||||
meas_time += 2.3f * oversampling_to_time(this->temperature_oversampling_);
|
||||
meas_time += 2.3f * oversampling_to_time(this->pressure_oversampling_) + 0.575f;
|
||||
|
||||
this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() {
|
||||
this->set_timeout(DATA_TIMEOUT_ID, uint32_t(ceilf(meas_time)), [this]() {
|
||||
int32_t t_fine = 0;
|
||||
float temperature = this->read_temperature_(&t_fine);
|
||||
if (std::isnan(temperature)) {
|
||||
|
||||
@@ -15,6 +15,8 @@ namespace esphome::bmp3xx_base {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bmp3xx.sensor");
|
||||
|
||||
static constexpr uint32_t DATA_TIMEOUT_ID = 0;
|
||||
|
||||
static const LogString *chip_type_to_str(uint8_t chip_type) {
|
||||
switch (chip_type) {
|
||||
case BMP388_ID:
|
||||
@@ -170,7 +172,7 @@ void BMP3XXComponent::update() {
|
||||
|
||||
const uint32_t meas_timeout = uint32_t(ceilf(meas_time));
|
||||
ESP_LOGVV(TAG, "measurement time %" PRIu32, meas_timeout);
|
||||
this->set_timeout("data", meas_timeout, [this]() {
|
||||
this->set_timeout(DATA_TIMEOUT_ID, meas_timeout, [this]() {
|
||||
float temperature = 0.0f;
|
||||
float pressure = 0.0f;
|
||||
if (this->pressure_sensor_ != nullptr) {
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace esphome::bmp581_base {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bmp581");
|
||||
|
||||
static constexpr uint32_t MEASUREMENT_TIMEOUT_ID = 0;
|
||||
|
||||
// Oversampling strings indexed by Oversampling enum (0-7): NONE, X2, X4, X8, X16, X32, X64, X128
|
||||
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
|
||||
|
||||
@@ -272,7 +274,7 @@ void BMP581Component::update() {
|
||||
|
||||
ESP_LOGVV(TAG, "Measurement should take %d ms", this->conversion_time_);
|
||||
|
||||
this->set_timeout("measurement", this->conversion_time_, [this]() {
|
||||
this->set_timeout(MEASUREMENT_TIMEOUT_ID, this->conversion_time_, [this]() {
|
||||
float temperature = 0.0;
|
||||
float pressure = 0.0;
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ namespace esphome::combination {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "combination");
|
||||
|
||||
static constexpr uint32_t NEW_VALUE_DEFER_ID = 0;
|
||||
|
||||
void CombinationComponent::log_config_(const LogString *combo_type) {
|
||||
LOG_SENSOR("", "Combination Sensor:", this);
|
||||
ESP_LOGCONFIG(TAG, " Combination Type: %s", LOG_STR_ARG(combo_type));
|
||||
@@ -43,8 +45,9 @@ void CombinationNoParameterComponent::setup() {
|
||||
for (const auto &sensor : this->sensors_) {
|
||||
// All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result
|
||||
// repeatedly in the same loop if multiple source senors update.
|
||||
sensor->add_on_state_callback(
|
||||
[this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); });
|
||||
sensor->add_on_state_callback([this](float value) -> void {
|
||||
this->defer(NEW_VALUE_DEFER_ID, [this, value]() { this->handle_new_value(value); });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,8 +122,9 @@ void LinearCombinationComponent::setup() {
|
||||
for (auto &source : this->sensor_sources_) {
|
||||
// All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result
|
||||
// repeatedly in the same loop if multiple source senors update.
|
||||
source.sensor->add_on_state_callback(
|
||||
[this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); });
|
||||
source.sensor->add_on_state_callback([this](float value) -> void {
|
||||
this->defer(NEW_VALUE_DEFER_ID, [this, value]() { this->handle_new_value(value); });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace esphome::cs5460a {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "cs5460a");
|
||||
|
||||
static constexpr uint32_t STATUS_CHECK_TIMEOUT_ID = 0;
|
||||
|
||||
void CS5460AComponent::write_register_(enum CS5460ARegister addr, uint32_t value) {
|
||||
this->write_byte(CMD_WRITE | (addr << 1));
|
||||
this->write_byte(value >> 16);
|
||||
@@ -149,13 +151,13 @@ void CS5460AComponent::schedule_next_check_() {
|
||||
this->status_momentary_warning("warning", 1000);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, "Device officially stuck, resetting");
|
||||
this->cancel_timeout("status-check");
|
||||
this->cancel_timeout(STATUS_CHECK_TIMEOUT_ID);
|
||||
this->hw_init_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this->set_timeout("status-check", time_left, [this]() {
|
||||
this->set_timeout(STATUS_CHECK_TIMEOUT_ID, time_left, [this]() {
|
||||
if (!this->check_status_())
|
||||
this->schedule_next_check_();
|
||||
});
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace esphome::ct_clamp {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "ct_clamp");
|
||||
|
||||
static constexpr uint32_t READ_TIMEOUT_ID = 0;
|
||||
|
||||
void CTClampSensor::dump_config() {
|
||||
LOG_SENSOR("", "CT Clamp Sensor", this);
|
||||
ESP_LOGCONFIG(TAG, " Sample Duration: %.2fs", this->sample_duration_ / 1e3f);
|
||||
@@ -21,7 +23,7 @@ void CTClampSensor::update() {
|
||||
this->high_freq_.start();
|
||||
|
||||
// Set timeout for ending sampling phase
|
||||
this->set_timeout("read", this->sample_duration_, [this]() {
|
||||
this->set_timeout(READ_TIMEOUT_ID, this->sample_duration_, [this]() {
|
||||
this->is_sampling_ = false;
|
||||
this->high_freq_.stop();
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace esphome::current_based {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "current_based.cover");
|
||||
|
||||
static constexpr uint32_t ROLLBACK_TIMEOUT_ID = 0;
|
||||
|
||||
using namespace esphome::cover;
|
||||
|
||||
CoverTraits CurrentBasedCover::get_traits() {
|
||||
@@ -73,7 +75,7 @@ void CurrentBasedCover::loop() {
|
||||
ESP_LOGD(TAG, "'%s' - Obstacle detected during opening.", LOG_STR_ARG(this->get_log_name()));
|
||||
this->direction_idle_();
|
||||
if (this->obstacle_rollback_ != 0) {
|
||||
this->set_timeout("rollback", 300, [this]() {
|
||||
this->set_timeout(ROLLBACK_TIMEOUT_ID, 300, [this]() {
|
||||
ESP_LOGD(TAG, "'%s' - Rollback.", LOG_STR_ARG(this->get_log_name()));
|
||||
this->target_position_ = clamp(this->position - this->obstacle_rollback_, 0.0F, 1.0F);
|
||||
this->start_direction_(COVER_OPERATION_CLOSING);
|
||||
@@ -94,7 +96,7 @@ void CurrentBasedCover::loop() {
|
||||
ESP_LOGD(TAG, "'%s' - Obstacle detected during closing.", LOG_STR_ARG(this->get_log_name()));
|
||||
this->direction_idle_();
|
||||
if (this->obstacle_rollback_ != 0) {
|
||||
this->set_timeout("rollback", 300, [this]() {
|
||||
this->set_timeout(ROLLBACK_TIMEOUT_ID, 300, [this]() {
|
||||
ESP_LOGD(TAG, "'%s' - Rollback.", LOG_STR_ARG(this->get_log_name()));
|
||||
this->target_position_ = clamp(this->position + this->obstacle_rollback_, 0.0F, 1.0F);
|
||||
this->start_direction_(COVER_OPERATION_OPENING);
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace esphome::dallas_temp {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "dallas.temp.sensor");
|
||||
|
||||
static constexpr uint32_t CONVERSION_TIMEOUT_ID = 0;
|
||||
|
||||
static const uint8_t DALLAS_MODEL_DS18S20 = 0x10;
|
||||
static const uint8_t DALLAS_MODEL_DS18B20 = 0x28;
|
||||
static const uint8_t DALLAS_COMMAND_START_CONVERSION = 0x44;
|
||||
@@ -44,7 +46,7 @@ void DallasTemperatureSensor::update() {
|
||||
|
||||
this->send_command_(DALLAS_COMMAND_START_CONVERSION);
|
||||
|
||||
this->set_timeout(this->get_address_name().c_str(), this->millis_to_wait_for_conversion_(), [this] {
|
||||
this->set_timeout(CONVERSION_TIMEOUT_ID, this->millis_to_wait_for_conversion_(), [this] {
|
||||
if (!this->read_scratch_pad_() || !this->check_scratch_pad_()) {
|
||||
this->publish_state(NAN);
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "debug_component.h"
|
||||
#ifdef USE_ZEPHYR
|
||||
#include <climits>
|
||||
#include "esphome/core/alloc_helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <esphome/components/zephyr/reset_reason.h>
|
||||
#include <zephyr/drivers/hwinfo.h>
|
||||
|
||||
@@ -36,6 +36,8 @@ class DemoCover final : public cover::Cover, public Component {
|
||||
}
|
||||
|
||||
protected:
|
||||
static constexpr uint32_t MOVE_TIMEOUT_ID = 0;
|
||||
|
||||
void control(const cover::CoverCall &call) override {
|
||||
auto pos = call.get_position();
|
||||
if (pos.has_value()) {
|
||||
@@ -43,7 +45,7 @@ class DemoCover final : public cover::Cover, public Component {
|
||||
this->current_operation =
|
||||
target > this->position ? cover::COVER_OPERATION_OPENING : cover::COVER_OPERATION_CLOSING;
|
||||
|
||||
this->set_timeout("move", 2000, [this, target]() {
|
||||
this->set_timeout(MOVE_TIMEOUT_ID, 2000, [this, target]() {
|
||||
this->current_operation = cover::COVER_OPERATION_IDLE;
|
||||
this->position = target;
|
||||
this->publish_state();
|
||||
@@ -54,7 +56,7 @@ class DemoCover final : public cover::Cover, public Component {
|
||||
this->tilt = *tilt;
|
||||
}
|
||||
if (call.get_stop()) {
|
||||
this->cancel_timeout("move");
|
||||
this->cancel_timeout(MOVE_TIMEOUT_ID);
|
||||
}
|
||||
|
||||
this->publish_state();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
@@ -18,8 +17,6 @@ from esphome.const import (
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@SimonFischer04", "@Tomer27cz", "@latonita", "@PolarGoose"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
DOMAIN = "dlms_meter"
|
||||
@@ -75,56 +72,6 @@ def validate_custom_pattern(value: ConfigType) -> ConfigType:
|
||||
return value
|
||||
|
||||
|
||||
def validate_provider_deprecation(config: ConfigType) -> ConfigType:
|
||||
if CONF_PROVIDER in config:
|
||||
provider = str(config[CONF_PROVIDER]).lower()
|
||||
if provider == "netznoe":
|
||||
_LOGGER.warning(
|
||||
"The 'provider: netznoe' option is deprecated and will be removed in 2026.11.0. "
|
||||
"The required custom patterns have been added automatically for this release, but you must update your configuration.\n"
|
||||
"Please remove the 'provider' key and explicitly replace it with the following:\n\n"
|
||||
"custom_patterns:\n"
|
||||
' - pattern: "L, TSTR"\n'
|
||||
' name: "MeterID"\n'
|
||||
' default_obis: "0.0.96.1.0.255"\n'
|
||||
' - pattern: "F, TDTM"\n'
|
||||
' name: "DateTime"\n'
|
||||
' default_obis: "0.0.1.0.0.255"\n'
|
||||
)
|
||||
patterns = config.get(CONF_CUSTOM_PATTERNS, [])
|
||||
|
||||
# Ensure "L, TSTR" for MeterID is present
|
||||
if not any(p.get(CONF_PATTERN) == "L, TSTR" for p in patterns):
|
||||
patterns.append(
|
||||
{
|
||||
CONF_PATTERN: "L, TSTR",
|
||||
CONF_NAME: "MeterID",
|
||||
CONF_DEFAULT_OBIS: [0, 0, 96, 1, 0, 255],
|
||||
CONF_PRIORITY: 0,
|
||||
}
|
||||
)
|
||||
|
||||
# Ensure "F, TDTM" for DateTime is present
|
||||
if not any(p.get(CONF_PATTERN) == "F, TDTM" for p in patterns):
|
||||
patterns.append(
|
||||
{
|
||||
CONF_PATTERN: "F, TDTM",
|
||||
CONF_NAME: "DateTime",
|
||||
CONF_DEFAULT_OBIS: [0, 0, 1, 0, 0, 255],
|
||||
CONF_PRIORITY: 0,
|
||||
}
|
||||
)
|
||||
|
||||
config[CONF_CUSTOM_PATTERNS] = patterns
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"The 'provider' option is deprecated and will be removed in 2026.11.0. "
|
||||
"The dlms_parser library now handles quirks dynamically. "
|
||||
"Please remove this option from your configuration."
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CUSTOM_PATTERN_SCHEMA = cv.All(
|
||||
custom_pattern_dict,
|
||||
cv.Schema(
|
||||
@@ -138,7 +85,7 @@ CUSTOM_PATTERN_SCHEMA = cv.All(
|
||||
validate_custom_pattern,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DlmsMeterComponent),
|
||||
@@ -146,15 +93,27 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_AUTH_KEY): cv.bind_key(name="Authentication key"),
|
||||
cv.Optional(CONF_CUSTOM_PATTERNS): cv.ensure_list(CUSTOM_PATTERN_SCHEMA),
|
||||
cv.Optional(CONF_SKIP_CRC, default=False): cv.boolean,
|
||||
cv.Optional(CONF_PROVIDER): cv.string,
|
||||
# Removed in 2026.11.0 - kept to provide helpful error message
|
||||
# Remove before 2027.5.0
|
||||
cv.Optional(CONF_PROVIDER): cv.invalid(
|
||||
"The 'provider' option has been removed in ESPHome 2026.11.0.\n"
|
||||
"For 'provider: netznoe', replace it with:\n\n"
|
||||
"custom_patterns:\n"
|
||||
' - pattern: "L, TSTR"\n'
|
||||
' name: "MeterID"\n'
|
||||
' default_obis: "0.0.96.1.0.255"\n'
|
||||
' - pattern: "F, TDTM"\n'
|
||||
' name: "DateTime"\n'
|
||||
' default_obis: "0.0.1.0.0.255"\n\n'
|
||||
"For any other provider, remove the option"
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_RECEIVE_TIMEOUT, default="1000ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
validate_provider_deprecation,
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema("dlms_meter", require_rx=True)
|
||||
@@ -193,7 +152,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.ArrayInitializer(
|
||||
p[CONF_PATTERN],
|
||||
name_expr,
|
||||
p.get(CONF_PRIORITY, 0),
|
||||
p[CONF_PRIORITY],
|
||||
obis_expr,
|
||||
)
|
||||
)
|
||||
@@ -213,43 +172,17 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
hub_id = config[CONF_ID].id
|
||||
|
||||
sensor_count = 0
|
||||
for sens_conf in CORE.config.get(SENSOR_DOMAIN, []):
|
||||
if (
|
||||
sens_conf.get("platform") == DOMAIN
|
||||
and sens_conf.get(CONF_DLMS_METER_ID).id == hub_id
|
||||
):
|
||||
if CONF_OBIS_CODE in sens_conf:
|
||||
sensor_count += 1
|
||||
else:
|
||||
from .sensor import NUMERIC_KEYS
|
||||
|
||||
sensor_count += sum(1 for key in NUMERIC_KEYS if key in sens_conf)
|
||||
|
||||
text_sensor_count = 0
|
||||
for sens_conf in CORE.config.get(TEXT_SENSOR_DOMAIN, []):
|
||||
if (
|
||||
sens_conf.get("platform") == DOMAIN
|
||||
and sens_conf.get(CONF_DLMS_METER_ID).id == hub_id
|
||||
):
|
||||
if CONF_OBIS_CODE in sens_conf:
|
||||
text_sensor_count += 1
|
||||
else:
|
||||
from .text_sensor import TEXT_KEYS
|
||||
|
||||
text_sensor_count += sum(1 for key in TEXT_KEYS if key in sens_conf)
|
||||
|
||||
binary_sensor_count = 0
|
||||
for sens_conf in CORE.config.get(BINARY_SENSOR_DOMAIN, []):
|
||||
if (
|
||||
sens_conf.get("platform") == DOMAIN
|
||||
and sens_conf.get(CONF_DLMS_METER_ID).id == hub_id
|
||||
):
|
||||
binary_sensor_count += 1
|
||||
|
||||
cg.add_define("DLMS_MAX_SENSORS", sensor_count)
|
||||
cg.add_define("DLMS_MAX_TEXT_SENSORS", text_sensor_count)
|
||||
cg.add_define("DLMS_MAX_BINARY_SENSORS", binary_sensor_count)
|
||||
for domain, define in (
|
||||
(SENSOR_DOMAIN, "DLMS_MAX_SENSORS"),
|
||||
(TEXT_SENSOR_DOMAIN, "DLMS_MAX_TEXT_SENSORS"),
|
||||
(BINARY_SENSOR_DOMAIN, "DLMS_MAX_BINARY_SENSORS"),
|
||||
):
|
||||
count = sum(
|
||||
1
|
||||
for conf in CORE.config.get(domain, [])
|
||||
if conf.get("platform") == DOMAIN and conf[CONF_DLMS_METER_ID].id == hub_id
|
||||
)
|
||||
cg.add_define(define, count)
|
||||
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
@@ -1,159 +1,59 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_POWER_FACTOR,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_AMPERE,
|
||||
UNIT_VOLT,
|
||||
UNIT_WATT,
|
||||
UNIT_WATT_HOURS,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ["dlms_meter"]
|
||||
|
||||
NUMERIC_KEYS = {
|
||||
"voltage_l1": "1.0.32.7.0.255",
|
||||
"voltage_l2": "1.0.52.7.0.255",
|
||||
"voltage_l3": "1.0.72.7.0.255",
|
||||
"current_l1": "1.0.31.7.0.255",
|
||||
"current_l2": "1.0.51.7.0.255",
|
||||
"current_l3": "1.0.71.7.0.255",
|
||||
"active_power_plus": "1.0.1.7.0.255",
|
||||
"active_power_minus": "1.0.2.7.0.255",
|
||||
"active_energy_plus": "1.0.1.8.0.255",
|
||||
"active_energy_minus": "1.0.2.8.0.255",
|
||||
"reactive_energy_plus": "1.0.3.8.0.255",
|
||||
"reactive_energy_minus": "1.0.4.8.0.255",
|
||||
"power_factor": "1.0.13.7.0.255",
|
||||
# Removed in 2026.11.0 - kept to provide helpful error message
|
||||
# Remove before 2027.5.0
|
||||
_MEASUREMENT = "state_class: measurement"
|
||||
_VOLTAGE = f"unit_of_measurement: V, accuracy_decimals: 1, device_class: voltage, {_MEASUREMENT}"
|
||||
_CURRENT = f"unit_of_measurement: A, accuracy_decimals: 2, device_class: current, {_MEASUREMENT}"
|
||||
_POWER = (
|
||||
f"unit_of_measurement: W, accuracy_decimals: 0, device_class: power, {_MEASUREMENT}"
|
||||
)
|
||||
_ENERGY = (
|
||||
"unit_of_measurement: Wh, accuracy_decimals: 0, device_class: energy, "
|
||||
"state_class: total_increasing"
|
||||
)
|
||||
_POWER_FACTOR = f"accuracy_decimals: 3, device_class: power_factor, {_MEASUREMENT}"
|
||||
REMOVED_KEYS = {
|
||||
"voltage_l1": ("1.0.32.7.0.255", _VOLTAGE),
|
||||
"voltage_l2": ("1.0.52.7.0.255", _VOLTAGE),
|
||||
"voltage_l3": ("1.0.72.7.0.255", _VOLTAGE),
|
||||
"current_l1": ("1.0.31.7.0.255", _CURRENT),
|
||||
"current_l2": ("1.0.51.7.0.255", _CURRENT),
|
||||
"current_l3": ("1.0.71.7.0.255", _CURRENT),
|
||||
"active_power_plus": ("1.0.1.7.0.255", _POWER),
|
||||
"active_power_minus": ("1.0.2.7.0.255", _POWER),
|
||||
"active_energy_plus": ("1.0.1.8.0.255", _ENERGY),
|
||||
"active_energy_minus": ("1.0.2.8.0.255", _ENERGY),
|
||||
"reactive_energy_plus": ("1.0.3.8.0.255", _ENERGY),
|
||||
"reactive_energy_minus": ("1.0.4.8.0.255", _ENERGY),
|
||||
"power_factor": ("1.0.13.7.0.255", _POWER_FACTOR),
|
||||
}
|
||||
|
||||
DYNAMIC_SCHEMA = sensor.sensor_schema().extend(
|
||||
|
||||
CONFIG_SCHEMA = sensor.sensor_schema().extend(
|
||||
{
|
||||
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
||||
cv.Required(CONF_OBIS_CODE): obis_code,
|
||||
**{
|
||||
cv.Optional(key): cv.invalid(
|
||||
f"The predefined '{key}' key was removed in ESPHome 2026.11.0. "
|
||||
f"Add a separate '- platform: dlms_meter' sensor with "
|
||||
f'obis_code: "{obis}" and {settings}'
|
||||
)
|
||||
for key, (obis, settings) in REMOVED_KEYS.items()
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def deprecation_warning(config: ConfigType) -> ConfigType:
|
||||
_LOGGER.warning(
|
||||
"The dlms_meter sensor schema using predefined keys (e.g., 'voltage_l1') is deprecated and will be removed in 2026.11.0. "
|
||||
"Please update your configuration to use the new schema with 'obis_code'."
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
OLD_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
||||
cv.Optional("voltage_l1"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("voltage_l2"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("voltage_l3"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("current_l1"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("current_l2"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("current_l3"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("active_power_plus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("active_power_minus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("active_energy_plus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional("active_energy_minus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional("reactive_energy_plus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional("reactive_energy_minus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional("power_factor"): sensor.sensor_schema(
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_POWER_FACTOR,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
deprecation_warning,
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Any(DYNAMIC_SCHEMA, OLD_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
|
||||
|
||||
if obis := config.get(CONF_OBIS_CODE):
|
||||
var = await sensor.new_sensor(config)
|
||||
cg.add(hub.register_sensor(obis, var))
|
||||
else:
|
||||
for key, obis_val in NUMERIC_KEYS.items():
|
||||
if sensor_config := config.get(key):
|
||||
sens = await sensor.new_sensor(sensor_config)
|
||||
cg.add(hub.register_sensor(obis_val, sens))
|
||||
var = await sensor.new_sensor(config)
|
||||
cg.add(hub.register_sensor(config[CONF_OBIS_CODE], var))
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
@@ -7,54 +5,33 @@ from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ["dlms_meter"]
|
||||
|
||||
TEXT_KEYS = {
|
||||
# Removed in 2026.11.0 - kept to provide helpful error message
|
||||
# Remove before 2027.5.0
|
||||
REMOVED_KEYS = {
|
||||
"timestamp": "0.0.1.0.0.255",
|
||||
"meternumber": "0.0.96.1.0.255",
|
||||
}
|
||||
|
||||
DYNAMIC_SCHEMA = text_sensor.text_sensor_schema().extend(
|
||||
|
||||
CONFIG_SCHEMA = text_sensor.text_sensor_schema().extend(
|
||||
{
|
||||
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
||||
cv.Required(CONF_OBIS_CODE): obis_code,
|
||||
**{
|
||||
cv.Optional(key): cv.invalid(
|
||||
f"The predefined '{key}' key was removed in ESPHome 2026.11.0. "
|
||||
f"Add a separate '- platform: dlms_meter' text sensor with "
|
||||
f'obis_code: "{obis}" instead'
|
||||
)
|
||||
for key, obis in REMOVED_KEYS.items()
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def deprecation_warning(config: ConfigType) -> ConfigType:
|
||||
_LOGGER.warning(
|
||||
"The dlms_meter text_sensor schema using predefined keys (e.g., 'timestamp') is deprecated and will be removed in 2026.11.0. "
|
||||
"Please update your configuration to use the new schema with 'obis_code'."
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
OLD_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
||||
cv.Optional("timestamp"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("meternumber"): text_sensor.text_sensor_schema(),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
deprecation_warning,
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Any(DYNAMIC_SCHEMA, OLD_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
|
||||
|
||||
if obis := config.get(CONF_OBIS_CODE):
|
||||
var = await text_sensor.new_text_sensor(config)
|
||||
cg.add(hub.register_text_sensor(obis, var))
|
||||
else:
|
||||
for key, obis_val in TEXT_KEYS.items():
|
||||
if text_sensor_config := config.get(key):
|
||||
sens = await text_sensor.new_text_sensor(text_sensor_config)
|
||||
cg.add(hub.register_text_sensor(obis_val, sens))
|
||||
var = await text_sensor.new_text_sensor(config)
|
||||
cg.add(hub.register_text_sensor(config[CONF_OBIS_CODE], var))
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace esphome::dps310 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "dps310");
|
||||
|
||||
static constexpr uint32_t READ_RETRY_TIMEOUT_ID = 0;
|
||||
|
||||
void DPS310Component::setup() {
|
||||
uint8_t coef_data_raw[DPS310_NUM_COEF_REGS];
|
||||
auto timer = DPS310_INIT_TIMEOUT;
|
||||
@@ -126,7 +128,7 @@ void DPS310Component::read_() {
|
||||
this->update_in_progress_ = false;
|
||||
this->status_clear_warning();
|
||||
} else {
|
||||
this->set_timeout("dps310", 10, [this]() { this->read_(); });
|
||||
this->set_timeout(READ_RETRY_TIMEOUT_ID, 10, [this]() { this->read_(); });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ namespace esphome::ens210 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "ens210");
|
||||
|
||||
static constexpr uint32_t DATA_TIMEOUT_ID = 0;
|
||||
|
||||
// ENS210 chip constants
|
||||
static const uint8_t ENS210_BOOTING_MS = 2; // Booting time in ms (also after reset, or going to high power)
|
||||
static const uint8_t ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS =
|
||||
@@ -149,7 +151,7 @@ void ENS210Component::update() {
|
||||
return;
|
||||
}
|
||||
// Wait for measurement to complete
|
||||
this->set_timeout("data", uint32_t(ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS), [this]() {
|
||||
this->set_timeout(DATA_TIMEOUT_ID, uint32_t(ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS), [this]() {
|
||||
int temperature_data, temperature_status, humidity_data, humidity_status;
|
||||
uint8_t data[6];
|
||||
uint32_t h_val_data, t_val_data;
|
||||
|
||||
@@ -2926,6 +2926,11 @@ async def to_code(config):
|
||||
# produce reproducible outputs and downstream tooling can reuse artifacts.
|
||||
add_idf_sdkconfig_option("CONFIG_APP_REPRODUCIBLE_BUILD", True)
|
||||
|
||||
# Static destructors never run, so skip registering them. See atexit_stubs.cpp.
|
||||
# --undefined: libsrc.a is scanned before the IDF libraries that also register them.
|
||||
cg.add_build_flag("-Wl,--wrap=__cxa_atexit")
|
||||
cg.add_build_flag("-Wl,--undefined=__wrap___cxa_atexit")
|
||||
|
||||
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
||||
cg.add_build_flag("-DUSE_ESP_IDF")
|
||||
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Linker wrap stub for __cxa_atexit().
|
||||
*
|
||||
* GCC registers the destructor of every global and function local static
|
||||
* object with __cxa_atexit(). The firmware never returns from main or calls
|
||||
* exit(), so these destructors never run, but the first registration links
|
||||
* newlib's exit handler tables (about 400 bytes of RAM on ESP32).
|
||||
*/
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome::esp32 {}
|
||||
|
||||
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
extern "C" int __wrap___cxa_atexit(void (*)(void *), void *, void *) { return 0; }
|
||||
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include "esphome/core/main_task.h"
|
||||
#include "esphome/core/time_conversion.h"
|
||||
|
||||
#ifndef PROGMEM
|
||||
@@ -31,6 +32,10 @@ __attribute__((always_inline)) inline bool in_isr_context() { return xPortInIsrC
|
||||
// NOLINTNEXTLINE(readability-redundant-declaration)
|
||||
extern "C" int64_t esp_timer_get_time(void);
|
||||
|
||||
/// Before setup() stores the handle every task counts as background; the wake is a no-op until then.
|
||||
__attribute__((always_inline)) inline bool is_main_loop_thread() {
|
||||
return xTaskGetCurrentTaskHandle() == esphome_main_task_handle;
|
||||
}
|
||||
__attribute__((always_inline)) inline void yield() { vPortYield(); }
|
||||
__attribute__((always_inline)) inline void delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); }
|
||||
__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(esp_timer_get_time()); }
|
||||
|
||||
@@ -150,6 +150,8 @@ void BLEClientBase::connect() {
|
||||
// A registration whose event never arrived must not block this connection's release.
|
||||
this->services_released_ = false;
|
||||
this->pending_notify_regs_ = 0;
|
||||
// A CONNECT_EVT seen while idle can leave the id of a link this attempt does not own.
|
||||
this->conn_id_ = UNSET_CONN_ID;
|
||||
// Enable loop for state processing
|
||||
this->enable_loop();
|
||||
// Immediately transition to CONNECTING to prevent duplicate connection attempts
|
||||
@@ -350,7 +352,6 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
if (!this->check_addr(param->open.remote_bda))
|
||||
return false;
|
||||
this->log_gattc_lifecycle_event_("OPEN");
|
||||
// conn_id was already set in ESP_GATTC_CONNECT_EVT
|
||||
this->service_count_ = 0;
|
||||
|
||||
// ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an
|
||||
@@ -362,19 +363,24 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
break;
|
||||
}
|
||||
|
||||
if (this->state() != espbt::ClientState::CONNECTING) {
|
||||
// This should not happen but lets log it in case it does
|
||||
// because it means we have a bad assumption about how the
|
||||
// ESP BT stack works.
|
||||
const bool open_failed = param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN;
|
||||
// Bluedroid reports a failed open as DISCONNECT_EVT then a failing OPEN_EVT, with no CONNECT_EVT,
|
||||
// so DISCONNECTING with no id is expected for a failure. Anything else means a bad assumption
|
||||
// about how the ESP BT stack works, so log it.
|
||||
const bool expected_failure =
|
||||
open_failed && this->state() == espbt::ClientState::DISCONNECTING && this->conn_id_ == UNSET_CONN_ID;
|
||||
if (this->state() != espbt::ClientState::CONNECTING && !expected_failure) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_,
|
||||
this->address_str_, espbt::client_state_to_string(this->state()), param->open.status);
|
||||
}
|
||||
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
|
||||
if (open_failed) {
|
||||
this->log_gattc_warning_("Connection open", param->open.status);
|
||||
// Connection was never established so CLOSE_EVT may not follow
|
||||
this->set_idle_();
|
||||
break;
|
||||
}
|
||||
// An open on a link that is already up sends no CONNECT_EVT, so take the id here.
|
||||
this->conn_id_ = param->open.conn_id;
|
||||
if (this->want_disconnect_) {
|
||||
// Disconnect was requested after connecting started,
|
||||
// but before the connection was established. Now that we have
|
||||
|
||||
@@ -444,6 +444,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add_build_flag("-DUSE_ARDUINO")
|
||||
cg.add_build_flag("-DUSE_ESP8266_FRAMEWORK_ARDUINO")
|
||||
cg.add_build_flag("-Wno-nonnull-compare")
|
||||
# .rodata is linked into RAM on ESP8266; keep switch lookup tables (CSWTCH) out of it
|
||||
cg.add_build_flag("-fno-tree-switch-conversion")
|
||||
if use_platformio:
|
||||
cg.add_platformio_option("framework", "arduino")
|
||||
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <c_types.h>
|
||||
#include <core_esp8266_features.h>
|
||||
#include <coredecls.h>
|
||||
#include <cstdint>
|
||||
#include <pgmspace.h>
|
||||
|
||||
@@ -40,6 +41,9 @@ void delay_microseconds_safe(uint32_t us);
|
||||
/// which is ISR-safe) so this helper is unused on this platform.
|
||||
__attribute__((always_inline)) inline bool in_isr_context() { return false; }
|
||||
|
||||
/// SDK SYS callbacks and interrupt handlers cannot yield, so they are not the loop context.
|
||||
__attribute__((always_inline)) inline bool is_main_loop_thread() { return can_yield(); }
|
||||
|
||||
__attribute__((always_inline)) inline void yield() { ::yield(); }
|
||||
__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(::micros()); }
|
||||
void delay(uint32_t ms);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from esphome import automation, pins
|
||||
from esphome.automation import Condition
|
||||
@@ -62,7 +61,6 @@ from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["network"]
|
||||
DOMAIN = "ethernet"
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Key for tracking IP state listener count in CORE.data
|
||||
ETHERNET_IP_STATE_LISTENERS_KEY = "ethernet_ip_state_listeners"
|
||||
@@ -214,13 +212,6 @@ CLK_MODES = {
|
||||
"CLK_OUT": emac_rmii_clock_mode_t.EMAC_CLK_OUT,
|
||||
}
|
||||
|
||||
CLK_MODES_DEPRECATED = {
|
||||
"GPIO0_IN": ("CLK_EXT_IN", 0),
|
||||
"GPIO0_OUT": ("CLK_OUT", 0),
|
||||
"GPIO16_OUT": ("CLK_OUT", 16),
|
||||
"GPIO17_OUT": ("CLK_OUT", 17),
|
||||
}
|
||||
|
||||
spi_host_device_t = cg.global_ns.enum("spi_host_device_t")
|
||||
|
||||
SPI_INTERFACE_MAP = {
|
||||
@@ -387,23 +378,6 @@ def _validate(config: ConfigType) -> ConfigType:
|
||||
get_esp32_variant,
|
||||
)
|
||||
|
||||
if CONF_CLK_MODE in config:
|
||||
mode, pin = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]]
|
||||
LOGGER.warning(
|
||||
"[ethernet] The 'clk_mode' option is deprecated. "
|
||||
"Please replace 'clk_mode: %s' with:\n"
|
||||
" clk:\n"
|
||||
" mode: %s\n"
|
||||
" pin: %s\n"
|
||||
"Removal scheduled for 2026.11.0.",
|
||||
config[CONF_CLK_MODE],
|
||||
mode,
|
||||
pin,
|
||||
)
|
||||
config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode, CONF_PIN: pin})
|
||||
del config[CONF_CLK_MODE]
|
||||
elif CONF_CLK not in config:
|
||||
raise cv.Invalid("'clk' is a required option for [ethernet].")
|
||||
variant = get_esp32_variant()
|
||||
if variant not in (VARIANT_ESP32, VARIANT_ESP32P4):
|
||||
raise cv.Invalid(
|
||||
@@ -452,10 +426,17 @@ RMII_SCHEMA = cv.All(
|
||||
{
|
||||
cv.Required(CONF_MDC_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Required(CONF_MDIO_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(CONF_CLK_MODE): cv.enum(
|
||||
CLK_MODES_DEPRECATED, upper=True, space="_"
|
||||
# Removed in 2026.11.0 - kept to provide helpful error message
|
||||
# Remove before 2027.5.0
|
||||
cv.Optional(CONF_CLK_MODE): cv.invalid(
|
||||
"The 'clk_mode' option has been removed in ESPHome 2026.11.0.\n"
|
||||
"Replace it with a 'clk:' block containing 'mode:' and 'pin:':\n"
|
||||
" GPIO0_IN -> mode: CLK_EXT_IN, pin: GPIO0\n"
|
||||
" GPIO0_OUT -> mode: CLK_OUT, pin: GPIO0\n"
|
||||
" GPIO16_OUT -> mode: CLK_OUT, pin: GPIO16\n"
|
||||
" GPIO17_OUT -> mode: CLK_OUT, pin: GPIO17"
|
||||
),
|
||||
cv.Optional(CONF_CLK): CLK_SCHEMA,
|
||||
cv.Required(CONF_CLK): CLK_SCHEMA,
|
||||
cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31),
|
||||
cv.Optional(CONF_POWER_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(CONF_PHY_REGISTERS): cv.ensure_list(PHY_REGISTER_SCHEMA),
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
namespace esphome::gp2y1010au0f {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "gp2y1010au0f");
|
||||
|
||||
static constexpr uint32_t READ_TIMEOUT_ID = 0;
|
||||
|
||||
static const float MIN_VOLTAGE = 0.0f;
|
||||
static const float MAX_VOLTAGE = 4.0f;
|
||||
|
||||
@@ -22,7 +25,7 @@ void GP2Y1010AU0FSensor::dump_config() {
|
||||
void GP2Y1010AU0FSensor::update() {
|
||||
is_sampling_ = true;
|
||||
|
||||
this->set_timeout("read", this->sample_duration_, [this]() {
|
||||
this->set_timeout(READ_TIMEOUT_ID, this->sample_duration_, [this]() {
|
||||
this->is_sampling_ = false;
|
||||
if (this->num_samples_ == 0)
|
||||
return;
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace esphome::hdc302x {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "hdc302x.sensor");
|
||||
|
||||
static constexpr uint32_t HEATER_OFF_TIMEOUT_ID = 0;
|
||||
|
||||
// Commands (per datasheet Table 7-4)
|
||||
static const uint8_t HDC302X_CMD_SOFT_RESET[2] = {0x30, 0xa2};
|
||||
static const uint8_t HDC302X_CMD_CLEAR_STATUS_REGISTER[2] = {0x30, 0x41};
|
||||
@@ -68,14 +70,14 @@ void HDC302XComponent::start_heater(uint16_t power, uint32_t duration_ms) {
|
||||
return;
|
||||
}
|
||||
this->heater_active_ = true;
|
||||
this->cancel_timeout("heater_off");
|
||||
this->cancel_timeout(HEATER_OFF_TIMEOUT_ID);
|
||||
if (duration_ms > 0) {
|
||||
this->set_timeout("heater_off", duration_ms, [this]() { this->stop_heater(); });
|
||||
this->set_timeout(HEATER_OFF_TIMEOUT_ID, duration_ms, [this]() { this->stop_heater(); });
|
||||
}
|
||||
}
|
||||
|
||||
void HDC302XComponent::stop_heater() {
|
||||
this->cancel_timeout("heater_off");
|
||||
this->cancel_timeout(HEATER_OFF_TIMEOUT_ID);
|
||||
if (!this->disable_heater_()) {
|
||||
ESP_LOGW(TAG, "Heater stop failed");
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ namespace esphome::honeywell_hih_i2c {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "honeywell_hih.i2c");
|
||||
|
||||
static constexpr uint32_t MEASUREMENT_TIMEOUT_ID = 0;
|
||||
|
||||
static const uint8_t REQUEST_CMD[1] = {0x00}; // Measurement Request Format
|
||||
static const uint16_t MAX_COUNT = 0x3FFE; // 2^14 - 2
|
||||
|
||||
@@ -68,13 +70,13 @@ void HoneywellHIComponent::update() {
|
||||
|
||||
this->start_measurement_();
|
||||
// The measurement cycle duration is typically 36.65 ms for temperature and humidity readings.
|
||||
this->set_timeout("meas_timeout", 100, [this] { this->measurement_timeout_(); });
|
||||
this->set_timeout(MEASUREMENT_TIMEOUT_ID, 100, [this] { this->measurement_timeout_(); });
|
||||
}
|
||||
|
||||
void HoneywellHIComponent::loop() {
|
||||
if (this->measurement_running_ && this->is_measurement_ready_()) {
|
||||
this->measurement_running_ = false;
|
||||
this->cancel_timeout("meas_timeout");
|
||||
this->cancel_timeout(MEASUREMENT_TIMEOUT_ID);
|
||||
this->read_sensor_data_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ static const uint8_t STATUS_MATH_SAT = 0;
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "honeywellabp2");
|
||||
|
||||
static constexpr uint32_t MEASUREMENT_TIMEOUT_ID = 0;
|
||||
|
||||
void HONEYWELLABP2Sensor::read_sensor_data() {
|
||||
if (this->read(raw_data_, 7) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
@@ -62,7 +64,7 @@ float HONEYWELLABP2Sensor::get_temperature() { return this->last_temperature_; }
|
||||
void HONEYWELLABP2Sensor::loop() {
|
||||
if (this->measurement_running_) {
|
||||
if (this->is_measurement_ready()) {
|
||||
this->cancel_timeout("meas_timeout");
|
||||
this->cancel_timeout(MEASUREMENT_TIMEOUT_ID);
|
||||
|
||||
this->read_sensor_data();
|
||||
if (pressure_sensor_ != nullptr) {
|
||||
@@ -79,7 +81,7 @@ void HONEYWELLABP2Sensor::update() {
|
||||
ESP_LOGV(TAG, "Update Honeywell ABP2 Sensor");
|
||||
|
||||
this->start_measurement();
|
||||
this->set_timeout("meas_timeout", 100, [this] { this->measurement_timeout(); });
|
||||
this->set_timeout(MEASUREMENT_TIMEOUT_ID, 100, [this] { this->measurement_timeout(); });
|
||||
}
|
||||
|
||||
void HONEYWELLABP2Sensor::dump_config() {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#ifdef USE_HOST
|
||||
|
||||
#include <cstdint>
|
||||
#include <pthread.h>
|
||||
#include <sched.h>
|
||||
|
||||
#define IRAM_ATTR
|
||||
@@ -16,6 +17,16 @@ namespace esphome {
|
||||
/// Host has no ISR concept.
|
||||
__attribute__((always_inline)) inline bool in_isr_context() { return false; }
|
||||
|
||||
inline pthread_t &main_loop_thread() {
|
||||
static pthread_t thread{};
|
||||
return thread;
|
||||
}
|
||||
|
||||
/// arch_init() stores the thread before any component runs, so no unset case exists.
|
||||
__attribute__((always_inline)) inline bool is_main_loop_thread() {
|
||||
return pthread_equal(pthread_self(), main_loop_thread()) != 0;
|
||||
}
|
||||
|
||||
__attribute__((always_inline)) inline void yield() { ::sched_yield(); }
|
||||
|
||||
void delay(uint32_t ms);
|
||||
@@ -25,7 +36,7 @@ uint64_t millis_64();
|
||||
void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming)
|
||||
uint32_t arch_get_cpu_cycle_count();
|
||||
|
||||
__attribute__((always_inline)) inline void arch_init() {}
|
||||
__attribute__((always_inline)) inline void arch_init() { main_loop_thread() = pthread_self(); }
|
||||
__attribute__((always_inline)) inline void arch_feed_wdt() {}
|
||||
__attribute__((always_inline)) inline uint32_t arch_get_cpu_freq_hz() { return 1000000000U; }
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
namespace esphome::hydreon_rgxx {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "hydreon_rgxx.sensor");
|
||||
|
||||
static constexpr uint32_t REBOOT_INTERVAL_ID = 0;
|
||||
|
||||
static const int MAX_DATA_LENGTH_BYTES = 80;
|
||||
static const uint8_t ASCII_LF = 0x0A;
|
||||
#define HYDREON_RGXX_COMMA ,
|
||||
@@ -138,7 +141,7 @@ void HydreonRGxxComponent::loop() {
|
||||
*/
|
||||
void HydreonRGxxComponent::schedule_reboot_() {
|
||||
this->boot_count_ = 0;
|
||||
this->set_interval("reboot", 5000, [this]() {
|
||||
this->set_interval(REBOOT_INTERVAL_ID, 5000, [this]() {
|
||||
if (this->boot_count_ < 0) {
|
||||
ESP_LOGW(TAG, "hydreon_rgxx failed to boot %d times", -this->boot_count_);
|
||||
}
|
||||
@@ -188,7 +191,7 @@ void HydreonRGxxComponent::process_line_() {
|
||||
} else {
|
||||
this->boot_count_++;
|
||||
}
|
||||
this->cancel_interval("reboot");
|
||||
this->cancel_interval(REBOOT_INTERVAL_ID);
|
||||
this->no_response_count_ = 0;
|
||||
ESP_LOGI(TAG, "Boot detected: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace esphome::hyt271 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "hyt271");
|
||||
|
||||
static constexpr uint32_t WAIT_CONVERT_TIMEOUT_ID = 0;
|
||||
|
||||
static const uint8_t HYT271_ADDRESS = 0x28;
|
||||
|
||||
void HYT271Component::dump_config() {
|
||||
@@ -23,7 +25,7 @@ void HYT271Component::update() {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
return;
|
||||
}
|
||||
this->set_timeout("wait_convert", 50, [this]() {
|
||||
this->set_timeout(WAIT_CONVERT_TIMEOUT_ID, 50, [this]() {
|
||||
uint8_t raw_data[4];
|
||||
if (this->read(raw_data, 4) != i2c::ERROR_OK) {
|
||||
this->status_set_warning();
|
||||
|
||||
@@ -21,6 +21,10 @@ namespace esphome::improv_ble {
|
||||
using namespace bytebuffer;
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "improv_ble.component");
|
||||
|
||||
static constexpr uint32_t STOP_SERVICE_TIMEOUT_ID = 0;
|
||||
static constexpr uint32_t WIFI_CONNECT_TIMEOUT_ID = 1;
|
||||
|
||||
static constexpr size_t IMPROV_MAX_LOG_BYTES = 128;
|
||||
static constexpr char ESPHOME_MY_LINK[] = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome";
|
||||
// command + data length + trailing byte
|
||||
@@ -322,7 +326,7 @@ void ImprovBLEComponent::stop() {
|
||||
// Wait before stopping the service to ensure all BLE clients see the state change.
|
||||
// This prevents clients from repeatedly reconnecting and wasting resources by allowing
|
||||
// them to observe that the device is provisioned before the service disappears.
|
||||
this->set_timeout("end-service", STOP_ADVERTISING_DELAY, [this] {
|
||||
this->set_timeout(STOP_SERVICE_TIMEOUT_ID, STOP_ADVERTISING_DELAY, [this] {
|
||||
if (this->state_ == improv::STATE_STOPPED || this->service_ == nullptr)
|
||||
return;
|
||||
// Release first so removing the service UUID does not restart advertising on the way out
|
||||
@@ -398,7 +402,7 @@ void ImprovBLEComponent::process_incoming_data_() {
|
||||
ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
|
||||
command.password.c_str());
|
||||
|
||||
this->set_timeout("wifi-connect-timeout", 30000, [this]() { this->on_wifi_connect_timeout_(); });
|
||||
this->set_timeout(WIFI_CONNECT_TIMEOUT_ID, 30000, [this]() { this->on_wifi_connect_timeout_(); });
|
||||
this->incoming_data_.clear();
|
||||
break;
|
||||
}
|
||||
@@ -438,7 +442,7 @@ void ImprovBLEComponent::check_wifi_connection_() {
|
||||
if (this->state_ == improv::STATE_PROVISIONING) {
|
||||
wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password());
|
||||
this->connecting_sta_ = {};
|
||||
this->cancel_timeout("wifi-connect-timeout");
|
||||
this->cancel_timeout(WIFI_CONNECT_TIMEOUT_ID);
|
||||
|
||||
// Build the URL list directly into a stack buffer with no heap allocation
|
||||
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
|
||||
|
||||
@@ -18,6 +18,8 @@ namespace esphome::improv_serial {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "improv_serial");
|
||||
|
||||
static constexpr uint32_t WIFI_CONNECT_TIMEOUT_ID = 0;
|
||||
|
||||
void ImprovSerialComponent::setup() {
|
||||
global_improv_serial_component = this;
|
||||
#ifdef USE_IMPROV_SERIAL_UART
|
||||
@@ -73,7 +75,7 @@ void ImprovSerialComponent::loop() {
|
||||
wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
|
||||
this->connecting_sta_.get_password());
|
||||
this->connecting_sta_ = {};
|
||||
this->cancel_timeout("wifi-connect-timeout");
|
||||
this->cancel_timeout(WIFI_CONNECT_TIMEOUT_ID);
|
||||
this->set_state_(improv::STATE_PROVISIONED);
|
||||
|
||||
this->send_settings_response_(improv::WIFI_SETTINGS);
|
||||
@@ -302,7 +304,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
|
||||
command.password.c_str());
|
||||
|
||||
this->set_timeout("wifi-connect-timeout", switching ? WIFI_SWITCH_TIMEOUT_MS : WIFI_CONNECT_TIMEOUT_MS,
|
||||
this->set_timeout(WIFI_CONNECT_TIMEOUT_ID, switching ? WIFI_SWITCH_TIMEOUT_MS : WIFI_CONNECT_TIMEOUT_MS,
|
||||
[this]() { this->on_wifi_connect_timeout_(); });
|
||||
#else
|
||||
// No Wi-Fi support compiled in; there is nothing to provision.
|
||||
|
||||
@@ -34,83 +34,35 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
|
||||
cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema(
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_FLASH,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
cv.Optional(CONF_LIGHT): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_LIGHTBULB,
|
||||
),
|
||||
cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
@@ -123,29 +75,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_FLASH,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
|
||||
@@ -34,84 +34,36 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
|
||||
cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_LIGHT): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_LIGHTBULB,
|
||||
unit_of_measurement=UNIT_EMPTY, # No standard unit for this light sensor
|
||||
),
|
||||
cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema(
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_FLASH,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
@@ -124,29 +76,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_FLASH,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
|
||||
@@ -46,41 +46,17 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
|
||||
cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_ACCOUNT_GROUP,
|
||||
),
|
||||
cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_HUMAN_GREETING_PROXIMITY,
|
||||
),
|
||||
cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_ACCOUNT_SWITCH,
|
||||
),
|
||||
}
|
||||
@@ -92,85 +68,37 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_X): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_ALPHA_X_BOX_OUTLINE,
|
||||
unit_of_measurement=UNIT_MILLIMETER,
|
||||
),
|
||||
cv.Optional(CONF_Y): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_ALPHA_Y_BOX_OUTLINE,
|
||||
unit_of_measurement=UNIT_MILLIMETER,
|
||||
),
|
||||
cv.Optional(CONF_SPEED): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_SPEED,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_SPEEDOMETER_SLOW,
|
||||
unit_of_measurement=UNIT_MILLIMETER_PER_SECOND,
|
||||
),
|
||||
cv.Optional(CONF_ANGLE): sensor.sensor_schema(
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_FORMAT_TEXT_ROTATION_ANGLE_UP,
|
||||
unit_of_measurement=UNIT_DEGREES,
|
||||
accuracy_decimals=1,
|
||||
),
|
||||
cv.Optional(CONF_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_MAP_MARKER_DISTANCE,
|
||||
unit_of_measurement=UNIT_MILLIMETER,
|
||||
),
|
||||
cv.Optional(CONF_RESOLUTION): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_RELATION_ZERO_OR_ONE_TO_ZERO_OR_ONE,
|
||||
unit_of_measurement=UNIT_MILLIMETER,
|
||||
),
|
||||
@@ -183,41 +111,17 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_MAP_MARKER_ACCOUNT,
|
||||
),
|
||||
cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_MAP_MARKER_ACCOUNT,
|
||||
),
|
||||
cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_MAP_MARKER_ACCOUNT,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -620,7 +620,7 @@ void LD6002BComponent::handle_frame_(uint16_t type, const uint8_t *data, uint16_
|
||||
this->process_command_queue_();
|
||||
if (refresh_areas) {
|
||||
this->area_write_in_flight_ = false;
|
||||
this->set_timeout(AREA_REFRESH_TIMEOUT, 50, [this]() { this->send_control_command_(CMD_GET_AREAS); });
|
||||
this->set_timeout(AREA_REFRESH_TIMEOUT_ID, 50, [this]() { this->send_control_command_(CMD_GET_AREAS); });
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1215,7 +1215,7 @@ void LD6002BComponent::send_command_internal_(uint16_t type, const uint8_t *data
|
||||
std::memcpy(this->wake_scratch_.data(), data, len);
|
||||
}
|
||||
// A button pulse must not raise the pin in the middle of this one.
|
||||
this->cancel_timeout(WAKE_BUTTON_TIMEOUT);
|
||||
this->cancel_timeout(WAKE_BUTTON_TIMEOUT_ID);
|
||||
this->wake_pulse_pending_ = true;
|
||||
this->wakeup_pin_->digital_write(false);
|
||||
const uint8_t generation = this->send_generation_;
|
||||
@@ -1364,7 +1364,8 @@ void LD6002BComponent::wake_() {
|
||||
if (this->wakeup_pin_ == nullptr || this->wake_pulse_pending_)
|
||||
return;
|
||||
this->wakeup_pin_->digital_write(false);
|
||||
this->set_timeout(WAKE_BUTTON_TIMEOUT, this->wakeup_pulse_ms_, [this]() { this->wakeup_pin_->digital_write(true); });
|
||||
this->set_timeout(WAKE_BUTTON_TIMEOUT_ID, this->wakeup_pulse_ms_,
|
||||
[this]() { this->wakeup_pin_->digital_write(true); });
|
||||
}
|
||||
|
||||
void LD6002BComponent::set_number_value(NumberType type, float value) {
|
||||
|
||||
@@ -413,12 +413,12 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
|
||||
// How long the module stays awake after any frame, and so still answers the next one.
|
||||
static constexpr uint32_t MODULE_AWAKE_MS = 10000;
|
||||
static constexpr uint8_t CMD_MAX_RETRIES = 3;
|
||||
// Named so a repeated press replaces its own pending timeout instead of stacking
|
||||
// Keyed so a repeated press replaces its own pending timeout instead of stacking
|
||||
// another, and so the command path can cancel it when it takes the pin over.
|
||||
static constexpr const char *WAKE_BUTTON_TIMEOUT = "wake_button";
|
||||
// Named so a burst of writes collapses to one read once they settle, rather than
|
||||
static constexpr uint32_t WAKE_BUTTON_TIMEOUT_ID = 0;
|
||||
// Keyed so a burst of writes collapses to one read once they settle, rather than
|
||||
// one read per write.
|
||||
static constexpr const char *AREA_REFRESH_TIMEOUT = "area_refresh";
|
||||
static constexpr uint32_t AREA_REFRESH_TIMEOUT_ID = 1;
|
||||
// A reply cannot trail the frame that earned it for longer than this; the field worst case is ~726ms.
|
||||
static constexpr uint32_t STALE_ACK_MAX_AGE_MS = 1000;
|
||||
|
||||
|
||||
@@ -30,44 +30,32 @@ from .const import (
|
||||
|
||||
DEPENDENCIES = ["ld6002b"]
|
||||
|
||||
# The ld2450 defaults for a streamed value: hold the last reading for a second so a
|
||||
# dropped frame does not read as absence, then rate-limit what reaches the frontend.
|
||||
_VALUE_SENSOR_FILTERS = [
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
]
|
||||
|
||||
TARGET_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_X): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_METER,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=_VALUE_SENSOR_FILTERS,
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_Y): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_METER,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=_VALUE_SENSOR_FILTERS,
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_Z): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_METER,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=_VALUE_SENSOR_FILTERS,
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_DOPPLER_INDEX): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
filters=_VALUE_SENSOR_FILTERS,
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_CLUSTER_ID): sensor.sensor_schema(
|
||||
|
||||
@@ -492,6 +492,9 @@ async def component_to_code(config):
|
||||
# Not enabled on RTL87xx/LN882x — costs more heap than it saves there.
|
||||
cg.add_build_flag("-DconfigSUPPORT_STATIC_ALLOCATION=1")
|
||||
|
||||
# Static destructors never run, so skip registering them. See atexit_stubs.cpp.
|
||||
cg.add_build_flag("-Wl,--wrap=__cxa_atexit")
|
||||
|
||||
# LN882x: a zero-size allocation must not trip the SDK's assert (see ln882x_zero_malloc.c).
|
||||
if config[CONF_COMPONENT_ID] == COMPONENT_LN882X:
|
||||
cg.add_build_flag("-Wl,--wrap=pvPortMalloc")
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Linker wrap stub for __cxa_atexit().
|
||||
*
|
||||
* GCC registers the destructor of every global and function local static
|
||||
* object with __cxa_atexit(). The firmware never returns from main or calls
|
||||
* exit(), so these destructors never run, but the first registration links
|
||||
* newlib's exit handler tables.
|
||||
*/
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_LIBRETINY
|
||||
|
||||
namespace esphome::libretiny {}
|
||||
|
||||
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
extern "C" int __wrap___cxa_atexit(void (*)(void *), void *, void *) { return 0; }
|
||||
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
|
||||
#endif // USE_LIBRETINY
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <FreeRTOS.h>
|
||||
#include <task.h>
|
||||
|
||||
#include "esphome/core/main_task.h"
|
||||
#include "esphome/core/time_64.h"
|
||||
|
||||
// IRAM_ATTR places a function in executable RAM so it is callable from an
|
||||
@@ -84,6 +85,10 @@ __attribute__((always_inline)) inline bool in_isr_context() {
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Before setup() stores the handle every task counts as background; the wake is a no-op until then.
|
||||
__attribute__((always_inline)) inline bool is_main_loop_thread() {
|
||||
return xTaskGetCurrentTaskHandle() == esphome_main_task_handle;
|
||||
}
|
||||
__attribute__((always_inline)) inline void yield() { ::yield(); }
|
||||
__attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); }
|
||||
__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(::micros()); }
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import number
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ON_RELEASE, CONF_RESTORE_VALUE
|
||||
from esphome.const import CONF_RESTORE_VALUE
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
from ..defines import (
|
||||
CONF_ANIMATED,
|
||||
CONF_TRIGGER,
|
||||
CONF_UPDATE_ON_RELEASE,
|
||||
CONF_WIDGET,
|
||||
LOGGER,
|
||||
)
|
||||
from ..defines import CONF_ANIMATED, CONF_TRIGGER, CONF_UPDATE_ON_RELEASE, CONF_WIDGET
|
||||
from ..lv_validation import animated
|
||||
from ..lvcode import (
|
||||
EVENT_ARG,
|
||||
@@ -31,20 +25,18 @@ CONFIG_SCHEMA = number.number_schema(LVGLNumber).extend(
|
||||
cv.Required(CONF_WIDGET): cv.use_id(LvNumber),
|
||||
**VALUE_TRIGGER_SCHEMA,
|
||||
cv.Optional(CONF_ANIMATED, default=True): animated,
|
||||
cv.Optional(CONF_UPDATE_ON_RELEASE): cv.boolean,
|
||||
# Removed in 2026.11.0 - kept to provide helpful error message
|
||||
# Remove before 2027.5.0
|
||||
cv.Optional(CONF_UPDATE_ON_RELEASE): cv.invalid(
|
||||
"The 'update_on_release' option has been removed in ESPHome 2026.11.0.\n"
|
||||
"Use 'trigger: on_release' instead."
|
||||
),
|
||||
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
trigger = config[CONF_TRIGGER]
|
||||
if CONF_UPDATE_ON_RELEASE in config:
|
||||
LOGGER.warning(
|
||||
"Option 'update_on_release' is deprecated and will be removed in 2026.11.0 - use 'trigger: on_release' instead"
|
||||
)
|
||||
if config[CONF_UPDATE_ON_RELEASE]:
|
||||
trigger = CONF_ON_RELEASE
|
||||
widget = await get_widgets(config, CONF_WIDGET)
|
||||
widget = widget[0]
|
||||
async with LambdaContext([], return_type=cg.float_) as value:
|
||||
@@ -70,6 +62,6 @@ async def to_code(config):
|
||||
lvgl_static.add_event_cb(
|
||||
widget.obj,
|
||||
await event.get_lambda(),
|
||||
*TRIGGER_EVENT_MAP[trigger],
|
||||
*TRIGGER_EVENT_MAP[config[CONF_TRIGGER]],
|
||||
)
|
||||
)
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace esphome::max31855 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "max31855");
|
||||
|
||||
static constexpr uint32_t READ_TIMEOUT_ID = 0;
|
||||
|
||||
void MAX31855Sensor::update() {
|
||||
this->enable();
|
||||
delay(1);
|
||||
@@ -14,7 +16,7 @@ void MAX31855Sensor::update() {
|
||||
this->disable();
|
||||
|
||||
// Conversion time typ: 170ms, max: 220ms
|
||||
this->set_timeout("value", 220, [this]() { this->read_data_(); });
|
||||
this->set_timeout(READ_TIMEOUT_ID, 220, [this]() { this->read_data_(); });
|
||||
}
|
||||
|
||||
void MAX31855Sensor::setup() { this->spi_setup(); }
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace esphome::max31856 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "max31856");
|
||||
|
||||
static constexpr uint32_t READ_TIMEOUT_ID = 0;
|
||||
|
||||
// Based on Adafruit's library: https://github.com/adafruit/Adafruit_MAX31856
|
||||
|
||||
void MAX31856Sensor::setup() {
|
||||
@@ -42,7 +44,7 @@ void MAX31856Sensor::update() {
|
||||
this->one_shot_temperature_();
|
||||
|
||||
// Datasheet max conversion time for 1 shot is 155ms for 60Hz / 185ms for 50Hz
|
||||
this->set_timeout("MAX31856Sensor::read_thermocouple_temperature_", filter_ == FILTER_60HZ ? 155 : 185,
|
||||
this->set_timeout(READ_TIMEOUT_ID, filter_ == FILTER_60HZ ? 155 : 185,
|
||||
[this]() { this->read_thermocouple_temperature_(); });
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace esphome::max31865 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "max31865");
|
||||
|
||||
static constexpr uint32_t READ_TIMEOUT_ID = 0;
|
||||
|
||||
void MAX31865Sensor::update() {
|
||||
// Check new faults since last measurement
|
||||
if (!has_fault_) {
|
||||
@@ -59,7 +61,7 @@ void MAX31865Sensor::update() {
|
||||
this->write_config_(0b11100000, 0b10100000);
|
||||
|
||||
// Datasheet max conversion time is 55ms for 60Hz / 66ms for 50Hz
|
||||
this->set_timeout("value", filter_ == FILTER_60HZ ? 55 : 66, [this]() { this->read_data_(); });
|
||||
this->set_timeout(READ_TIMEOUT_ID, filter_ == FILTER_60HZ ? 55 : 66, [this]() { this->read_data_(); });
|
||||
}
|
||||
|
||||
void MAX31865Sensor::setup() {
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace esphome::max6675 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "max6675");
|
||||
|
||||
static constexpr uint32_t READ_TIMEOUT_ID = 0;
|
||||
|
||||
void MAX6675Sensor::update() {
|
||||
this->enable();
|
||||
delay(1);
|
||||
@@ -12,7 +14,7 @@ void MAX6675Sensor::update() {
|
||||
this->disable();
|
||||
|
||||
// Conversion time typ: 170ms, max: 220ms
|
||||
this->set_timeout("value", 250, [this]() { this->read_data_(); });
|
||||
this->set_timeout(READ_TIMEOUT_ID, 250, [this]() { this->read_data_(); });
|
||||
}
|
||||
|
||||
void MAX6675Sensor::setup() { this->spi_setup(); }
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
namespace esphome::mcp23xxx_base {
|
||||
|
||||
void IRAM_ATTR mcp23xxx_gpio_intr(Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
template<uint8_t N> void MCP23XXXGPIOPin<N>::setup() {
|
||||
this->pin_mode(flags_);
|
||||
// When interrupt_pin is configured, pin_mode() already auto-enables CHANGE
|
||||
|
||||
@@ -8,6 +8,10 @@ namespace esphome::mcp23xxx_base {
|
||||
|
||||
enum MCP23XXXInterruptMode : uint8_t { MCP23XXX_NO_INTERRUPT = 0, MCP23XXX_CHANGE, MCP23XXX_RISING, MCP23XXX_FALLING };
|
||||
|
||||
// Not a template member: GCC 10 (ESP8266) ignores section attributes on template instantiations, leaving the
|
||||
// ISR in flash.
|
||||
void IRAM_ATTR mcp23xxx_gpio_intr(Component *arg);
|
||||
|
||||
template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander::CachedGpioExpander<uint8_t, N> {
|
||||
public:
|
||||
virtual void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||
@@ -35,7 +39,8 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander:
|
||||
void setup_interrupt_pin_() {
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->attach_interrupt(&MCP23XXXBase::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
this->interrupt_pin_->attach_interrupt(&mcp23xxx_gpio_intr, static_cast<Component *>(this),
|
||||
gpio::INTERRUPT_FALLING_EDGE);
|
||||
this->set_invalidate_on_read_(false);
|
||||
}
|
||||
// Disable loop until an input pin is configured via pin_mode()
|
||||
@@ -43,7 +48,6 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander:
|
||||
// For polling mode, loop is re-enabled when pin_mode() registers an input pin
|
||||
this->disable_loop();
|
||||
}
|
||||
static void IRAM_ATTR gpio_intr(MCP23XXXBase *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
// read a given register
|
||||
virtual bool read_reg(uint8_t reg, uint8_t *value) = 0;
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace esphome::mics_4514 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "mics_4514");
|
||||
|
||||
static constexpr uint32_t WARMUP_TIMEOUT_ID = 0;
|
||||
|
||||
static const uint8_t SENSOR_REGISTER = 0x04;
|
||||
static const uint8_t POWER_MODE_REGISTER = 0x0a;
|
||||
|
||||
@@ -18,7 +20,7 @@ void MICS4514Component::setup() {
|
||||
power_mode = 0x01;
|
||||
this->write_register(POWER_MODE_REGISTER, &power_mode, 1);
|
||||
delay(100); // NOLINT
|
||||
this->set_timeout("warmup", 3 * 60 * 1000, [this]() { this->warmed_up_ = true; });
|
||||
this->set_timeout(WARMUP_TIMEOUT_ID, 3 * 60 * 1000, [this]() { this->warmed_up_ = true; });
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace esphome::mlx90393 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "mlx90393");
|
||||
|
||||
static constexpr uint32_t VERIFY_SETTINGS_TIMEOUT_ID = 0;
|
||||
|
||||
const LogString *settings_to_string(MLX90393Setting setting) {
|
||||
switch (setting) {
|
||||
case MLX90393_GAIN_SEL:
|
||||
@@ -114,7 +116,7 @@ void MLX90393Cls::setup() {
|
||||
}
|
||||
|
||||
// start verify settings process
|
||||
this->set_timeout("verify settings", 3000, [this]() { this->verify_settings_timeout_(MLX90393_GAIN_SEL); });
|
||||
this->set_timeout(VERIFY_SETTINGS_TIMEOUT_ID, 3000, [this]() { this->verify_settings_timeout_(MLX90393_GAIN_SEL); });
|
||||
}
|
||||
|
||||
void MLX90393Cls::dump_config() {
|
||||
@@ -268,7 +270,8 @@ void MLX90393Cls::verify_settings_timeout_(MLX90393Setting stage) {
|
||||
next_stage = static_cast<MLX90393Setting>(0);
|
||||
}
|
||||
|
||||
this->set_timeout("verify settings", 3000, [this, next_stage]() { this->verify_settings_timeout_(next_stage); });
|
||||
this->set_timeout(VERIFY_SETTINGS_TIMEOUT_ID, 3000,
|
||||
[this, next_stage]() { this->verify_settings_timeout_(next_stage); });
|
||||
}
|
||||
|
||||
} // namespace esphome::mlx90393
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace esphome::modbus {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "modbus");
|
||||
|
||||
static constexpr uint32_t DEFERRED_SEND_TIMEOUT_ID = 0;
|
||||
|
||||
static constexpr size_t MODBUS_MAX_LOG_BYTES = 64;
|
||||
|
||||
static constexpr uint32_t US_PER_SEC = 1000000;
|
||||
@@ -205,7 +207,7 @@ 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");
|
||||
this->cancel_timeout(DEFERRED_SEND_TIMEOUT_ID);
|
||||
ESP_LOGD(TAG, "Dropped deferred reply to %" PRIu8 ": a new frame arrived first", this->deferred_payload_[0]);
|
||||
this->deferred_payload_len_ = 0;
|
||||
}
|
||||
@@ -1216,7 +1218,7 @@ void ModbusServerHub::send_raw_(const uint8_t *payload, uint16_t len) {
|
||||
std::memcpy(this->deferred_payload_.data(), payload, len);
|
||||
this->deferred_payload_len_ = len;
|
||||
// set_timeout() takes milliseconds; round the microsecond delay up so we never fire early.
|
||||
this->set_timeout("deferred_send", (this->tx_delay_remaining() + US_PER_MS - 1) / US_PER_MS, [this]() {
|
||||
this->set_timeout(DEFERRED_SEND_TIMEOUT_ID, (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;
|
||||
|
||||
@@ -273,6 +273,8 @@ class MQTTComponent : public Component {
|
||||
void subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos = 0);
|
||||
|
||||
protected:
|
||||
static constexpr uint32_t SEND_DEFER_ID = 0;
|
||||
|
||||
/// Helper method to get the discovery topic for this component into a buffer.
|
||||
StringRef get_discovery_topic_to_(std::span<char, MQTT_DISCOVERY_TOPIC_MAX_LEN> buf,
|
||||
const MQTTDiscoveryInfo &discovery_info) const;
|
||||
|
||||
@@ -121,7 +121,7 @@ void MQTTFanComponent::setup() {
|
||||
});
|
||||
}
|
||||
|
||||
this->state_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); });
|
||||
this->state_->add_on_state_callback([this]() { this->defer(SEND_DEFER_ID, [this]() { this->publish_state(); }); });
|
||||
}
|
||||
|
||||
void MQTTFanComponent::dump_config() {
|
||||
|
||||
@@ -28,7 +28,7 @@ void MQTTJSONLightComponent::setup() {
|
||||
}
|
||||
|
||||
void MQTTJSONLightComponent::on_light_remote_values_update() {
|
||||
this->defer("send", [this]() { this->publish_state_(); });
|
||||
this->defer(SEND_DEFER_ID, [this]() { this->publish_state_(); });
|
||||
}
|
||||
|
||||
MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {}
|
||||
|
||||
@@ -29,7 +29,7 @@ void MQTTLockComponent::setup() {
|
||||
}
|
||||
});
|
||||
this->lock_->add_on_state_callback(
|
||||
[this](LockState /*state*/) { this->defer("send", [this]() { this->publish_state(); }); });
|
||||
[this](LockState /*state*/) { this->defer(SEND_DEFER_ID, [this]() { this->publish_state(); }); });
|
||||
}
|
||||
void MQTTLockComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Lock '%s': ", LOG_STR_ARG(this->lock_->get_log_name()));
|
||||
|
||||
@@ -34,7 +34,7 @@ void MQTTSwitchComponent::setup() {
|
||||
}
|
||||
});
|
||||
this->switch_->add_on_state_callback(
|
||||
[this](bool enabled) { this->defer("send", [this, enabled]() { this->publish_state(enabled); }); });
|
||||
[this](bool enabled) { this->defer(SEND_DEFER_ID, [this, enabled]() { this->publish_state(enabled); }); });
|
||||
}
|
||||
void MQTTSwitchComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Switch '%s': ", LOG_STR_ARG(this->switch_->get_log_name()));
|
||||
|
||||
@@ -24,7 +24,7 @@ void MQTTUpdateComponent::setup() {
|
||||
}
|
||||
});
|
||||
|
||||
this->update_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); });
|
||||
this->update_->add_on_state_callback([this]() { this->defer(SEND_DEFER_ID, [this]() { this->publish_state(); }); });
|
||||
}
|
||||
|
||||
bool MQTTUpdateComponent::publish_state() {
|
||||
|
||||
@@ -6,6 +6,9 @@ namespace esphome::ms5611 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "ms5611");
|
||||
|
||||
static constexpr uint32_t TEMPERATURE_TIMEOUT_ID = 0;
|
||||
static constexpr uint32_t PRESSURE_TIMEOUT_ID = 1;
|
||||
|
||||
static const uint8_t MS5611_ADDRESS = 0x77;
|
||||
static const uint8_t MS5611_CMD_ADC_READ = 0x00;
|
||||
static const uint8_t MS5611_CMD_RESET = 0x1E;
|
||||
@@ -44,7 +47,7 @@ void MS5611Component::update() {
|
||||
return;
|
||||
}
|
||||
|
||||
this->set_timeout("temperature", 10, [this]() { this->read_temperature_(); });
|
||||
this->set_timeout(TEMPERATURE_TIMEOUT_ID, 10, [this]() { this->read_temperature_(); });
|
||||
}
|
||||
void MS5611Component::read_temperature_() {
|
||||
uint8_t bytes[3];
|
||||
@@ -60,7 +63,7 @@ void MS5611Component::read_temperature_() {
|
||||
return;
|
||||
}
|
||||
|
||||
this->set_timeout("pressure", 10, [this, raw_temperature]() { this->read_pressure_(raw_temperature); });
|
||||
this->set_timeout(PRESSURE_TIMEOUT_ID, 10, [this, raw_temperature]() { this->read_pressure_(raw_temperature); });
|
||||
}
|
||||
void MS5611Component::read_pressure_(uint32_t raw_temperature) {
|
||||
uint8_t bytes[3];
|
||||
|
||||
@@ -9,6 +9,12 @@ namespace esphome::ms8607 {
|
||||
/// TAG used for logging calls
|
||||
ESPHOME_LOG_TAG(TAG, "ms8607");
|
||||
|
||||
static constexpr uint32_t RESET_TIMEOUT_ID = 0;
|
||||
static constexpr uint32_t PROM_READ_TIMEOUT_ID = 1;
|
||||
static constexpr uint32_t TEMPERATURE_TIMEOUT_ID = 2;
|
||||
static constexpr uint32_t PRESSURE_TIMEOUT_ID = 3;
|
||||
static constexpr uint32_t HUMIDITY_TIMEOUT_ID = 4;
|
||||
|
||||
/// Reset the Pressure/Temperature sensor
|
||||
static const uint8_t MS8607_PT_CMD_RESET = 0x1E;
|
||||
|
||||
@@ -97,7 +103,7 @@ void MS8607Component::try_reset_() {
|
||||
if (--this->reset_attempts_remaining_ > 0) {
|
||||
uint32_t delay = this->reset_interval_;
|
||||
this->reset_interval_ *= 5;
|
||||
this->set_timeout("reset", delay, [this]() { this->try_reset_(); });
|
||||
this->set_timeout(RESET_TIMEOUT_ID, delay, [this]() { this->try_reset_(); });
|
||||
this->status_set_error();
|
||||
} else {
|
||||
this->mark_failed();
|
||||
@@ -110,7 +116,7 @@ void MS8607Component::try_reset_() {
|
||||
this->status_clear_error();
|
||||
|
||||
// 15ms delay matches datasheet, Adafruit_MS8607 & SparkFun_PHT_MS8607_Arduino_Library
|
||||
this->set_timeout("prom-read", 15, [this]() {
|
||||
this->set_timeout(PROM_READ_TIMEOUT_ID, 15, [this]() {
|
||||
if (this->read_calibration_values_from_prom_()) {
|
||||
this->setup_status_ = SetupStatus::SUCCESSFUL;
|
||||
this->status_clear_error();
|
||||
@@ -251,7 +257,7 @@ void MS8607Component::request_read_temperature_() {
|
||||
}
|
||||
|
||||
// datasheet says 17.2ms max conversion time at OSR 8192
|
||||
this->set_timeout("temperature", 20, [this]() { this->read_temperature_(); });
|
||||
this->set_timeout(TEMPERATURE_TIMEOUT_ID, 20, [this]() { this->read_temperature_(); });
|
||||
}
|
||||
|
||||
void MS8607Component::read_temperature_() {
|
||||
@@ -272,7 +278,8 @@ void MS8607Component::request_read_pressure_(uint32_t d2_raw_temperature) {
|
||||
}
|
||||
|
||||
// datasheet says 17.2ms max conversion time at OSR 8192
|
||||
this->set_timeout("pressure", 20, [this, d2_raw_temperature]() { this->read_pressure_(d2_raw_temperature); });
|
||||
this->set_timeout(PRESSURE_TIMEOUT_ID, 20,
|
||||
[this, d2_raw_temperature]() { this->read_pressure_(d2_raw_temperature); });
|
||||
}
|
||||
|
||||
void MS8607Component::read_pressure_(uint32_t d2_raw_temperature) {
|
||||
@@ -293,7 +300,7 @@ void MS8607Component::request_read_humidity_(float temperature_float) {
|
||||
}
|
||||
|
||||
// datasheet says 15.89ms max conversion time at OSR 8192
|
||||
this->set_timeout("humidity", 20, [this, temperature_float]() { this->read_humidity_(temperature_float); });
|
||||
this->set_timeout(HUMIDITY_TIMEOUT_ID, 20, [this, temperature_float]() { this->read_humidity_(temperature_float); });
|
||||
}
|
||||
|
||||
void MS8607Component::read_humidity_(float temperature_float) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import logging
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display, esp32, uart
|
||||
@@ -43,8 +41,6 @@ from .base_component import (
|
||||
CONF_WAKE_UP_PAGE,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
@@ -56,15 +52,6 @@ def AUTO_LOAD() -> list[str]:
|
||||
return base
|
||||
|
||||
|
||||
def _deprecated_dump_device_info(value):
|
||||
_LOGGER.warning(
|
||||
"'dump_device_info' is deprecated and will be removed in ESPHome 2026.11.0. "
|
||||
"Device info is now always logged at connection time. "
|
||||
"Please remove this option from your configuration."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def _validate_tft_upload(config):
|
||||
has_tft_url = CONF_TFT_URL in config
|
||||
for conf_key in (
|
||||
@@ -91,9 +78,11 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=TimePeriod(milliseconds=255)),
|
||||
),
|
||||
# Deprecated — device info is now always logged. Remove before 2026.11.0.
|
||||
cv.Optional(CONF_DUMP_DEVICE_INFO): cv.All(
|
||||
cv.boolean, _deprecated_dump_device_info
|
||||
# Removed in 2026.11.0 - kept to provide helpful error message
|
||||
# Remove before 2027.5.0
|
||||
cv.Optional(CONF_DUMP_DEVICE_INFO): cv.invalid(
|
||||
"The 'dump_device_info' option has been removed in ESPHome 2026.11.0.\n"
|
||||
"Device info is now always logged, so remove this option from your configuration."
|
||||
),
|
||||
cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MAX_QUEUE_AGE, default="8000ms"): cv.All(
|
||||
|
||||
@@ -12,6 +12,7 @@ from esphome.build_helpers import pch
|
||||
from esphome.build_helpers.ccache import resolve_ccache_path
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.zephyr import (
|
||||
Section,
|
||||
add_extra_script,
|
||||
copy_files as zephyr_copy_files,
|
||||
zephyr_add_overlay,
|
||||
@@ -126,11 +127,37 @@ def set_core_data(config: ConfigType) -> ConfigType:
|
||||
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = KEY_ZEPHYR
|
||||
|
||||
if config[KEY_BOOTLOADER] in BOOTLOADER_CONFIG:
|
||||
zephyr_add_pm_static(BOOTLOADER_CONFIG[config[KEY_BOOTLOADER]])
|
||||
sections = BOOTLOADER_CONFIG[config[KEY_BOOTLOADER]]
|
||||
if CORE.testing_mode:
|
||||
sections = _testing_mode_sections(sections)
|
||||
zephyr_add_pm_static(sections)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
# In testing mode, fake a larger flash to allow linking grouped component
|
||||
# tests. The nRF52840 has 1 MB and a mcumgr OTA config halves the app slot,
|
||||
# which an openthread config alone fills; CI images are never flashed.
|
||||
NRF52840_FLASH_SIZE = 0x100000
|
||||
TESTING_FLASH_SIZE = 0x400000
|
||||
|
||||
|
||||
def _testing_mode_sections(sections: list[Section]) -> list[Section]:
|
||||
"""Move a bootloader pinned to the end of the real flash to the end of the
|
||||
faked one, so the partition manager still sees a single gap for the app."""
|
||||
return [
|
||||
Section(
|
||||
section.name,
|
||||
section.address + TESTING_FLASH_SIZE - NRF52840_FLASH_SIZE,
|
||||
section.size,
|
||||
section.region,
|
||||
)
|
||||
if section.end_address == NRF52840_FLASH_SIZE
|
||||
else section
|
||||
for section in sections
|
||||
]
|
||||
|
||||
|
||||
_TOOLCHAINS = (Toolchain.PLATFORMIO, Toolchain.SDK_NRF)
|
||||
_resolve_toolchain = cv.resolve_toolchain("nRF52", _TOOLCHAINS, Toolchain.SDK_NRF)
|
||||
|
||||
@@ -381,6 +408,15 @@ async def to_code(config: ConfigType) -> None:
|
||||
zephyr_setup_preferences()
|
||||
zephyr_to_code(config)
|
||||
|
||||
if CORE.testing_mode:
|
||||
zephyr_add_overlay(
|
||||
f"""
|
||||
&flash0 {{
|
||||
reg = <0x0 {TESTING_FLASH_SIZE:#x}>;
|
||||
}};
|
||||
"""
|
||||
)
|
||||
|
||||
if dfu_config := config.get(CONF_DFU):
|
||||
CORE.add_job(_dfu_to_code, dfu_config)
|
||||
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||
|
||||
@@ -4,19 +4,6 @@ namespace esphome::one_wire {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "one_wire");
|
||||
|
||||
const std::string &OneWireDevice::get_address_name() {
|
||||
if (this->address_name_.empty()) {
|
||||
char hex_buf[19]; // "0x" + 16 hex chars + null
|
||||
this->address_name_ = format_hex_prefixed_to(hex_buf, this->address_);
|
||||
}
|
||||
return this->address_name_;
|
||||
}
|
||||
|
||||
void OneWireDevice::set_address(uint64_t address) {
|
||||
this->address_ = address;
|
||||
this->address_name_.clear();
|
||||
}
|
||||
|
||||
bool OneWireDevice::send_command_(uint8_t cmd) {
|
||||
if (!this->bus_->select(this->address_))
|
||||
return false;
|
||||
|
||||
@@ -7,14 +7,17 @@
|
||||
namespace esphome::one_wire {
|
||||
|
||||
#define LOG_ONE_WIRE_DEVICE(this) \
|
||||
ESP_LOGCONFIG(TAG, " Address: %s (%s)", this->get_address_name().c_str(), \
|
||||
LOG_STR_ARG(this->bus_->get_model_str(this->address_ & 0xff)));
|
||||
do { \
|
||||
char address_buf[format_hex_prefixed_size(sizeof(uint64_t))]; \
|
||||
ESP_LOGCONFIG(TAG, " Address: %s (%s)", format_hex_prefixed_to(address_buf, (this)->address_), \
|
||||
LOG_STR_ARG((this)->bus_->get_model_str((this)->address_ & 0xff))); \
|
||||
} while (0)
|
||||
|
||||
class OneWireDevice {
|
||||
public:
|
||||
/// @brief store the address of the device
|
||||
/// @param address of the device
|
||||
void set_address(uint64_t address);
|
||||
void set_address(uint64_t address) { this->address_ = address; }
|
||||
|
||||
void set_index(uint8_t index) { this->index_ = index; }
|
||||
|
||||
@@ -22,16 +25,12 @@ class OneWireDevice {
|
||||
/// @param bus pointer to the OneWireBus object
|
||||
void set_one_wire_bus(OneWireBus *bus) { this->bus_ = bus; }
|
||||
|
||||
/// Helper to create (and cache) the name for this sensor. For example "0xfe0000031f1eaf29".
|
||||
const std::string &get_address_name();
|
||||
|
||||
protected:
|
||||
static constexpr uint8_t INDEX_NOT_SET = 255;
|
||||
|
||||
uint64_t address_{0};
|
||||
uint8_t index_{INDEX_NOT_SET};
|
||||
OneWireBus *bus_{nullptr}; ///< pointer to OneWireBus instance
|
||||
std::string address_name_;
|
||||
|
||||
/// @brief find an address if necessary
|
||||
/// should be called from setup
|
||||
|
||||
@@ -5,6 +5,9 @@ namespace esphome::opt3001 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "opt3001.sensor");
|
||||
|
||||
static constexpr uint32_t RESULT_RETRY_TIMEOUT_ID = 0;
|
||||
static constexpr uint32_t READ_TIMEOUT_ID = 1;
|
||||
|
||||
static const uint8_t OPT3001_REG_RESULT = 0x00;
|
||||
static const uint8_t OPT3001_REG_CONFIGURATION = 0x01;
|
||||
// See datasheet for full description of each bit.
|
||||
@@ -42,7 +45,7 @@ void OPT3001Sensor::read_result_(const std::function<void(float)> &f) {
|
||||
if ((raw_value & OPT3001_CONFIGURATION_CONVERSION_MODE_MASK) != OPT3001_CONFIGURATION_CONVERSION_MODE_SHUTDOWN) {
|
||||
// not ready; wait 10ms and try again
|
||||
ESP_LOGW(TAG, "Data not ready; waiting 10ms");
|
||||
this->set_timeout("opt3001_wait", 10, [this, f]() { read_result_(f); });
|
||||
this->set_timeout(RESULT_RETRY_TIMEOUT_ID, 10, [this, f]() { read_result_(f); });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -70,7 +73,7 @@ void OPT3001Sensor::read_lx_(const std::function<void(float)> &f) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->set_timeout("read", OPT3001_CONVERSION_TIME_800, [this, f]() {
|
||||
this->set_timeout(READ_TIMEOUT_ID, OPT3001_CONVERSION_TIME_800, [this, f]() {
|
||||
if (this->write(&OPT3001_REG_CONFIGURATION, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Starting configuration register read failed");
|
||||
f(NAN);
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace esphome::output {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "output.button");
|
||||
|
||||
static constexpr uint32_t RESET_TIMEOUT_ID = 0;
|
||||
|
||||
void OutputButton::dump_config() {
|
||||
LOG_BUTTON("", "Output Button", this);
|
||||
ESP_LOGCONFIG(TAG, " Duration: %.1fs", this->duration_ / 1e3f);
|
||||
@@ -12,8 +14,8 @@ void OutputButton::dump_config() {
|
||||
void OutputButton::press_action() {
|
||||
this->output_->turn_on();
|
||||
|
||||
// Use a named timeout so that it's automatically cancelled if button is pressed again before it's reset
|
||||
this->set_timeout("reset", this->duration_, [this]() { this->output_->turn_off(); });
|
||||
// One timeout id, so a second press restarts the reset instead of stacking another
|
||||
this->set_timeout(RESET_TIMEOUT_ID, this->duration_, [this]() { this->output_->turn_off(); });
|
||||
}
|
||||
|
||||
} // namespace esphome::output
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace esphome::pid {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "pid.climate");
|
||||
|
||||
static constexpr uint32_t AUTOTUNE_PROGRESS_INTERVAL_ID = 0;
|
||||
|
||||
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);
|
||||
@@ -191,7 +193,7 @@ void PIDClimate::start_autotune(std::unique_ptr<PIDAutotuner> &&autotune) {
|
||||
"Until your sensor provides a reading, the autotuner may display \'nan\'",
|
||||
LOG_STR_ARG(this->get_log_name()));
|
||||
|
||||
this->set_interval("autotune-progress", 10000, [this]() {
|
||||
this->set_interval(AUTOTUNE_PROGRESS_INTERVAL_ID, 10000, [this]() {
|
||||
if (this->autotuner_ != nullptr && !this->autotuner_->is_finished())
|
||||
this->autotuner_->dump_config();
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace esphome::power_supply {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "power_supply");
|
||||
|
||||
static constexpr uint32_t POWER_OFF_TIMEOUT_ID = 0;
|
||||
|
||||
void PowerSupply::setup() {
|
||||
this->pin_->setup();
|
||||
this->pin_->digital_write(false);
|
||||
@@ -31,7 +33,7 @@ bool PowerSupply::is_enabled() const { return this->active_requests_ != 0; }
|
||||
|
||||
void PowerSupply::request_high_power() {
|
||||
if (this->active_requests_ == 0) {
|
||||
this->cancel_timeout("power-supply-off");
|
||||
this->cancel_timeout(POWER_OFF_TIMEOUT_ID);
|
||||
ESP_LOGV(TAG, "Enabling");
|
||||
this->pin_->digital_write(true);
|
||||
delay(this->enable_time_);
|
||||
@@ -46,7 +48,7 @@ void PowerSupply::unrequest_high_power() {
|
||||
}
|
||||
this->active_requests_--;
|
||||
if (this->active_requests_ == 0) {
|
||||
this->set_timeout("power-supply-off", this->keep_on_time_, [this]() {
|
||||
this->set_timeout(POWER_OFF_TIMEOUT_ID, this->keep_on_time_, [this]() {
|
||||
ESP_LOGV(TAG, "Disabling");
|
||||
this->pin_->digital_write(false);
|
||||
});
|
||||
|
||||
@@ -9,6 +9,8 @@ namespace esphome::pvvx_mithermometer {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "display.pvvx_mithermometer");
|
||||
|
||||
static constexpr uint32_t DISCONNECT_TIMEOUT_ID = 0;
|
||||
|
||||
void PVVXDisplay::dump_config() {
|
||||
char service_buf[ble_device_base::UUID_STR_LEN];
|
||||
char char_buf[ble_device_base::UUID_STR_LEN];
|
||||
@@ -39,7 +41,7 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t
|
||||
case ESP_GATTC_DISCONNECT_EVT:
|
||||
ESP_LOGV(TAG, "[%s] Disconnected", this->parent_->address_str());
|
||||
this->connection_established_ = false;
|
||||
this->cancel_timeout("disconnect");
|
||||
this->cancel_timeout(DISCONNECT_TIMEOUT_ID);
|
||||
this->char_handle_ = 0;
|
||||
break;
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
@@ -147,8 +149,8 @@ void PVVXDisplay::send_to_setup_char_(uint8_t *blk, size_t size) {
|
||||
void PVVXDisplay::delayed_disconnect_() {
|
||||
if (this->disconnect_delay_ms_ == 0)
|
||||
return;
|
||||
this->cancel_timeout("disconnect");
|
||||
this->set_timeout("disconnect", this->disconnect_delay_ms_, [this]() { this->parent_->set_enabled(false); });
|
||||
this->cancel_timeout(DISCONNECT_TIMEOUT_ID);
|
||||
this->set_timeout(DISCONNECT_TIMEOUT_ID, this->disconnect_delay_ms_, [this]() { this->parent_->set_enabled(false); });
|
||||
}
|
||||
|
||||
void PVVXDisplay::sync_time_and_display_() {
|
||||
|
||||
@@ -1019,6 +1019,43 @@ async def nec_action(var, config, args):
|
||||
cg.add(var.set_command_repeats(template_))
|
||||
|
||||
|
||||
# Onkyo RI
|
||||
OnkyoRIData, OnkyoRIBinarySensor, OnkyoRITrigger, OnkyoRIAction, OnkyoRIDumper = (
|
||||
declare_protocol("OnkyoRI")
|
||||
)
|
||||
ONKYORI_SCHEMA = cv.Schema({cv.Required(CONF_DATA): cv.hex_int_range(0, 0xFFF)})
|
||||
|
||||
|
||||
@register_binary_sensor("onkyori", OnkyoRIBinarySensor, ONKYORI_SCHEMA)
|
||||
def onkyori_binary_sensor(var: MockObj, config: ConfigType) -> None:
|
||||
cg.add(
|
||||
var.set_data(
|
||||
cg.StructInitializer(
|
||||
OnkyoRIData,
|
||||
("data", config[CONF_DATA]),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@register_trigger("onkyori", OnkyoRITrigger, OnkyoRIData)
|
||||
def onkyori_trigger(var: MockObj, config: ConfigType) -> None:
|
||||
"""The trigger takes no options beyond the automation."""
|
||||
|
||||
|
||||
@register_dumper("onkyori", OnkyoRIDumper)
|
||||
def onkyori_dumper(var: MockObj, config: ConfigType) -> None:
|
||||
"""The dumper takes no options."""
|
||||
|
||||
|
||||
@register_action("onkyori", OnkyoRIAction, ONKYORI_SCHEMA)
|
||||
async def onkyori_action(
|
||||
var: MockObj, config: ConfigType, args: TemplateArgsType
|
||||
) -> None:
|
||||
template_ = await cg.templatable(config[CONF_DATA], args, cg.uint16)
|
||||
cg.add(var.set_data(template_))
|
||||
|
||||
|
||||
# Pioneer
|
||||
(
|
||||
PioneerData,
|
||||
@@ -2061,11 +2098,20 @@ def midea_dumper(var, config):
|
||||
pass
|
||||
|
||||
|
||||
async def _code_bytes(
|
||||
value: Any, args: TemplateArgsType, set_template: MockObj, set_static: MockObj
|
||||
) -> None:
|
||||
"""Constant codes share one PROGMEM table pool across the remote protocols."""
|
||||
await automation.templatable_bytes(
|
||||
value, args, set_template, set_static, "remote_base_code"
|
||||
)
|
||||
|
||||
|
||||
@register_action("midea", MideaAction, MIDEA_SCHEMA)
|
||||
async def midea_action(var, config, args):
|
||||
vec_ = cg.std_vector.template(cg.uint8)
|
||||
template_ = await cg.templatable(config[CONF_CODE], args, vec_, vec_)
|
||||
cg.add(var.set_code(template_))
|
||||
await _code_bytes(
|
||||
config[CONF_CODE], args, var.set_code_template, var.set_code_static
|
||||
)
|
||||
|
||||
|
||||
# AEHA
|
||||
@@ -2117,10 +2163,9 @@ def aeha_dumper(var, config):
|
||||
async def aeha_action(var, config, args):
|
||||
template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint16)
|
||||
cg.add(var.set_address(template_))
|
||||
template_ = await cg.templatable(
|
||||
config[CONF_DATA], args, cg.std_vector.template(cg.uint8)
|
||||
await _code_bytes(
|
||||
config[CONF_DATA], args, var.set_data_template, var.set_data_static
|
||||
)
|
||||
cg.add(var.set_data(template_))
|
||||
templ = await cg.templatable(config[CONF_CARRIER_FREQUENCY], args, cg.uint32)
|
||||
cg.add(var.set_carrier_frequency(templ))
|
||||
|
||||
@@ -2216,9 +2261,9 @@ def haier_dumper(var, config):
|
||||
|
||||
@register_action("haier", HaierAction, HAIER_SCHEMA)
|
||||
async def haier_action(var, config, args):
|
||||
vec_ = cg.std_vector.template(cg.uint8)
|
||||
template_ = await cg.templatable(config[CONF_CODE], args, vec_, vec_)
|
||||
cg.add(var.set_code(template_))
|
||||
await _code_bytes(
|
||||
config[CONF_CODE], args, var.set_code_template, var.set_code_static
|
||||
)
|
||||
|
||||
|
||||
# ABBWelcome
|
||||
@@ -2359,9 +2404,9 @@ def mirage_dumper(var, config):
|
||||
|
||||
@register_action("mirage", MirageAction, MIRAGE_SCHEMA)
|
||||
async def mirage_action(var, config, args):
|
||||
vec_ = cg.std_vector.template(cg.uint8)
|
||||
template_ = await cg.templatable(config[CONF_CODE], args, vec_, vec_)
|
||||
cg.add(var.set_code(template_))
|
||||
await _code_bytes(
|
||||
config[CONF_CODE], args, var.set_code_template, var.set_code_static
|
||||
)
|
||||
|
||||
|
||||
# Toto
|
||||
|
||||
@@ -28,10 +28,9 @@ DECLARE_REMOTE_PROTOCOL(AEHA)
|
||||
template<typename... Ts> class AEHAAction : public RemoteTransmitterActionBase<Ts...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint16_t, address)
|
||||
TEMPLATABLE_VALUE(std::vector<uint8_t>, data)
|
||||
TEMPLATABLE_BYTES(data)
|
||||
TEMPLATABLE_VALUE(uint32_t, carrier_frequency);
|
||||
|
||||
void set_data(const std::vector<uint8_t> &data) { data_ = data; }
|
||||
void encode(RemoteTransmitData *dst, Ts... x) override {
|
||||
AEHAData data{};
|
||||
data.address = this->address_.value(x...);
|
||||
|
||||
@@ -25,7 +25,7 @@ DECLARE_REMOTE_PROTOCOL(Haier)
|
||||
|
||||
template<typename... Ts> class HaierAction : public RemoteTransmitterActionBase<Ts...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(std::vector<uint8_t>, code)
|
||||
TEMPLATABLE_BYTES(code)
|
||||
|
||||
void encode(RemoteTransmitData *dst, Ts... x) override {
|
||||
HaierData data{};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "remote_base.h"
|
||||
|
||||
@@ -21,6 +22,9 @@ class MideaData {
|
||||
MideaData(const std::vector<uint8_t> &data) {
|
||||
std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin());
|
||||
}
|
||||
MideaData(const uint8_t *data, size_t len) {
|
||||
progmem_memcpy(this->data_.data(), data, std::min(len, this->data_.size()));
|
||||
}
|
||||
|
||||
uint8_t *data() { return this->data_.data(); }
|
||||
const uint8_t *data() const { return this->data_.data(); }
|
||||
@@ -75,10 +79,12 @@ class MideaProtocol : public RemoteProtocol<MideaData> {
|
||||
DECLARE_REMOTE_PROTOCOL(Midea)
|
||||
|
||||
template<typename... Ts> class MideaAction : public RemoteTransmitterActionBase<Ts...> {
|
||||
TEMPLATABLE_VALUE(std::vector<uint8_t>, code)
|
||||
public:
|
||||
TEMPLATABLE_BYTES(code)
|
||||
|
||||
void encode(RemoteTransmitData *dst, Ts... x) override {
|
||||
MideaData data(this->code_.value(x...));
|
||||
MideaData data = this->code_.is_static() ? MideaData(this->code_.data(), this->code_.size())
|
||||
: MideaData(this->code_.value(x...));
|
||||
data.finalize();
|
||||
MideaProtocol().encode(dst, data);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ DECLARE_REMOTE_PROTOCOL(Mirage)
|
||||
|
||||
template<typename... Ts> class MirageAction : public RemoteTransmitterActionBase<Ts...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(std::vector<uint8_t>, code)
|
||||
TEMPLATABLE_BYTES(code)
|
||||
|
||||
void encode(RemoteTransmitData *dst, Ts... x) override {
|
||||
MirageData data{};
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#include "onkyori_protocol.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::remote_base {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "remote.onkyori");
|
||||
|
||||
static constexpr uint8_t NBITS = 12;
|
||||
static constexpr uint16_t MAX_DATA = (1U << NBITS) - 1;
|
||||
static constexpr uint32_t HEADER_HIGH_US = 3000;
|
||||
static constexpr uint32_t HEADER_LOW_US = 1000;
|
||||
static constexpr uint32_t BIT_HIGH_US = 1000;
|
||||
static constexpr uint32_t BIT_ONE_LOW_US = 2000;
|
||||
static constexpr uint32_t BIT_ZERO_LOW_US = 1000;
|
||||
static constexpr uint32_t TRAILER_HIGH_US = 1000;
|
||||
|
||||
void OnkyoRIProtocol::encode(RemoteTransmitData *dst, const OnkyoRIData &data) {
|
||||
if (data.data > MAX_DATA) {
|
||||
ESP_LOGE(TAG, "Data 0x%04X does not fit in a 12-bit RI frame, not sending", data.data);
|
||||
return;
|
||||
}
|
||||
|
||||
// RI is a baseband signal on a wire, so it has no carrier to modulate.
|
||||
dst->set_carrier_frequency(0);
|
||||
dst->reserve(2 + NBITS * 2u + 1);
|
||||
|
||||
dst->item(HEADER_HIGH_US, HEADER_LOW_US);
|
||||
|
||||
for (uint32_t mask = 1UL << (NBITS - 1); mask != 0; mask >>= 1) {
|
||||
dst->item(BIT_HIGH_US, (data.data & mask) != 0 ? BIT_ONE_LOW_US : BIT_ZERO_LOW_US);
|
||||
}
|
||||
|
||||
dst->mark(TRAILER_HIGH_US);
|
||||
}
|
||||
|
||||
optional<OnkyoRIData> OnkyoRIProtocol::decode(RemoteReceiveData src) {
|
||||
OnkyoRIData out{.data = 0};
|
||||
|
||||
if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US))
|
||||
return {};
|
||||
|
||||
for (uint8_t i = 0; i < NBITS; i++) {
|
||||
out.data <<= 1;
|
||||
if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) {
|
||||
out.data |= 1;
|
||||
} else if (!src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
if (!src.expect_mark(TRAILER_HIGH_US))
|
||||
return {};
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void OnkyoRIProtocol::dump(const OnkyoRIData &data) { ESP_LOGI(TAG, "Received OnkyoRI: data=0x%03X", data.data); }
|
||||
|
||||
} // namespace esphome::remote_base
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "remote_base.h"
|
||||
|
||||
namespace esphome::remote_base {
|
||||
|
||||
struct OnkyoRIData {
|
||||
uint16_t data;
|
||||
|
||||
bool operator==(const OnkyoRIData &rhs) const { return data == rhs.data; }
|
||||
};
|
||||
|
||||
class OnkyoRIProtocol : public RemoteProtocol<OnkyoRIData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const OnkyoRIData &data);
|
||||
optional<OnkyoRIData> decode(RemoteReceiveData src);
|
||||
void dump(const OnkyoRIData &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(OnkyoRI)
|
||||
|
||||
template<typename... Ts> class OnkyoRIAction : public RemoteTransmitterActionBase<Ts...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint16_t, data)
|
||||
|
||||
void encode(RemoteTransmitData *dst, Ts... x) override {
|
||||
OnkyoRIData data{};
|
||||
data.data = this->data_.value(x...);
|
||||
OnkyoRIProtocol().encode(dst, data);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::remote_base
|
||||
@@ -11,6 +11,8 @@ namespace esphome::remote_transmitter {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "remote_transmitter");
|
||||
|
||||
static constexpr uint32_t COMPLETE_TIMEOUT_ID = 0;
|
||||
|
||||
// Maximum RMT symbol duration (15-bit field)
|
||||
static constexpr uint32_t RMT_SYMBOL_DURATION_MAX = 0x7FFF;
|
||||
|
||||
@@ -206,7 +208,7 @@ void RemoteTransmitterComponent::wait_for_rmt_() {
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
|
||||
void RemoteTransmitterComponent::flush_pending_completion() {
|
||||
// a frame still on the wire is waited out, and its completion reported, before the next one
|
||||
if (this->non_blocking_ && this->cancel_timeout("complete")) {
|
||||
if (this->non_blocking_ && this->cancel_timeout(COMPLETE_TIMEOUT_ID)) {
|
||||
this->wait_for_rmt_();
|
||||
}
|
||||
}
|
||||
@@ -292,7 +294,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
|
||||
this->status_clear_warning();
|
||||
|
||||
if (this->non_blocking_) {
|
||||
this->set_timeout("complete", total_duration / 1000, [this]() { this->wait_for_rmt_(); });
|
||||
this->set_timeout(COMPLETE_TIMEOUT_ID, total_duration / 1000, [this]() { this->wait_for_rmt_(); });
|
||||
} else {
|
||||
this->wait_for_rmt_();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <pico/platform.h>
|
||||
|
||||
#include "esphome/core/time_conversion.h"
|
||||
|
||||
#define IRAM_ATTR __attribute__((noinline, long_call, section(".time_critical")))
|
||||
@@ -40,6 +42,9 @@ __attribute__((always_inline)) inline bool in_isr_context() {
|
||||
return ipsr != 0;
|
||||
}
|
||||
|
||||
/// arduino-pico runs setup() and loop() on core 0; ESPHome never uses core 1.
|
||||
__attribute__((always_inline)) inline bool is_main_loop_thread() { return !in_isr_context() && get_core_num() == 0; }
|
||||
|
||||
__attribute__((always_inline)) inline void yield() { ::yield(); }
|
||||
__attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); }
|
||||
__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(::micros()); }
|
||||
|
||||
@@ -10,6 +10,8 @@ namespace esphome::scd30 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "scd30");
|
||||
|
||||
static constexpr uint32_t STATUS_CHECK_INTERVAL_ID = 0;
|
||||
|
||||
static const uint16_t SCD30_CMD_GET_FIRMWARE_VERSION = 0xd100;
|
||||
static const uint16_t SCD30_CMD_START_CONTINUOUS_MEASUREMENTS = 0x0010;
|
||||
static const uint16_t SCD30_CMD_ALTITUDE_COMPENSATION = 0x5102;
|
||||
@@ -107,7 +109,7 @@ void SCD30Component::setup() {
|
||||
}
|
||||
|
||||
// check each 500ms if data is ready, and read it in that case
|
||||
this->set_interval("status-check", 500, [this]() {
|
||||
this->set_interval(STATUS_CHECK_INTERVAL_ID, 500, [this]() {
|
||||
if (this->is_data_ready_())
|
||||
this->update();
|
||||
});
|
||||
|
||||
@@ -10,6 +10,8 @@ namespace esphome::sendspin_ {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "sendspin.image");
|
||||
|
||||
static constexpr uint32_t TRANSITION_ACK_TIMEOUT_ID = 0;
|
||||
|
||||
// How long a displayed frame may wait for sendspin.image.transition_finished before a warning
|
||||
// names the missing ack. Generous next to a typical fade of a second or two.
|
||||
static constexpr uint32_t TRANSITION_ACK_WARNING_MS = 10000;
|
||||
@@ -163,7 +165,7 @@ void SendspinImageSlot::on_display_(uint32_t lateness_ms) {
|
||||
// that never reaches the action stalls the slot with nothing in the log. Name the cause after
|
||||
// a generous wait. Arming again replaces the previous timeout, so it cannot fire for a frame
|
||||
// that was already acked and superseded.
|
||||
this->set_timeout("transition_ack", TRANSITION_ACK_WARNING_MS, [this]() {
|
||||
this->set_timeout(TRANSITION_ACK_TIMEOUT_ID, TRANSITION_ACK_WARNING_MS, [this]() {
|
||||
if (this->transition_pending_) {
|
||||
ESP_LOGW(TAG,
|
||||
"Slot %u: displayed artwork was never acknowledged; no new artwork will arrive until "
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
@@ -43,6 +44,7 @@ from esphome.const import (
|
||||
CONF_TIMEOUT,
|
||||
CONF_TO,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TYPE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VALUE,
|
||||
CONF_WEB_SERVER,
|
||||
@@ -222,6 +224,13 @@ def validate_send_first_at(value):
|
||||
FILTER_REGISTRY = Registry()
|
||||
validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
|
||||
|
||||
# Default for streamed readings (radar sensors): hold the last value for a second so a dropped
|
||||
# frame doesn't read as absence, then rate limit. Codegen turns the pair into one TimeoutThrottleFilter.
|
||||
TIMEOUT_THROTTLE_FILTERS = [
|
||||
{"timeout": {"timeout": cv.TimePeriod(milliseconds=1000), "value": "last"}},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
]
|
||||
|
||||
|
||||
def validate_datapoint(value):
|
||||
if isinstance(value, dict):
|
||||
@@ -293,6 +302,7 @@ ThrottleWithPriorityNanFilter = sensor_ns.class_(
|
||||
TimeoutFilterBase = sensor_ns.class_("TimeoutFilterBase", Filter, cg.Component)
|
||||
TimeoutFilterLast = sensor_ns.class_("TimeoutFilterLast", TimeoutFilterBase)
|
||||
TimeoutFilterConfigured = sensor_ns.class_("TimeoutFilterConfigured", TimeoutFilterBase)
|
||||
TimeoutThrottleFilter = sensor_ns.class_("TimeoutThrottleFilter", TimeoutFilterLast)
|
||||
DebounceFilter = sensor_ns.class_("DebounceFilter", Filter)
|
||||
HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter)
|
||||
DeltaFilter = sensor_ns.class_("DeltaFilter", Filter)
|
||||
@@ -683,6 +693,12 @@ THROTTLE_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value(
|
||||
)
|
||||
|
||||
|
||||
def _is_nan_only(values: Any) -> bool:
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
return bool(values) and all(isinstance(v, float) and math.isnan(v) for v in values)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
"throttle_with_priority",
|
||||
ThrottleWithPriorityFilter,
|
||||
@@ -696,7 +712,7 @@ async def throttle_with_priority_filter_to_code(config, filter_id):
|
||||
# omits `value:`) to avoid the TemplatableFn<float> array + NaN lambda the
|
||||
# generic ValueListFilter path requires. Behavior is identical: NaN sensor
|
||||
# readings always bypass the throttle.
|
||||
if values and all(isinstance(v, float) and math.isnan(v) for v in values):
|
||||
if _is_nan_only(values):
|
||||
filter_id = filter_id.copy()
|
||||
filter_id.type = ThrottleWithPriorityNanFilter
|
||||
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT])
|
||||
@@ -941,6 +957,43 @@ async def build_filters(config):
|
||||
return await cg.build_registry_list(FILTER_REGISTRY, config)
|
||||
|
||||
|
||||
def _timeout_throttle_period(
|
||||
first: ConfigType, second: ConfigType
|
||||
) -> cv.TimePeriod | None:
|
||||
"""Period of a `timeout` (value `last`) directly followed by a NaN only `throttle_with_priority`."""
|
||||
timeout = first.get("timeout")
|
||||
throttle = second.get("throttle_with_priority")
|
||||
if not isinstance(timeout, dict) or not isinstance(throttle, dict):
|
||||
return None
|
||||
if timeout[CONF_VALUE] != "last" or timeout[CONF_TIMEOUT] != throttle[CONF_TIMEOUT]:
|
||||
return None
|
||||
if not _is_nan_only(throttle[CONF_VALUE]):
|
||||
return None
|
||||
return timeout[CONF_TIMEOUT]
|
||||
|
||||
|
||||
async def _build_chain_filters(config: list[ConfigType]) -> list:
|
||||
"""Like build_filters, but merges the radar `timeout` + `throttle_with_priority` pair into one filter."""
|
||||
filters = []
|
||||
i = 0
|
||||
while i < len(config):
|
||||
if (
|
||||
i + 1 < len(config)
|
||||
and (period := _timeout_throttle_period(config[i], config[i + 1]))
|
||||
is not None
|
||||
):
|
||||
filter_id = config[i][CONF_TYPE_ID].copy()
|
||||
filter_id.type = TimeoutThrottleFilter
|
||||
var = cg.new_Pvariable(filter_id, period)
|
||||
await cg.register_component(var, {})
|
||||
filters.append(var)
|
||||
i += 2
|
||||
continue
|
||||
filters.append(await cg.build_registry_entry(FILTER_REGISTRY, config[i]))
|
||||
i += 1
|
||||
return filters
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_VALUE, "add_on_state_callback", [(float, "x")]
|
||||
@@ -979,7 +1032,7 @@ async def setup_sensor_core_(var, config):
|
||||
cg.add(var.set_force_update(True))
|
||||
if config.get(CONF_FILTERS): # must exist and not be empty
|
||||
cg.add_define("USE_SENSOR_FILTER")
|
||||
filters = await build_filters(config[CONF_FILTERS])
|
||||
filters = await _build_chain_filters(config[CONF_FILTERS])
|
||||
cg.add(var.set_filters(filters))
|
||||
|
||||
CORE.add_job(_build_sensor_automations, var, config)
|
||||
|
||||
@@ -13,6 +13,17 @@ namespace esphome::sensor {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "sensor.filter");
|
||||
|
||||
/// Shared pass check for throttle_with_priority (NaN only): passes and stamps `last_input` when the
|
||||
/// period has elapsed, on the first value, or for NaN.
|
||||
static bool throttle_nan_passes(uint32_t &last_input, uint32_t period, float value) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (last_input == 0 || now - last_input >= period || std::isnan(value)) {
|
||||
last_input = now;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter
|
||||
void Filter::input(float value) {
|
||||
ESP_LOGVV(TAG, "Filter(%p)::input(%f)", this, value);
|
||||
@@ -266,11 +277,8 @@ optional<float> throttle_with_priority_new_value(Sensor *parent, float value, co
|
||||
ThrottleWithPriorityNanFilter::ThrottleWithPriorityNanFilter(uint32_t min_time_between_inputs)
|
||||
: min_time_between_inputs_(min_time_between_inputs) {}
|
||||
optional<float> ThrottleWithPriorityNanFilter::new_value(float value) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (this->last_input_ == 0 || now - this->last_input_ >= this->min_time_between_inputs_ || std::isnan(value)) {
|
||||
this->last_input_ = now;
|
||||
if (throttle_nan_passes(this->last_input_, this->min_time_between_inputs_, value))
|
||||
return value;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -355,6 +363,23 @@ optional<float> TimeoutFilterConfigured::new_value(float value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// TimeoutThrottleFilter
|
||||
optional<float> TimeoutThrottleFilter::new_value(float value) {
|
||||
TimeoutFilterLast::new_value(value);
|
||||
if (throttle_nan_passes(this->last_input_, this->time_period_, value))
|
||||
return value;
|
||||
return {};
|
||||
}
|
||||
|
||||
void TimeoutThrottleFilter::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (now - this->timeout_start_time_ >= this->time_period_) {
|
||||
if (throttle_nan_passes(this->last_input_, this->time_period_, this->pending_value_))
|
||||
this->output(this->pending_value_);
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
// DebounceFilter
|
||||
optional<float> DebounceFilter::new_value(float value) {
|
||||
App.scheduler.set_timeout(this, this->time_period_, [this, value]() { this->output(value); });
|
||||
|
||||
@@ -479,6 +479,19 @@ class TimeoutFilterConfigured : public TimeoutFilterBase {
|
||||
// Total: 8 (base) + 4 = 12 bytes + vtable ptr + Component overhead
|
||||
};
|
||||
|
||||
/// Codegen use only: `timeout` (value `last`) followed by `throttle_with_priority` (NaN only) with the
|
||||
/// same period, as one object. Output matches the two filter chain exactly.
|
||||
class TimeoutThrottleFilter : public TimeoutFilterLast {
|
||||
public:
|
||||
explicit TimeoutThrottleFilter(uint32_t time_period) : TimeoutFilterLast(time_period) {}
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
void loop() override;
|
||||
|
||||
protected:
|
||||
uint32_t last_input_{0}; // When the throttle stage last let a value through
|
||||
};
|
||||
|
||||
class DebounceFilter : public Filter {
|
||||
public:
|
||||
explicit DebounceFilter(uint32_t time_period);
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace esphome::spa06_base {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "spa06");
|
||||
|
||||
static constexpr uint32_t MEASUREMENT_TIMEOUT_ID = 0;
|
||||
|
||||
// Sign extension function for <=16 bit types
|
||||
inline int16_t decode16(uint8_t msb, uint8_t lsb, size_t bits, size_t head = 0) {
|
||||
return static_cast<int16_t>(encode_uint16(msb, lsb) << head) >> (16 - bits);
|
||||
@@ -243,7 +245,7 @@ void SPA06Component::update() {
|
||||
}
|
||||
|
||||
// Queue a background task for retrieving the measurement
|
||||
this->set_timeout("measurement", this->conversion_time_, [this]() {
|
||||
this->set_timeout(MEASUREMENT_TIMEOUT_ID, this->conversion_time_, [this]() {
|
||||
float raw_temperature;
|
||||
float temperature = 0.0;
|
||||
float pressure = 0.0;
|
||||
|
||||
@@ -52,6 +52,11 @@ static const UBaseType_t ANNOUNCEMENT_PIPELINE_TASK_PRIORITY = 1;
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "speaker_media_player");
|
||||
|
||||
static constexpr uint32_t UNPAUSE_MEDIA_INTERVAL_ID = 0;
|
||||
static constexpr uint32_t NEXT_ANNOUNCEMENT_TIMEOUT_ID = 1;
|
||||
static constexpr uint32_t NEXT_MEDIA_TIMEOUT_ID = 2;
|
||||
static constexpr uint32_t UNPAUSE_ANNOUNCEMENT_INTERVAL_ID = 3;
|
||||
|
||||
void SpeakerMediaPlayer::setup() {
|
||||
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
|
||||
state = media_player::MEDIA_PLAYER_STATE_OFF;
|
||||
@@ -112,13 +117,13 @@ void SpeakerMediaPlayer::set_playlist_delay_ms(AudioPipelineType pipeline_type,
|
||||
void SpeakerMediaPlayer::stop_and_unpause_media_() {
|
||||
this->media_pipeline_->stop();
|
||||
this->unpause_media_remaining_ = 3;
|
||||
this->set_interval("unpause_med", 50, [this]() {
|
||||
this->set_interval(UNPAUSE_MEDIA_INTERVAL_ID, 50, [this]() {
|
||||
if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) {
|
||||
this->cancel_interval("unpause_med");
|
||||
this->cancel_interval(UNPAUSE_MEDIA_INTERVAL_ID);
|
||||
this->media_pipeline_->set_pause_state(false);
|
||||
this->is_paused_ = false;
|
||||
} else if (--this->unpause_media_remaining_ == 0) {
|
||||
this->cancel_interval("unpause_med");
|
||||
this->cancel_interval(UNPAUSE_MEDIA_INTERVAL_ID);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -152,7 +157,7 @@ void SpeakerMediaPlayer::watch_media_commands_() {
|
||||
if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
|
||||
if (!enqueue) {
|
||||
// Ensure the loaded next item doesn't start playing, clear the queue, start the file, and unpause
|
||||
this->cancel_timeout("next_ann");
|
||||
this->cancel_timeout(NEXT_ANNOUNCEMENT_TIMEOUT_ID);
|
||||
this->announcement_playlist_.clear();
|
||||
this->announcement_item_failed_ = false;
|
||||
if (media_command.file.has_value()) {
|
||||
@@ -166,7 +171,7 @@ void SpeakerMediaPlayer::watch_media_commands_() {
|
||||
} else {
|
||||
if (!enqueue) {
|
||||
// Ensure the loaded next item doesn't start playing, clear the queue, start the file, and unpause
|
||||
this->cancel_timeout("next_media");
|
||||
this->cancel_timeout(NEXT_MEDIA_TIMEOUT_ID);
|
||||
this->media_playlist_.clear();
|
||||
this->media_item_failed_ = false;
|
||||
if (this->is_paused_) {
|
||||
@@ -236,23 +241,23 @@ void SpeakerMediaPlayer::watch_media_commands_() {
|
||||
if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
|
||||
#endif
|
||||
if (this->announcement_pipeline_ != nullptr) {
|
||||
this->cancel_timeout("next_ann");
|
||||
this->cancel_timeout(NEXT_ANNOUNCEMENT_TIMEOUT_ID);
|
||||
this->announcement_playlist_.clear();
|
||||
this->announcement_item_failed_ = false;
|
||||
this->announcement_pipeline_->stop();
|
||||
this->unpause_announcement_remaining_ = 3;
|
||||
this->set_interval("unpause_ann", 50, [this]() {
|
||||
this->set_interval(UNPAUSE_ANNOUNCEMENT_INTERVAL_ID, 50, [this]() {
|
||||
if (this->announcement_pipeline_state_ == AudioPipelineState::STOPPED) {
|
||||
this->cancel_interval("unpause_ann");
|
||||
this->cancel_interval(UNPAUSE_ANNOUNCEMENT_INTERVAL_ID);
|
||||
this->announcement_pipeline_->set_pause_state(false);
|
||||
} else if (--this->unpause_announcement_remaining_ == 0) {
|
||||
this->cancel_interval("unpause_ann");
|
||||
this->cancel_interval(UNPAUSE_ANNOUNCEMENT_INTERVAL_ID);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (this->media_pipeline_ != nullptr) {
|
||||
this->cancel_timeout("next_media");
|
||||
this->cancel_timeout(NEXT_MEDIA_TIMEOUT_ID);
|
||||
this->media_playlist_.clear();
|
||||
this->media_item_failed_ = false;
|
||||
this->stop_and_unpause_media_();
|
||||
@@ -408,7 +413,8 @@ void SpeakerMediaPlayer::loop() {
|
||||
this->announcement_pipeline_->set_pause_state(true);
|
||||
// Internally unpause the pipeline after the delay between playlist items. Announcements do not follow the
|
||||
// media player's pause state.
|
||||
this->set_timeout("next_ann", timeout_ms, [this]() { this->announcement_pipeline_->set_pause_state(false); });
|
||||
this->set_timeout(NEXT_ANNOUNCEMENT_TIMEOUT_ID, timeout_ms,
|
||||
[this]() { this->announcement_pipeline_->set_pause_state(false); });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -454,7 +460,7 @@ void SpeakerMediaPlayer::loop() {
|
||||
this->media_pipeline_->set_pause_state(true);
|
||||
// Internally unpause the pipeline after the delay between playlist items, if the media player state is
|
||||
// not paused.
|
||||
this->set_timeout("next_media", timeout_ms,
|
||||
this->set_timeout(NEXT_MEDIA_TIMEOUT_ID, timeout_ms,
|
||||
[this]() { this->media_pipeline_->set_pause_state(this->is_paused_); });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,13 +322,11 @@ optional<size_t> SprinklerValveRunRequest::valve_as_opt() {
|
||||
SprinklerValveOperator *SprinklerValveRunRequest::valve_operator() { return this->valve_op_; }
|
||||
|
||||
Sprinkler::Sprinkler() : Sprinkler("") {}
|
||||
Sprinkler::Sprinkler(const char *name) : name_(name) {
|
||||
// The `name` is stored for dump_config logging
|
||||
this->timer_.init(2);
|
||||
// Timer names only need to be unique within this component instance
|
||||
this->timer_.push_back({"sm", false, 0, 0, [this]() { this->sm_timer_callback_(); }});
|
||||
this->timer_.push_back({"vs", false, 0, 0, [this]() { this->valve_selection_callback_(); }});
|
||||
}
|
||||
// The `name` is stored for dump_config logging; the scheduler keys each timer by its SprinklerTimerIndex
|
||||
Sprinkler::Sprinkler(const char *name)
|
||||
: name_(name),
|
||||
timer_{{{false, 0, 0, [this]() { this->sm_timer_callback_(); }},
|
||||
{false, 0, 0, [this]() { this->valve_selection_callback_(); }}}} {}
|
||||
|
||||
void Sprinkler::setup() {
|
||||
this->all_valves_off_(true);
|
||||
@@ -1520,7 +1518,7 @@ const LogString *Sprinkler::state_as_str_(SprinklerState state) {
|
||||
|
||||
void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) {
|
||||
if (this->timer_duration_(timer_index) > 0) {
|
||||
this->set_timeout(this->timer_[timer_index].name, this->timer_duration_(timer_index),
|
||||
this->set_timeout(static_cast<uint32_t>(timer_index), this->timer_duration_(timer_index),
|
||||
this->timer_cbf_(timer_index));
|
||||
this->timer_[timer_index].start_time = millis();
|
||||
this->timer_[timer_index].active = true;
|
||||
@@ -1531,7 +1529,7 @@ void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) {
|
||||
|
||||
bool Sprinkler::cancel_timer_(const SprinklerTimerIndex timer_index) {
|
||||
this->timer_[timer_index].active = false;
|
||||
return this->cancel_timeout(this->timer_[timer_index].name);
|
||||
return this->cancel_timeout(static_cast<uint32_t>(timer_index));
|
||||
}
|
||||
|
||||
bool Sprinkler::timer_active_(const SprinklerTimerIndex timer_index) { return this->timer_[timer_index].active; }
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "esphome/components/number/number.h"
|
||||
#include "esphome/components/switch/switch.h"
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::sprinkler {
|
||||
@@ -49,7 +50,6 @@ struct SprinklerQueueItem {
|
||||
};
|
||||
|
||||
struct SprinklerTimer {
|
||||
const char *name;
|
||||
bool active;
|
||||
uint32_t time;
|
||||
uint32_t start_time;
|
||||
@@ -567,8 +567,8 @@ class Sprinkler final : public Component {
|
||||
/// Sprinkler valve operator objects
|
||||
std::vector<SprinklerValveOperator> valve_op_{2};
|
||||
|
||||
/// Valve control timers - FixedVector enforces that this can never grow beyond init() size
|
||||
FixedVector<SprinklerTimer> timer_;
|
||||
/// Valve control timers, indexed by SprinklerTimerIndex (also used as the scheduler id)
|
||||
std::array<SprinklerTimer, 2> timer_;
|
||||
|
||||
/// Other Sprinkler instances we should be aware of (used to check if pumps are in use)
|
||||
std::vector<Sprinkler *> other_controllers_;
|
||||
|
||||
@@ -117,8 +117,13 @@ CONFIG_SCHEMA = cv.All(
|
||||
async def to_code(config):
|
||||
var = await alarm_control_panel.new_alarm_control_panel(config)
|
||||
await cg.register_component(var, config)
|
||||
if CONF_CODES in config:
|
||||
cg.add(var.set_codes(config[CONF_CODES]))
|
||||
# Shared flash table ended by nullptr, so the panel stores only a pointer.
|
||||
# requires_code_to_arm needs codes (validate_config) and defaults to false.
|
||||
if codes := config.get(CONF_CODES):
|
||||
table = cg.shared_progmem_array(
|
||||
"alarm_codes", cg.const_char_ptr, [*codes, cg.nullptr]
|
||||
)
|
||||
cg.add(var.set_codes(table))
|
||||
if CONF_REQUIRES_CODE_TO_ARM in config:
|
||||
cg.add(var.set_requires_code_to_arm(config[CONF_REQUIRES_CODE_TO_ARM]))
|
||||
|
||||
|
||||
@@ -36,6 +36,11 @@ static const LogString *sensor_type_to_string(AlarmSensorType type) {
|
||||
#endif
|
||||
|
||||
void TemplateAlarmControlPanel::dump_config() {
|
||||
size_t num_codes = 0;
|
||||
if (this->codes_ != nullptr) {
|
||||
while (this->codes_[num_codes] != nullptr)
|
||||
num_codes++;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"TemplateAlarmControlPanel:\n"
|
||||
" Current State: %s\n"
|
||||
@@ -47,8 +52,8 @@ void TemplateAlarmControlPanel::dump_config() {
|
||||
" Pending Time: %" PRIu32 "s\n"
|
||||
" Trigger Time: %" PRIu32 "s\n"
|
||||
" Supported Features: %" PRIu32,
|
||||
LOG_STR_ARG(alarm_control_panel_state_to_string(this->current_state_)), this->codes_.size(),
|
||||
YESNO(!this->codes_.empty() && this->requires_code_to_arm_), (this->arming_away_time_ / 1000),
|
||||
LOG_STR_ARG(alarm_control_panel_state_to_string(this->current_state_)), num_codes,
|
||||
YESNO(this->codes_ != nullptr && this->requires_code_to_arm_), (this->arming_away_time_ / 1000),
|
||||
(this->arming_home_time_ / 1000), (this->arming_night_time_ / 1000), (this->pending_time_ / 1000),
|
||||
(this->trigger_time_ / 1000), this->get_supported_features());
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
@@ -195,13 +200,12 @@ void TemplateAlarmControlPanel::loop() {
|
||||
}
|
||||
|
||||
bool TemplateAlarmControlPanel::is_code_valid_(optional<std::string> code) {
|
||||
if (!this->codes_.empty()) {
|
||||
if (this->codes_ != nullptr) {
|
||||
if (code.has_value()) {
|
||||
ESP_LOGVV(TAG, "Checking code: %s", code.value().c_str());
|
||||
// Use strcmp for const char* comparison
|
||||
const char *code_cstr = code.value().c_str();
|
||||
for (const char *stored_code : this->codes_) {
|
||||
if (strcmp(stored_code, code_cstr) == 0)
|
||||
for (const char *const *stored = this->codes_; *stored != nullptr; stored++) {
|
||||
if (strcmp(*stored, code_cstr) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user