mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
Merge remote-tracking branch 'origin/dev' into HEAD
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 {
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.22.0
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.23.0
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
+178
-88
@@ -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,13 +257,24 @@ 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)})"
|
||||
|
||||
|
||||
def _names_parent(text: str) -> bool:
|
||||
return any(f == "parent" for _, f, _, _ in string.Formatter().parse(text))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ApplyCall:
|
||||
"""One statement from config keys, e.g. ``"set_range({}, {})"`` with ``((CONF_LOW, cg.float_), ...)``.
|
||||
|
||||
Each arg is ``(conf_key, type_)`` or ``(conf_key, type_, const_fn)``. A ``conf_key`` may be a
|
||||
path into nested sections. A plain ``str`` ``type_`` is raw C++ type text and may use
|
||||
``{parent}``. ``const_fn(config, value)`` renders a constant's argument text; a lambda or an
|
||||
path into nested sections. ``target`` and a plain ``str`` ``type_`` (raw C++ type text) may
|
||||
name the parent object as ``{parent}``. ``const_fn(config, value)`` renders a constant's argument text; a lambda or an
|
||||
id bypasses it. The statement is skipped when none of its keys is set, always emitted when it
|
||||
has no keys, and a partial set is a config error.
|
||||
"""
|
||||
@@ -277,13 +286,13 @@ class ApplyCall:
|
||||
fields = [
|
||||
f for _, f, _, _ in string.Formatter().parse(self.target) if f is not None
|
||||
]
|
||||
if any(fields):
|
||||
if any(f not in ("", "parent") for f in fields):
|
||||
raise ValueError(
|
||||
f"apply target {self.target!r}: only bare {{}} placeholders"
|
||||
f"apply target {self.target!r}: only {{}} and {{parent}} placeholders"
|
||||
)
|
||||
if len(fields) != len(self.args):
|
||||
if (count := fields.count("")) != len(self.args):
|
||||
raise ValueError(
|
||||
f"apply target {self.target!r} has {len(fields)} "
|
||||
f"apply target {self.target!r} has {count} "
|
||||
f"placeholder(s) for {len(self.args)} config key(s)"
|
||||
)
|
||||
if any(len(arg) not in (2, 3) for arg in self.args):
|
||||
@@ -291,6 +300,12 @@ class ApplyCall:
|
||||
f"apply target {self.target!r}: each arg is (conf_key, type_[, const_fn])"
|
||||
)
|
||||
|
||||
@property
|
||||
def names_parent(self) -> bool:
|
||||
return _names_parent(self.target) or any(
|
||||
isinstance(arg[1], str) and _names_parent(arg[1]) for arg in self.args
|
||||
)
|
||||
|
||||
@property
|
||||
def members(self) -> list[tuple[Any, Any, Any]]:
|
||||
"""Each arg as ``(conf_key, type_, const_fn or None)``."""
|
||||
@@ -365,9 +380,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 +421,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,12 +499,12 @@ 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
|
||||
)
|
||||
statements.append(f"{receiver}{target.format(*exprs)};")
|
||||
statements.append(f"{receiver}{target.format(*exprs, parent=parent)};")
|
||||
if call:
|
||||
statements = [
|
||||
f"auto apply_call = {parent}->{call}();",
|
||||
@@ -496,6 +518,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, parent=parent)
|
||||
|
||||
|
||||
def register_apply_condition(
|
||||
name: str, schema: cv.Schema, check: str | ApplyCall, id_key: str = CONF_ID
|
||||
) -> None:
|
||||
@@ -521,22 +564,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 +709,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 +739,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 +1048,78 @@ 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,
|
||||
parent: MockObj | 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. ``when`` may name ``parent`` as ``{parent}``, e.g.
|
||||
``"{parent}->is_fully_open()"``.
|
||||
"""
|
||||
members: list[tuple[Any, Any, Any]] = []
|
||||
if when is not None:
|
||||
call = when if isinstance(when, ApplyCall) else ApplyCall(when)
|
||||
members = call.members
|
||||
if parent is None and call.names_parent:
|
||||
raise ValueError(f"trigger filter {call.target!r} 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 if parent is None else str(parent_ref(parent)),
|
||||
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 +1131,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 +1142,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, parent
|
||||
)
|
||||
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 +1200,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 +1224,7 @@ async def build_callback_automations(
|
||||
entry.args,
|
||||
conf,
|
||||
forwarder=entry.forwarder,
|
||||
params=entry.params,
|
||||
forward=entry.forward,
|
||||
when=entry.when,
|
||||
)
|
||||
|
||||
+51
-15
@@ -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 = (
|
||||
@@ -397,15 +425,24 @@ project({CORE.name})
|
||||
# Emit per-memory-type JSON size data for ESPHome to read post-build.
|
||||
# json2 stays small; raw dumps every symbol (~2s on a large map) and
|
||||
# this command runs inside the link edge, blocking everything downstream.
|
||||
# The map is a BYPRODUCT so ninja knows the link writes it; IDF's size
|
||||
# target depends on the map and can then be built in the same run as all.
|
||||
# IDF's cmakev2 declares the map itself, so drop this line on that switch.
|
||||
# IDF's size target depends on the map, so ninja has to know the link
|
||||
# writes it for size to build in the same run as all. IDF declares the
|
||||
# map itself since espressif/esp-idf#19201 (and in cmakev2), and ninja
|
||||
# rejects two rules for one output, so declare it here only when IDF has
|
||||
# not. add_custom_command(OUTPUT) marks its outputs GENERATED.
|
||||
get_source_file_property(esphome_map_declared
|
||||
${{CMAKE_BINARY_DIR}}/${{CMAKE_PROJECT_NAME}}.map GENERATED)
|
||||
if(esphome_map_declared)
|
||||
set(esphome_map_byproducts)
|
||||
else()
|
||||
set(esphome_map_byproducts BYPRODUCTS ${{CMAKE_BINARY_DIR}}/${{CMAKE_PROJECT_NAME}}.map)
|
||||
endif()
|
||||
add_custom_command(
|
||||
TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD
|
||||
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=json2
|
||||
-o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json
|
||||
${{CMAKE_PROJECT_NAME}}.map
|
||||
BYPRODUCTS ${{CMAKE_BINARY_DIR}}/${{CMAKE_PROJECT_NAME}}.map
|
||||
${{esphome_map_byproducts}}
|
||||
WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}}
|
||||
VERBATIM
|
||||
)
|
||||
@@ -554,10 +591,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()),
|
||||
|
||||
@@ -34,11 +34,13 @@ from esphome.cpp_generator import ( # noqa: F401
|
||||
extern_progmem_array,
|
||||
get_variable,
|
||||
get_variable_with_full_id,
|
||||
is_static_pointer,
|
||||
is_template,
|
||||
new_Pvariable,
|
||||
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_();
|
||||
});
|
||||
|
||||
@@ -760,7 +760,7 @@ message ListEntitiesLightResponse {
|
||||
bool legacy_supports_color_temperature = 8 [deprecated=true];
|
||||
float min_mireds = 9;
|
||||
float max_mireds = 10;
|
||||
repeated string effects = 11 [(container_pointer_no_template) = "FixedVector<const char *>"];
|
||||
repeated string effects = 11 [(container_pointer_no_template) = "light::LightEffectNames"];
|
||||
bool disabled_by_default = 13;
|
||||
string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
EntityCategory entity_category = 15;
|
||||
|
||||
@@ -644,16 +644,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
|
||||
msg.min_mireds = traits.get_min_mireds();
|
||||
msg.max_mireds = traits.get_max_mireds();
|
||||
}
|
||||
FixedVector<const char *> effects_list;
|
||||
if (light->supports_effects()) {
|
||||
auto &light_effects = light->get_effects();
|
||||
effects_list.init(light_effects.size() + 1);
|
||||
effects_list.push_back("None");
|
||||
for (auto *effect : light_effects) {
|
||||
// c_str() is safe as effect names are null-terminated strings from codegen
|
||||
effects_list.push_back(effect->get_name().c_str());
|
||||
}
|
||||
}
|
||||
light::LightEffectNames effects_list(light->get_effects());
|
||||
msg.effects = &effects_list;
|
||||
return fill_and_encode_entity_info(light, msg, conn, remaining_size);
|
||||
}
|
||||
|
||||
@@ -688,8 +688,8 @@ uint8_t *ListEntitiesLightResponse::encode_msg(const void *self, uint8_t *__rest
|
||||
if (uint32_t raw = float_to_raw(msg.max_mireds); raw != 0) [[likely]] {
|
||||
pos = ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 85, raw);
|
||||
}
|
||||
for (const char *it : *msg.effects) {
|
||||
pos = ProtoEncode::encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, 11, it, strlen(it));
|
||||
for (const auto &it : *msg.effects) {
|
||||
pos = ProtoEncode::encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, 11, it);
|
||||
}
|
||||
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 13, msg.disabled_by_default);
|
||||
#ifdef USE_ENTITY_ICON
|
||||
@@ -713,8 +713,8 @@ uint32_t ListEntitiesLightResponse::calc_size_msg(const void *self) {
|
||||
size += ProtoSize::calc_float(1, msg.min_mireds);
|
||||
size += ProtoSize::calc_float(1, msg.max_mireds);
|
||||
if (!msg.effects->empty()) {
|
||||
for (const char *it : *msg.effects) {
|
||||
size += ProtoSize::calc_length_force(1, strlen(it));
|
||||
for (const auto &it : *msg.effects) {
|
||||
size += ProtoSize::calc_length_force(1, it.size());
|
||||
}
|
||||
}
|
||||
size += ProtoSize::calc_bool(1, msg.disabled_by_default);
|
||||
|
||||
@@ -1059,7 +1059,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage {
|
||||
const light::ColorModeMask *supported_color_modes{};
|
||||
float min_mireds{0.0f};
|
||||
float max_mireds{0.0f};
|
||||
const FixedVector<const char *> *effects{};
|
||||
const light::LightEffectNames *effects{};
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#endif
|
||||
|
||||
#ifdef USE_LIGHT
|
||||
#include "esphome/components/light/light_effect_names.h"
|
||||
#include "esphome/components/light/light_traits.h"
|
||||
#endif
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -36,19 +36,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def _subtree_references_uart(node: object, uart_id: str) -> bool:
|
||||
"""Return True if any dict in the subtree has a uart_id entry naming this bus."""
|
||||
if isinstance(node, dict):
|
||||
return any(
|
||||
(key == CONF_UART_ID and str(value) == uart_id)
|
||||
or _subtree_references_uart(value, uart_id)
|
||||
for key, value in node.items()
|
||||
)
|
||||
if isinstance(node, list):
|
||||
return any(_subtree_references_uart(item, uart_id) for item in node)
|
||||
return False
|
||||
|
||||
|
||||
def _reject_debug(uart_conf: ConfigType) -> ConfigType:
|
||||
# The worker tasks use the IDF driver directly, so the uart debugger never sees
|
||||
# bridge traffic and its dummy_receiver would drain RX bytes on the main loop.
|
||||
@@ -86,7 +73,7 @@ def _final_validate(config: ConfigType) -> ConfigType:
|
||||
for domain, domain_conf in full_config.items():
|
||||
if domain == BRIDGE_DOMAIN:
|
||||
continue
|
||||
if _subtree_references_uart(domain_conf, owned_id):
|
||||
if uart.subtree_references_uart(domain_conf, owned_id):
|
||||
raise cv.Invalid(
|
||||
f"The {label} '{owned_id}' is also used by '{domain}'; a bridge "
|
||||
f"requires exclusive use of its {label}.",
|
||||
|
||||
@@ -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); });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection"
|
||||
CONF_ENABLED = "enabled"
|
||||
CONF_GYROSCOPE_ODR = "gyroscope_odr"
|
||||
CONF_GYROSCOPE_RANGE = "gyroscope_range"
|
||||
CONF_HOLD_STATE = "hold_state"
|
||||
CONF_HOST = "host"
|
||||
CONF_HUMIDITY_SOURCE = "humidity_source"
|
||||
CONF_IAQ = "iaq"
|
||||
|
||||
@@ -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);
|
||||
@@ -146,16 +148,16 @@ void CS5460AComponent::schedule_next_check_() {
|
||||
*/
|
||||
if (time_left > -15000) {
|
||||
time_left = 1000;
|
||||
this->status_momentary_warning("warning", 1000);
|
||||
this->status_momentary_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_();
|
||||
});
|
||||
@@ -177,67 +179,67 @@ bool CS5460AComponent::check_status_() {
|
||||
if (status & (1 << 2)) {
|
||||
clear |= 1 << 2;
|
||||
ESP_LOGE(TAG, "Low supply detected");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 3)) {
|
||||
clear |= 1 << 3;
|
||||
ESP_LOGE(TAG, "Modulator oscillation on current channel");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 4)) {
|
||||
clear |= 1 << 4;
|
||||
ESP_LOGE(TAG, "Modulator oscillation on voltage channel");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 5)) {
|
||||
clear |= 1 << 5;
|
||||
ESP_LOGE(TAG, "Watch-dog timeout");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 11)) {
|
||||
clear |= 1 << 11;
|
||||
ESP_LOGE(TAG, "EOUT Energy Accumulation Register out of range");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 12)) {
|
||||
clear |= 1 << 12;
|
||||
ESP_LOGE(TAG, "Energy out of range");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 13)) {
|
||||
clear |= 1 << 13;
|
||||
ESP_LOGE(TAG, "RMS voltage out of range");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 14)) {
|
||||
clear |= 1 << 14;
|
||||
ESP_LOGE(TAG, "RMS current out of range");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 15)) {
|
||||
clear |= 1 << 15;
|
||||
ESP_LOGE(TAG, "Power calculation out of range");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 16)) {
|
||||
clear |= 1 << 16;
|
||||
ESP_LOGE(TAG, "Voltage out of range");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 17)) {
|
||||
clear |= 1 << 17;
|
||||
ESP_LOGE(TAG, "Current out of range");
|
||||
this->status_momentary_warning("warning", 500);
|
||||
this->status_momentary_warning(500);
|
||||
}
|
||||
|
||||
if (status & (1 << 19)) {
|
||||
|
||||
@@ -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.", this->name_.c_str());
|
||||
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.", this->name_.c_str());
|
||||
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.", this->name_.c_str());
|
||||
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.", this->name_.c_str());
|
||||
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;
|
||||
|
||||
@@ -43,9 +43,6 @@ DateEntity = datetime_ns.class_("DateEntity", DateTimeBase)
|
||||
TimeEntity = datetime_ns.class_("TimeEntity", DateTimeBase)
|
||||
DateTimeEntity = datetime_ns.class_("DateTimeEntity", DateTimeBase)
|
||||
|
||||
DateTimeStateTrigger = datetime_ns.class_(
|
||||
"DateTimeStateTrigger", automation.Trigger.template(cg.ESPTime)
|
||||
)
|
||||
|
||||
OnTimeTrigger = datetime_ns.class_(
|
||||
"OnTimeTrigger", automation.Trigger, cg.Component, cg.Parented.template(TimeEntity)
|
||||
@@ -75,11 +72,7 @@ def _validate_time_present(config: ConfigType) -> ConfigType:
|
||||
_DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_ON_VALUE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DateTimeStateTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_VALUE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||
}
|
||||
)
|
||||
@@ -144,9 +137,16 @@ async def setup_datetime_core_(var: MockObj, config: ConfigType) -> None:
|
||||
await mqtt.register_mqtt_component(mqtt_, config)
|
||||
if web_server_config := config.get(CONF_WEB_SERVER):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
# The state callback carries nothing; the automation gets the value as an ESPTime.
|
||||
for conf in config.get(CONF_ON_VALUE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(cg.ESPTime, "x")], conf)
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_on_state_callback",
|
||||
[(cg.ESPTime, "x")],
|
||||
conf,
|
||||
params=[],
|
||||
forward=[automation.parent_ref(var).state_as_esptime()],
|
||||
)
|
||||
|
||||
if CONF_TIME_ID in config:
|
||||
rtc = await cg.get_variable(config[CONF_TIME_ID])
|
||||
|
||||
@@ -31,14 +31,4 @@ class DateTimeBase : public EntityBase {
|
||||
#endif
|
||||
};
|
||||
|
||||
class DateTimeStateTrigger final : public Trigger<ESPTime> {
|
||||
public:
|
||||
explicit DateTimeStateTrigger(DateTimeBase *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this]() { this->trigger(this->parent_->state_as_esptime()); });
|
||||
}
|
||||
|
||||
protected:
|
||||
DateTimeBase *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::datetime
|
||||
|
||||
@@ -58,7 +58,7 @@ void DebugComponent::loop() {
|
||||
if (new_free_heap < this->free_heap_ / 2) {
|
||||
this->free_heap_ = new_free_heap;
|
||||
ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_);
|
||||
this->status_momentary_warning("heap", 1000);
|
||||
this->status_momentary_warning(1000);
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "debug_component.h"
|
||||
#ifdef USE_ZEPHYR
|
||||
#include <cinttypes>
|
||||
#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>
|
||||
@@ -399,18 +401,17 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
auto uicr = [](volatile uint32_t *data, uint8_t size) {
|
||||
std::string res;
|
||||
// 13 words of 8 hex digits, separated by spaces
|
||||
char uicr_buf[13 * 9];
|
||||
auto uicr = [&uicr_buf](volatile uint32_t *data, uint8_t size) {
|
||||
size_t len = 0;
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
if (i > 0) {
|
||||
res += ' ';
|
||||
}
|
||||
res += format_hex_pretty<uint32_t>(data[i], '\0', false);
|
||||
len = buf_append_printf(uicr_buf, sizeof(uicr_buf), len, i > 0 ? " %08" PRIX32 : "%08" PRIX32, data[i]);
|
||||
}
|
||||
return res;
|
||||
return uicr_buf;
|
||||
};
|
||||
ESP_LOGD(TAG, " NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str());
|
||||
ESP_LOGD(TAG, " NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str());
|
||||
ESP_LOGD(TAG, " NRFFW %s", uicr(NRF_UICR->NRFFW, 13));
|
||||
ESP_LOGD(TAG, " NRFHW %s", uicr(NRF_UICR->NRFHW, 12));
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
log_peripherals_info();
|
||||
#endif
|
||||
|
||||
@@ -111,7 +111,17 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
if (this->sleep_duration_.has_value())
|
||||
esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
|
||||
|
||||
// Single pin wakeup (ext0) - ESP32, S2, S3 only
|
||||
#ifndef SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP
|
||||
#ifdef USE_GPIO_HOLD
|
||||
// Variants without per-pin deep sleep hold need the global enable
|
||||
gpio_deep_sleep_hold_en();
|
||||
#else
|
||||
if (this->wakeup_pin_ != nullptr)
|
||||
gpio_deep_sleep_hold_en();
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Single pin wakeup (ext0) - ESP32, S2, S3 only
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
@@ -124,11 +134,6 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
}
|
||||
gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
|
||||
gpio_hold_en(gpio_pin);
|
||||
#if !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP
|
||||
// Some ESP32 variants support holding a single GPIO during deep sleep without this function
|
||||
// For those variants, gpio_hold_en() is sufficient to hold the pin state during deep sleep
|
||||
gpio_deep_sleep_hold_en();
|
||||
#endif
|
||||
bool level = !this->wakeup_pin_->is_inverted();
|
||||
if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
|
||||
level = !level;
|
||||
|
||||
@@ -33,7 +33,7 @@ class DemoAlarmControlPanel final : public AlarmControlPanel, public Component {
|
||||
case ACP_STATE_ARMED_AWAY:
|
||||
if (this->get_requires_code_to_arm()) {
|
||||
if (!code.has_value() || *code != "1234") {
|
||||
this->status_momentary_error("invalid_code", 5000);
|
||||
this->status_momentary_error(5000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ class DemoAlarmControlPanel final : public AlarmControlPanel, public Component {
|
||||
case ACP_STATE_DISARMED:
|
||||
if (this->get_requires_code()) {
|
||||
if (!code.has_value() || *code != "1234") {
|
||||
this->status_momentary_error("invalid_code", 5000);
|
||||
this->status_momentary_error(5000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -20,9 +20,10 @@ from esphome.const import (
|
||||
CONF_ON_VALUE,
|
||||
CONF_SWITCH,
|
||||
CONF_TEXT,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TYPE,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@numo68"]
|
||||
DOMAIN = "display_menu_base"
|
||||
@@ -86,26 +87,6 @@ MENU_MODES = {
|
||||
CONF_JOYSTICK: MenuMode.MENU_MODE_JOYSTICK,
|
||||
}
|
||||
|
||||
DisplayMenuOnEnterTrigger = display_menu_base_ns.class_(
|
||||
"DisplayMenuOnEnterTrigger", automation.Trigger
|
||||
)
|
||||
|
||||
DisplayMenuOnLeaveTrigger = display_menu_base_ns.class_(
|
||||
"DisplayMenuOnLeaveTrigger", automation.Trigger
|
||||
)
|
||||
|
||||
DisplayMenuOnValueTrigger = display_menu_base_ns.class_(
|
||||
"DisplayMenuOnValueTrigger", automation.Trigger
|
||||
)
|
||||
|
||||
DisplayMenuOnNextTrigger = display_menu_base_ns.class_(
|
||||
"DisplayMenuOnNextTrigger", automation.Trigger
|
||||
)
|
||||
|
||||
DisplayMenuOnPrevTrigger = display_menu_base_ns.class_(
|
||||
"DisplayMenuOnPrevTrigger", automation.Trigger
|
||||
)
|
||||
|
||||
|
||||
def validate_format(format):
|
||||
if re.search(r"^%[+-]*(\d+)?(\.\d+)?[fg]$", format) is None:
|
||||
@@ -129,44 +110,20 @@ MENU_ITEM_COMMON_SCHEMA = cv.Schema(
|
||||
|
||||
MENU_ITEM_ENTER_LEAVE_SCHEMA = MENU_ITEM_COMMON_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_ON_ENTER): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayMenuOnEnterTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_LEAVE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayMenuOnLeaveTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ENTER): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_LEAVE): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
|
||||
MENU_ITEM_VALUE_SCHEMA = MENU_ITEM_COMMON_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_ON_VALUE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayMenuOnValueTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_VALUE): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
|
||||
MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA = MENU_ITEM_ENTER_LEAVE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_ON_VALUE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayMenuOnValueTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_VALUE): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -230,20 +187,8 @@ MENU_ITEM_SCHEMA = cv.typed_schema(
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(MenuItemCustom),
|
||||
cv.Optional(CONF_IMMEDIATE_EDIT, default=False): cv.boolean,
|
||||
cv.Optional(CONF_VALUE_LAMBDA): cv.returning_lambda,
|
||||
cv.Optional(CONF_ON_NEXT): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayMenuOnNextTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_PREV): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayMenuOnPrevTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_NEXT): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_PREV): automation.validate_automation({}),
|
||||
}
|
||||
),
|
||||
},
|
||||
@@ -256,20 +201,8 @@ DISPLAY_MENU_BASE_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
|
||||
cv.GenerateID(CONF_ROOT_ITEM_ID): cv.declare_id(MenuItemMenu),
|
||||
cv.Optional(CONF_MODE, default=CONF_ROTARY): cv.enum(MENU_MODES),
|
||||
cv.Optional(CONF_ON_ENTER): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayMenuOnEnterTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_LEAVE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayMenuOnLeaveTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ENTER): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_LEAVE): automation.validate_automation({}),
|
||||
cv.Required(CONF_ITEMS): cv.All(
|
||||
cv.ensure_list(MENU_ITEM_SCHEMA), cv.Length(min=1)
|
||||
),
|
||||
@@ -302,6 +235,24 @@ automation.register_apply_condition(
|
||||
)
|
||||
|
||||
|
||||
# Each item callback carries nothing; the automation receives the item itself.
|
||||
_ITEM_CALLBACKS = (
|
||||
(CONF_ON_ENTER, "add_on_enter_callback"),
|
||||
(CONF_ON_LEAVE, "add_on_leave_callback"),
|
||||
(CONF_ON_VALUE, "add_on_value_callback"),
|
||||
(CONF_ON_NEXT, "add_on_next_callback"),
|
||||
(CONF_ON_PREV, "add_on_prev_callback"),
|
||||
)
|
||||
|
||||
|
||||
async def _build_item_automations(item: MockObj, config: ConfigType) -> None:
|
||||
for conf_key, callback_method in _ITEM_CALLBACKS:
|
||||
for conf in config.get(conf_key, []):
|
||||
await automation.build_parent_callback_automation(
|
||||
item, callback_method, (MenuItemConstPtr, "it"), conf
|
||||
)
|
||||
|
||||
|
||||
async def menu_item_to_code(menu, config, parent):
|
||||
if config[CONF_TYPE] in MENU_ITEMS_WITH_SPECIALIZED_CLASSES:
|
||||
item = cg.new_Pvariable(config[CONF_ID])
|
||||
@@ -340,21 +291,7 @@ async def menu_item_to_code(menu, config, parent):
|
||||
cg.add(item.set_switch_variable(var))
|
||||
cg.add(item.set_on_text(config[CONF_ON_TEXT]))
|
||||
cg.add(item.set_off_text(config[CONF_OFF_TEXT]))
|
||||
for conf in config.get(CONF_ON_ENTER, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
|
||||
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
|
||||
for conf in config.get(CONF_ON_LEAVE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
|
||||
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
|
||||
for conf in config.get(CONF_ON_VALUE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
|
||||
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
|
||||
for conf in config.get(CONF_ON_NEXT, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
|
||||
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
|
||||
for conf in config.get(CONF_ON_PREV, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
|
||||
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
|
||||
await _build_item_automations(item, config)
|
||||
|
||||
|
||||
async def display_menu_to_code(menu, config):
|
||||
@@ -364,9 +301,4 @@ async def display_menu_to_code(menu, config):
|
||||
cg.add(menu.set_mode(config[CONF_MODE]))
|
||||
for c in config[CONF_ITEMS]:
|
||||
await menu_item_to_code(menu, c, root_item)
|
||||
for conf in config.get(CONF_ON_ENTER, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], root_item)
|
||||
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
|
||||
for conf in config.get(CONF_ON_LEAVE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], root_item)
|
||||
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
|
||||
await _build_item_automations(root_item, config)
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "display_menu_base.h"
|
||||
|
||||
namespace esphome::display_menu_base {
|
||||
|
||||
class DisplayMenuOnEnterTrigger final : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnEnterTrigger(MenuItem *parent) : parent_(parent) {
|
||||
parent->add_on_enter_callback([this]() { this->trigger(this->parent_); });
|
||||
}
|
||||
|
||||
protected:
|
||||
MenuItem *parent_;
|
||||
};
|
||||
|
||||
class DisplayMenuOnLeaveTrigger final : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) : parent_(parent) {
|
||||
parent->add_on_leave_callback([this]() { this->trigger(this->parent_); });
|
||||
}
|
||||
|
||||
protected:
|
||||
MenuItem *parent_;
|
||||
};
|
||||
|
||||
class DisplayMenuOnValueTrigger final : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnValueTrigger(MenuItem *parent) : parent_(parent) {
|
||||
parent->add_on_value_callback([this]() { this->trigger(this->parent_); });
|
||||
}
|
||||
|
||||
protected:
|
||||
MenuItem *parent_;
|
||||
};
|
||||
|
||||
class DisplayMenuOnNextTrigger final : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) : parent_(parent) {
|
||||
parent->add_on_next_callback([this]() { this->trigger(this->parent_); });
|
||||
}
|
||||
|
||||
protected:
|
||||
MenuItemCustom *parent_;
|
||||
};
|
||||
|
||||
class DisplayMenuOnPrevTrigger final : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) : parent_(parent) {
|
||||
parent->add_on_prev_callback([this]() { this->trigger(this->parent_); });
|
||||
}
|
||||
|
||||
protected:
|
||||
MenuItemCustom *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::display_menu_base
|
||||
@@ -1,12 +1,8 @@
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, uart
|
||||
from esphome.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from esphome.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from esphome.components.text_sensor import DOMAIN as TEXT_SENSOR_DOMAIN
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
@@ -16,10 +12,9 @@ from esphome.const import (
|
||||
CONF_RECEIVE_TIMEOUT,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@SimonFischer04", "@Tomer27cz", "@latonita", "@PolarGoose"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
DOMAIN = "dlms_meter"
|
||||
@@ -33,19 +28,17 @@ CONF_SKIP_CRC = "skip_crc"
|
||||
CONF_DEFAULT_OBIS = "default_obis"
|
||||
CONF_PROVIDER = "provider"
|
||||
|
||||
DLMS_PARSER_VERSION = "2.2.0"
|
||||
|
||||
dlms_meter_component_ns = cg.esphome_ns.namespace("dlms_meter")
|
||||
DlmsMeterComponent = dlms_meter_component_ns.class_(
|
||||
"DlmsMeterComponent", cg.Component, uart.UARTDevice
|
||||
)
|
||||
CustomPattern = dlms_meter_component_ns.struct("CustomPattern")
|
||||
ObisId = cg.global_ns.namespace("dlms_parser").class_("ObisId")
|
||||
|
||||
|
||||
def obis_code(value: Any) -> str:
|
||||
# Normalize the OBIS code to the strict A.B.C.D.E.F format
|
||||
bytes_list = parse_obis_code_bytes(value)
|
||||
return ".".join(str(b) for b in bytes_list)
|
||||
|
||||
|
||||
def parse_obis_code_bytes(value: Any) -> list[int]:
|
||||
def obis_string_to_byte_list(value: Any) -> list[int]:
|
||||
value = cv.string(value)
|
||||
normalized = re.sub(r"[\-\:\*]", ".", value)
|
||||
parts = normalized.split(".")
|
||||
@@ -63,82 +56,51 @@ def parse_obis_code_bytes(value: Any) -> list[int]:
|
||||
return bytes_list
|
||||
|
||||
|
||||
def to_obis_id_struct(value: list[int]) -> cg.Expression:
|
||||
return ObisId(*value)
|
||||
|
||||
|
||||
_request_sensor_slot = cg.slot_counter("DLMS_MAX_SENSORS")
|
||||
_request_text_sensor_slot = cg.slot_counter("DLMS_MAX_TEXT_SENSORS")
|
||||
_request_binary_sensor_slot = cg.slot_counter("DLMS_MAX_BINARY_SENSORS")
|
||||
|
||||
|
||||
def register_sensor(hub: MockObj, obis: list[int], var: MockObj) -> None:
|
||||
_request_sensor_slot(str(hub))
|
||||
cg.add(hub.register_sensor(to_obis_id_struct(obis), var))
|
||||
|
||||
|
||||
def register_text_sensor(hub: MockObj, obis: list[int], var: MockObj) -> None:
|
||||
_request_text_sensor_slot(str(hub))
|
||||
cg.add(hub.register_text_sensor(to_obis_id_struct(obis), var))
|
||||
|
||||
|
||||
def register_binary_sensor(hub: MockObj, obis: list[int], var: MockObj) -> None:
|
||||
_request_binary_sensor_slot(str(hub))
|
||||
cg.add(hub.register_binary_sensor(to_obis_id_struct(obis), var))
|
||||
|
||||
|
||||
def custom_pattern_dict(value: Any) -> ConfigType:
|
||||
if isinstance(value, str):
|
||||
return {CONF_PATTERN: value}
|
||||
return value
|
||||
|
||||
|
||||
def validate_custom_pattern(value: ConfigType) -> ConfigType:
|
||||
if CONF_DEFAULT_OBIS in value and CONF_NAME not in value:
|
||||
raise cv.Invalid(f"'{CONF_DEFAULT_OBIS}' requires '{CONF_NAME}' to be set")
|
||||
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(
|
||||
{
|
||||
cv.Required(CONF_PATTERN): cv.string,
|
||||
cv.Optional(CONF_NAME): cv.string,
|
||||
cv.Optional(CONF_NAME, default="CUSTOM"): cv.string,
|
||||
cv.Optional(CONF_PRIORITY, default=0): cv.int_,
|
||||
cv.Optional(CONF_DEFAULT_OBIS): parse_obis_code_bytes,
|
||||
cv.Optional(
|
||||
CONF_DEFAULT_OBIS, default="0.0.0.0.0.0"
|
||||
): obis_string_to_byte_list,
|
||||
}
|
||||
),
|
||||
validate_custom_pattern,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DlmsMeterComponent),
|
||||
@@ -146,115 +108,57 @@ 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)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
dec_key_expr = cg.RawExpression("std::nullopt")
|
||||
if dec_key := config.get(CONF_DECRYPTION_KEY):
|
||||
key_bytes = [str(int(dec_key[i : i + 2], 16)) for i in range(0, 32, 2)]
|
||||
dec_key_expr = cg.RawExpression(
|
||||
f"std::array<uint8_t, 16>{{{', '.join(key_bytes)}}}"
|
||||
custom_patterns = [
|
||||
cg.StructInitializer(
|
||||
CustomPattern,
|
||||
(CONF_PATTERN, pattern[CONF_PATTERN]),
|
||||
(CONF_NAME, pattern[CONF_NAME]),
|
||||
(CONF_PRIORITY, pattern[CONF_PRIORITY]),
|
||||
(CONF_DEFAULT_OBIS, to_obis_id_struct(pattern[CONF_DEFAULT_OBIS])),
|
||||
)
|
||||
|
||||
auth_key_expr = cg.RawExpression("std::nullopt")
|
||||
if auth_key := config.get(CONF_AUTH_KEY):
|
||||
key_bytes = [str(int(auth_key[i : i + 2], 16)) for i in range(0, 32, 2)]
|
||||
auth_key_expr = cg.RawExpression(
|
||||
f"std::array<uint8_t, 16>{{{', '.join(key_bytes)}}}"
|
||||
)
|
||||
|
||||
patterns = []
|
||||
if custom_patterns := config.get(CONF_CUSTOM_PATTERNS):
|
||||
for p in custom_patterns:
|
||||
name_expr = cg.RawExpression("std::nullopt")
|
||||
if name_val := p.get(CONF_NAME):
|
||||
name_expr = name_val
|
||||
|
||||
if obis_vals := p.get(CONF_DEFAULT_OBIS):
|
||||
obis_expr = cg.RawExpression(
|
||||
f"std::array<uint8_t, 6>{{{obis_vals[0]}, {obis_vals[1]}, {obis_vals[2]}, {obis_vals[3]}, {obis_vals[4]}, {obis_vals[5]}}}"
|
||||
)
|
||||
else:
|
||||
obis_expr = cg.RawExpression("std::nullopt")
|
||||
|
||||
patterns.append(
|
||||
cg.ArrayInitializer(
|
||||
p[CONF_PATTERN],
|
||||
name_expr,
|
||||
p.get(CONF_PRIORITY, 0),
|
||||
obis_expr,
|
||||
)
|
||||
)
|
||||
|
||||
patterns_expr = (
|
||||
cg.ArrayInitializer(*patterns) if patterns else cg.RawExpression("{}")
|
||||
)
|
||||
for pattern in config.get(CONF_CUSTOM_PATTERNS, [])
|
||||
]
|
||||
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
config[CONF_RECEIVE_TIMEOUT],
|
||||
config[CONF_SKIP_CRC],
|
||||
dec_key_expr,
|
||||
auth_key_expr,
|
||||
patterns_expr,
|
||||
config.get(CONF_DECRYPTION_KEY, cg.nullptr),
|
||||
config.get(CONF_AUTH_KEY, cg.nullptr),
|
||||
cg.ArrayInitializer(*custom_patterns, multiline=True),
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
if CORE.is_esp32:
|
||||
esp32.add_idf_component(name="esphome/dlms_parser", ref="1.1.0")
|
||||
esp32.add_idf_component(name="esphome/dlms_parser", ref=DLMS_PARSER_VERSION)
|
||||
else:
|
||||
cg.add_library("esphome/dlms_parser", "1.1.0")
|
||||
cg.add_library("esphome/dlms_parser", DLMS_PARSER_VERSION)
|
||||
|
||||
@@ -3,14 +3,20 @@ from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code
|
||||
from .. import (
|
||||
CONF_DLMS_METER_ID,
|
||||
CONF_OBIS_CODE,
|
||||
DlmsMeterComponent,
|
||||
obis_string_to_byte_list,
|
||||
register_binary_sensor,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["dlms_meter"]
|
||||
|
||||
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend(
|
||||
{
|
||||
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
||||
cv.Required(CONF_OBIS_CODE): obis_code,
|
||||
cv.Required(CONF_OBIS_CODE): obis_string_to_byte_list,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -18,4 +24,4 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend(
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
cg.add(hub.register_binary_sensor(config[CONF_OBIS_CODE], var))
|
||||
register_binary_sensor(hub, config[CONF_OBIS_CODE], var)
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// Ignore Zephyr. It doesn't have any encryption library.
|
||||
#if defined(USE_ESP32) || defined(USE_ARDUINO) || defined(USE_HOST)
|
||||
|
||||
#include "dlms_meter.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstdio>
|
||||
|
||||
namespace esphome::dlms_meter {
|
||||
@@ -31,54 +36,40 @@ static void log_callback(dlms_parser::LogLevel level, const char *fmt, va_list a
|
||||
}
|
||||
}
|
||||
|
||||
DlmsMeterComponent::DlmsMeterComponent(uint32_t receive_timeout_ms, bool skip_crc_check,
|
||||
std::optional<std::array<uint8_t, 16>> decryption_key,
|
||||
std::optional<std::array<uint8_t, 16>> authentication_key,
|
||||
DlmsMeterComponent::DlmsMeterComponent(const uint32_t receive_timeout_ms, const bool skip_crc_check,
|
||||
const char *decryption_key, const char *authentication_key,
|
||||
std::vector<CustomPattern> custom_patterns)
|
||||
: receive_timeout_ms_(receive_timeout_ms),
|
||||
skip_crc_check_(skip_crc_check),
|
||||
custom_patterns_(std::move(custom_patterns)),
|
||||
parser_(&decryptor_) {
|
||||
parser_([this](const dlms_parser::AxdrCapture &capture) { this->on_data_(capture); }, &this->decryptor_) {
|
||||
dlms_parser::Logger::set_log_function(log_callback);
|
||||
|
||||
if (decryption_key.has_value()) {
|
||||
#ifdef DLMS_METER_NO_CRYPTO
|
||||
ESP_LOGE(TAG, "Decryption is not supported on this platform (no compatible crypto library found)");
|
||||
#else
|
||||
auto opt_key = dlms_parser::Aes128GcmDecryptionKey::from_bytes(decryption_key.value());
|
||||
if (opt_key) {
|
||||
this->parser_.set_decryption_key(*opt_key);
|
||||
if (decryption_key != nullptr) {
|
||||
const auto key = dlms_parser::Aes128GcmDecryptionKey::from_hex(decryption_key);
|
||||
if (key.has_value()) {
|
||||
this->parser_.set_decryption_key(*key);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to set decryption key: invalid key format");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (authentication_key.has_value()) {
|
||||
#ifdef DLMS_METER_NO_CRYPTO
|
||||
ESP_LOGE(TAG, "Authentication is not supported on this platform (no compatible crypto library found)");
|
||||
#else
|
||||
auto opt_key = dlms_parser::Aes128GcmAuthenticationKey::from_bytes(authentication_key.value());
|
||||
if (opt_key) {
|
||||
this->parser_.set_authentication_key(*opt_key);
|
||||
if (authentication_key != nullptr) {
|
||||
const auto key = dlms_parser::Aes128GcmAuthenticationKey::from_hex(authentication_key);
|
||||
if (key.has_value()) {
|
||||
this->parser_.set_authentication_key(*key);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to set authentication key: invalid key format");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
this->parser_.set_skip_crc_check(this->skip_crc_check_);
|
||||
|
||||
this->parser_.load_default_patterns();
|
||||
for (const auto &pattern : this->custom_patterns_) {
|
||||
if (pattern.default_obis.has_value() && pattern.name.has_value()) {
|
||||
this->parser_.register_pattern(pattern.name->c_str(), pattern.pattern.c_str(), pattern.priority,
|
||||
pattern.default_obis.value());
|
||||
} else if (pattern.name.has_value()) {
|
||||
this->parser_.register_pattern(pattern.name->c_str(), pattern.pattern.c_str(), pattern.priority);
|
||||
} else {
|
||||
this->parser_.register_pattern(pattern.pattern.c_str());
|
||||
}
|
||||
|
||||
for (const auto &custom_pattern : this->custom_patterns_) {
|
||||
this->parser_.register_pattern(custom_pattern.name, custom_pattern.pattern, custom_pattern.priority,
|
||||
custom_pattern.default_obis);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,36 +80,36 @@ void DlmsMeterComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " Receive Timeout: %" PRIu32 " ms", this->receive_timeout_ms_);
|
||||
ESP_LOGCONFIG(TAG, " Skip CRC Check: %s", YESNO(this->skip_crc_check_));
|
||||
|
||||
for (const auto &pattern : this->custom_patterns_) {
|
||||
if (pattern.default_obis.has_value() && pattern.name.has_value()) {
|
||||
const auto &obis = pattern.default_obis.value();
|
||||
ESP_LOGCONFIG(TAG, " Custom Pattern: '%s' (name: %s, priority: %d, default_obis: %d.%d.%d.%d.%d.%d)",
|
||||
pattern.pattern.c_str(), pattern.name->c_str(), pattern.priority, obis[0], obis[1], obis[2],
|
||||
obis[3], obis[4], obis[5]);
|
||||
} else if (pattern.name.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " Custom Pattern: '%s' (name: %s, priority: %d)", pattern.pattern.c_str(),
|
||||
pattern.name->c_str(), pattern.priority);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Custom Pattern: '%s'", pattern.pattern.c_str());
|
||||
}
|
||||
std::array<char, 24> obis_buf;
|
||||
|
||||
for (const auto &custom_pattern : this->custom_patterns_) {
|
||||
const auto default_obis = custom_pattern.default_obis.to_string(obis_buf);
|
||||
ESP_LOGCONFIG(TAG, " Custom Pattern: '%s' (name: %s, priority: %d, default_obis: %.*s)", custom_pattern.pattern,
|
||||
custom_pattern.name, custom_pattern.priority, static_cast<int>(default_obis.size()),
|
||||
default_obis.data());
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
for (const auto &entry : this->sensors_) {
|
||||
LOG_SENSOR(" ", "Numeric Sensor (OBIS)", entry.sensor);
|
||||
ESP_LOGCONFIG(TAG, " OBIS: %s", entry.obis_code.c_str());
|
||||
for (const auto &item : this->sensors_) {
|
||||
LOG_SENSOR(" ", "Numeric Sensor (OBIS)", item.sensor);
|
||||
const auto obis = item.obis.to_string(obis_buf);
|
||||
ESP_LOGCONFIG(TAG, " OBIS: %.*s", static_cast<int>(obis.size()), obis.data());
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
for (const auto &entry : this->text_sensors_) {
|
||||
LOG_TEXT_SENSOR(" ", "Text Sensor (OBIS)", entry.sensor);
|
||||
ESP_LOGCONFIG(TAG, " OBIS: %s", entry.obis_code.c_str());
|
||||
for (const auto &item : this->text_sensors_) {
|
||||
LOG_TEXT_SENSOR(" ", "Text Sensor (OBIS)", item.sensor);
|
||||
const auto obis = item.obis.to_string(obis_buf);
|
||||
ESP_LOGCONFIG(TAG, " OBIS: %.*s", static_cast<int>(obis.size()), obis.data());
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
for (const auto &entry : this->binary_sensors_) {
|
||||
LOG_BINARY_SENSOR(" ", "Binary Sensor (OBIS)", entry.sensor);
|
||||
ESP_LOGCONFIG(TAG, " OBIS: %s", entry.obis_code.c_str());
|
||||
for (const auto &item : this->binary_sensors_) {
|
||||
LOG_BINARY_SENSOR(" ", "Binary Sensor (OBIS)", item.sensor);
|
||||
const auto obis = item.obis.to_string(obis_buf);
|
||||
ESP_LOGCONFIG(TAG, " OBIS: %.*s", static_cast<int>(obis.size()), obis.data());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -166,71 +157,73 @@ void DlmsMeterComponent::read_rx_buffer_() {
|
||||
void DlmsMeterComponent::process_frame_() {
|
||||
ESP_LOGV(TAG, "Processing frame of size: %zu bytes", this->bytes_accumulated_);
|
||||
|
||||
auto callback = [this](const char *obis_code, float float_val, const char *str_val, bool is_numeric) {
|
||||
this->on_data_(obis_code, float_val, str_val, is_numeric);
|
||||
};
|
||||
|
||||
this->parser_.parse({this->rx_buffer_.data(), this->bytes_accumulated_}, callback);
|
||||
this->parser_.parse({this->rx_buffer_.data(), this->bytes_accumulated_});
|
||||
|
||||
this->bytes_accumulated_ = 0;
|
||||
}
|
||||
|
||||
void DlmsMeterComponent::on_data_(const char *obis_code, float float_val, const char *str_val, bool is_numeric) {
|
||||
void DlmsMeterComponent::on_data_(const dlms_parser::AxdrCapture &capture) {
|
||||
int updated_count = 0;
|
||||
|
||||
if (capture.is_numeric()) {
|
||||
#if defined(USE_SENSOR) || defined(USE_BINARY_SENSOR)
|
||||
const float value = capture.value_as_float_with_scaler_applied();
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
if (is_numeric) {
|
||||
for (auto &item : this->sensors_) {
|
||||
if (item.obis_code == obis_code) {
|
||||
item.sensor->publish_state(float_val);
|
||||
for (const auto &item : this->sensors_) {
|
||||
if (item.obis == capture.obis) {
|
||||
item.sensor->publish_state(value);
|
||||
updated_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (!is_numeric && str_val != nullptr) {
|
||||
for (auto &item : this->text_sensors_) {
|
||||
if (item.obis_code == obis_code) {
|
||||
item.sensor->publish_state(str_val);
|
||||
updated_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (is_numeric) {
|
||||
bool state = float_val != 0.0f;
|
||||
for (auto &item : this->binary_sensors_) {
|
||||
if (item.obis_code == obis_code) {
|
||||
item.sensor->publish_state(state);
|
||||
for (const auto &item : this->binary_sensors_) {
|
||||
if (item.obis == capture.obis) {
|
||||
item.sensor->publish_state(value != 0.0f);
|
||||
updated_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
for (const auto &item : this->text_sensors_) {
|
||||
if (item.obis == capture.obis) {
|
||||
std::array<char, 128> value_buf;
|
||||
const auto value = capture.value_as_string(value_buf);
|
||||
item.sensor->publish_state(value.data(), value.size());
|
||||
updated_count++;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (updated_count == 0) {
|
||||
ESP_LOGV(TAG, "Received OBIS %s, but no sensors are registered for it.", obis_code);
|
||||
std::array<char, 24> obis_buf;
|
||||
const auto obis = capture.obis.to_string(obis_buf);
|
||||
ESP_LOGV(TAG, "Received OBIS %.*s, but no sensors are registered for it.", static_cast<int>(obis.size()),
|
||||
obis.data());
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
void DlmsMeterComponent::register_sensor(const std::string &obis_code, sensor::Sensor *sensor) {
|
||||
this->sensors_.push_back({obis_code, sensor});
|
||||
void DlmsMeterComponent::register_sensor(const dlms_parser::ObisId &obis, sensor::Sensor *sensor) {
|
||||
this->sensors_.push_back({obis, sensor});
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void DlmsMeterComponent::register_text_sensor(const std::string &obis_code, text_sensor::TextSensor *sensor) {
|
||||
this->text_sensors_.push_back({obis_code, sensor});
|
||||
void DlmsMeterComponent::register_text_sensor(const dlms_parser::ObisId &obis, text_sensor::TextSensor *sensor) {
|
||||
this->text_sensors_.push_back({obis, sensor});
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void DlmsMeterComponent::register_binary_sensor(const std::string &obis_code, binary_sensor::BinarySensor *sensor) {
|
||||
this->binary_sensors_.push_back({obis_code, sensor});
|
||||
void DlmsMeterComponent::register_binary_sensor(const dlms_parser::ObisId &obis, binary_sensor::BinarySensor *sensor) {
|
||||
this->binary_sensors_.push_back({obis, sensor});
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::dlms_meter
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
// Ignore Zephyr. It doesn't have any encryption library.
|
||||
#if defined(USE_ESP32) || defined(USE_ARDUINO) || defined(USE_HOST)
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
@@ -19,12 +23,6 @@
|
||||
|
||||
#include <dlms_parser/dlms_parser.h>
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
|
||||
#if __has_include(<psa/crypto.h>)
|
||||
#include <dlms_parser/decryption/aes_128_gcm_decryptor_tfpsa.h>
|
||||
#elif !defined(USE_ESP8266) && __has_include(<mbedtls/gcm.h>)
|
||||
@@ -35,9 +33,12 @@
|
||||
#elif __has_include(<bearssl/bearssl.h>)
|
||||
#include <dlms_parser/decryption/aes_128_gcm_decryptor_bearssl.h>
|
||||
#else
|
||||
#define DLMS_METER_NO_CRYPTO
|
||||
#error "The platform doesn't provide a compatible encryption library for dlms_parser"
|
||||
#endif
|
||||
|
||||
namespace esphome::dlms_meter {
|
||||
|
||||
// Entity storage is sized by codegen; a platform with no entities emits no define
|
||||
#ifndef DLMS_MAX_SENSORS
|
||||
static constexpr uint8_t DLMS_MAX_SENSORS = 0;
|
||||
#endif
|
||||
@@ -48,82 +49,64 @@ static constexpr uint8_t DLMS_MAX_TEXT_SENSORS = 0;
|
||||
static constexpr uint8_t DLMS_MAX_BINARY_SENSORS = 0;
|
||||
#endif
|
||||
|
||||
namespace esphome::dlms_meter {
|
||||
|
||||
#ifdef DLMS_METER_NO_CRYPTO
|
||||
// Fallback dummy decryptor for platforms without supported crypto (e.g., Zephyr during clang-tidy)
|
||||
class Aes128GcmDecryptorDummy : public dlms_parser::Aes128GcmDecryptor {
|
||||
public:
|
||||
void set_decryption_key(const dlms_parser::Aes128GcmDecryptionKey &key) override {}
|
||||
bool decrypt_in_place(std::span<const uint8_t> iv, std::span<uint8_t> ciphertext_and_plaintext,
|
||||
std::span<const uint8_t> aad, std::span<const uint8_t> tag) override {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
||||
#if __has_include(<psa/crypto.h>)
|
||||
using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorTfPsa;
|
||||
#elif !defined(USE_ESP8266) && __has_include(<mbedtls/gcm.h>)
|
||||
using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorMbedTls;
|
||||
#elif __has_include(<bearssl/bearssl.h>)
|
||||
using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorBearSsl;
|
||||
#else
|
||||
using Aes128GcmDecryptorImpl = Aes128GcmDecryptorDummy;
|
||||
#endif
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
struct SensorItem {
|
||||
std::string obis_code;
|
||||
dlms_parser::ObisId obis;
|
||||
sensor::Sensor *sensor;
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
struct TextSensorItem {
|
||||
std::string obis_code;
|
||||
dlms_parser::ObisId obis;
|
||||
text_sensor::TextSensor *sensor;
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
struct BinarySensorItem {
|
||||
std::string obis_code;
|
||||
dlms_parser::ObisId obis;
|
||||
binary_sensor::BinarySensor *sensor;
|
||||
};
|
||||
#endif
|
||||
|
||||
struct CustomPattern {
|
||||
std::string pattern;
|
||||
std::optional<std::string> name;
|
||||
int priority{0};
|
||||
std::optional<std::array<uint8_t, 6>> default_obis;
|
||||
const char *pattern;
|
||||
const char *name;
|
||||
int priority;
|
||||
dlms_parser::ObisId default_obis;
|
||||
};
|
||||
|
||||
#if __has_include(<psa/crypto.h>)
|
||||
using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorTfPsa;
|
||||
#elif !defined(USE_ESP8266) && __has_include(<mbedtls/gcm.h>)
|
||||
using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorMbedTls;
|
||||
#else
|
||||
using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorBearSsl;
|
||||
#endif
|
||||
|
||||
class DlmsMeterComponent final : public Component, public uart::UARTDevice {
|
||||
public:
|
||||
DlmsMeterComponent(uint32_t receive_timeout_ms, bool skip_crc_check,
|
||||
std::optional<std::array<uint8_t, 16>> decryption_key,
|
||||
std::optional<std::array<uint8_t, 16>> authentication_key,
|
||||
std::vector<CustomPattern> custom_patterns);
|
||||
DlmsMeterComponent(uint32_t receive_timeout_ms, bool skip_crc_check, const char *decryption_key,
|
||||
const char *authentication_key, std::vector<CustomPattern> custom_patterns);
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
void register_sensor(const std::string &obis_code, sensor::Sensor *sensor);
|
||||
void register_sensor(const dlms_parser::ObisId &obis, sensor::Sensor *sensor);
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void register_text_sensor(const std::string &obis_code, text_sensor::TextSensor *sensor);
|
||||
void register_text_sensor(const dlms_parser::ObisId &obis, text_sensor::TextSensor *sensor);
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void register_binary_sensor(const std::string &obis_code, binary_sensor::BinarySensor *sensor);
|
||||
void register_binary_sensor(const dlms_parser::ObisId &obis, binary_sensor::BinarySensor *sensor);
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void read_rx_buffer_();
|
||||
void flush_rx_buffer_();
|
||||
void process_frame_();
|
||||
void on_data_(const char *obis_code, float float_val, const char *str_val, bool is_numeric);
|
||||
void on_data_(const dlms_parser::AxdrCapture &capture);
|
||||
|
||||
std::array<uint8_t, 2048> rx_buffer_;
|
||||
size_t bytes_accumulated_{0};
|
||||
@@ -149,3 +132,5 @@ class DlmsMeterComponent final : public Component, public uart::UARTDevice {
|
||||
};
|
||||
|
||||
} // namespace esphome::dlms_meter
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,159 +1,65 @@
|
||||
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__)
|
||||
from .. import (
|
||||
CONF_DLMS_METER_ID,
|
||||
CONF_OBIS_CODE,
|
||||
DlmsMeterComponent,
|
||||
obis_string_to_byte_list,
|
||||
register_sensor,
|
||||
)
|
||||
|
||||
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.Required(CONF_OBIS_CODE): obis_string_to_byte_list,
|
||||
**{
|
||||
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)
|
||||
register_sensor(hub, config[CONF_OBIS_CODE], var)
|
||||
|
||||
@@ -1,60 +1,43 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .. import (
|
||||
CONF_DLMS_METER_ID,
|
||||
CONF_OBIS_CODE,
|
||||
DlmsMeterComponent,
|
||||
obis_string_to_byte_list,
|
||||
register_text_sensor,
|
||||
)
|
||||
|
||||
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.Required(CONF_OBIS_CODE): obis_string_to_byte_list,
|
||||
**{
|
||||
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)
|
||||
register_text_sensor(hub, 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;
|
||||
|
||||
@@ -231,7 +231,7 @@ bool ES8388::set_dac_output(DacOutputLine line) {
|
||||
optional<DacOutputLine> ES8388::get_dac_power() {
|
||||
uint8_t dac_power;
|
||||
if (!this->read_byte(ES8388_DACPOWER, &dac_power)) {
|
||||
this->status_momentary_warning("dacpower_read");
|
||||
this->status_momentary_warning();
|
||||
return {};
|
||||
}
|
||||
switch (dac_power) {
|
||||
@@ -274,7 +274,7 @@ bool ES8388::set_adc_input_mic(AdcInputMicLine line) {
|
||||
optional<AdcInputMicLine> ES8388::get_mic_input() {
|
||||
uint8_t mic_input;
|
||||
if (!this->read_byte(ES8388_ADCCONTROL2, &mic_input)) {
|
||||
this->status_momentary_warning("adccontrol2_read");
|
||||
this->status_momentary_warning();
|
||||
return {};
|
||||
}
|
||||
switch (mic_input) {
|
||||
|
||||
@@ -15,7 +15,6 @@ from esphome.components.const import (
|
||||
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION,
|
||||
CONF_IGNORE_NOT_FOUND,
|
||||
)
|
||||
from esphome.config_helpers import filter_source_files_from_defines
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADVANCED,
|
||||
@@ -2926,6 +2925,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")
|
||||
@@ -3962,10 +3966,3 @@ def process_stacktrace(config, line, backtrace_state):
|
||||
_decode_pc(config, addr.group())
|
||||
|
||||
return backtrace_state
|
||||
|
||||
|
||||
# gpio.cpp only implements ESP32InternalGPIOPin and its ISR helpers, which
|
||||
# are instantiated solely by the pin schema codegen (esp32_pin_to_code)
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_defines(
|
||||
{"gpio.cpp": "USE_ESP32_INTERNAL_GPIO"}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -1,9 +1,10 @@
|
||||
#include "esphome/core/defines.h"
|
||||
// Also defines the core ISRInternalGPIOPin methods; those are only reachable
|
||||
// via ESP32InternalGPIOPin::to_isr(), so the same define gates both safely.
|
||||
#if defined(USE_ESP32) && defined(USE_ESP32_INTERNAL_GPIO)
|
||||
// Always built: InternalGPIOPin is this class, so components that merely hold a
|
||||
// pointer to it reference these methods even in configs without a pin.
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "gpio.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/rtc_io.h"
|
||||
@@ -49,17 +50,52 @@ struct ISRPinArg {
|
||||
gpio_num_t pin;
|
||||
gpio::Flags flags;
|
||||
bool inverted;
|
||||
#ifdef USE_GPIO_HOLD
|
||||
bool hold;
|
||||
#endif
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
bool use_rtc;
|
||||
int rtc_pin;
|
||||
#endif
|
||||
};
|
||||
|
||||
#ifdef USE_GPIO_HOLD
|
||||
// Re-latch a held pad onto the values just written to its registers.
|
||||
static inline void refresh_hold(const ESP32InternalGPIOPin &pin) {
|
||||
pin.set_hold(false);
|
||||
pin.set_hold(true);
|
||||
}
|
||||
|
||||
static inline void IRAM_ATTR isr_refresh_hold(const ISRPinArg *arg) {
|
||||
if (!arg->hold)
|
||||
return;
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
if (arg->use_rtc) {
|
||||
rtcio_hal_hold_disable(arg->rtc_pin);
|
||||
rtcio_hal_hold_enable(arg->rtc_pin);
|
||||
return;
|
||||
}
|
||||
#elif defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
// GPIO0-21 are held from the RTC domain; the digital hold mask is zero for them
|
||||
if (arg->pin < SOC_RTCIO_PIN_COUNT) {
|
||||
rtcio_hal_hold_disable(arg->pin);
|
||||
rtcio_hal_hold_enable(arg->pin);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
gpio_hal_hold_dis(&GPIO_HAL, arg->pin);
|
||||
gpio_hal_hold_en(&GPIO_HAL, arg->pin);
|
||||
}
|
||||
#endif
|
||||
|
||||
ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const {
|
||||
auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
arg->pin = this->get_pin_num();
|
||||
arg->flags = gpio::FLAG_NONE;
|
||||
arg->inverted = this->pin_flags_.inverted;
|
||||
#ifdef USE_GPIO_HOLD
|
||||
arg->hold = this->get_hold();
|
||||
#endif
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
arg->use_rtc = rtc_gpio_is_valid_gpio(this->get_pin_num());
|
||||
if (arg->use_rtc)
|
||||
@@ -68,7 +104,7 @@ ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const {
|
||||
return ISRInternalGPIOPin((void *) arg);
|
||||
}
|
||||
|
||||
void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||
void ESP32InternalGPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||
gpio_int_type_t idf_type = GPIO_INTR_ANYEDGE;
|
||||
switch (type) {
|
||||
case gpio::INTERRUPT_RISING_EDGE:
|
||||
@@ -104,7 +140,23 @@ size_t ESP32InternalGPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "GPIO%" PRIu32, static_cast<uint32_t>(this->pin_));
|
||||
}
|
||||
|
||||
#ifdef USE_GPIO_HOLD
|
||||
void ESP32InternalGPIOPin::set_hold(bool hold) const {
|
||||
if (!this->get_hold())
|
||||
return;
|
||||
if (hold) {
|
||||
gpio_hold_en(this->get_pin_num());
|
||||
} else {
|
||||
gpio_hold_dis(this->get_pin_num());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void ESP32InternalGPIOPin::setup() {
|
||||
#ifdef USE_GPIO_HOLD
|
||||
// Hold before gpio_config so the pad keeps its state while the registers change
|
||||
this->set_hold(true);
|
||||
#endif
|
||||
gpio_config_t conf{};
|
||||
conf.pin_bit_mask = 1ULL << static_cast<uint32_t>(this->pin_);
|
||||
conf.mode = flags_to_mode(this->flags_);
|
||||
@@ -115,6 +167,18 @@ void ESP32InternalGPIOPin::setup() {
|
||||
if (this->flags_ & gpio::FLAG_OUTPUT) {
|
||||
gpio_set_drive_capability(this->get_pin_num(), this->get_drive_strength());
|
||||
}
|
||||
#ifdef USE_GPIO_HOLD
|
||||
// gpio_hold_dis logs an error on input-only pads
|
||||
if (GPIO_IS_VALID_OUTPUT_GPIO(this->get_pin_num())) {
|
||||
if (!this->get_hold()) {
|
||||
// Release a hold left behind by an earlier boot
|
||||
gpio_hold_dis(this->get_pin_num());
|
||||
} else if (this->flags_ & gpio::FLAG_INPUT) {
|
||||
// Inputs apply the config now so reads work; outputs wait for the first write
|
||||
refresh_hold(*this);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) {
|
||||
@@ -129,6 +193,9 @@ void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) {
|
||||
pull_mode = GPIO_PULLDOWN_ONLY;
|
||||
}
|
||||
gpio_set_pull_mode(this->get_pin_num(), pull_mode);
|
||||
#ifdef USE_GPIO_HOLD
|
||||
refresh_hold(*this);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ESP32InternalGPIOPin::digital_read() {
|
||||
@@ -136,6 +203,9 @@ bool ESP32InternalGPIOPin::digital_read() {
|
||||
}
|
||||
void ESP32InternalGPIOPin::digital_write(bool value) {
|
||||
gpio_set_level(this->get_pin_num(), value != this->pin_flags_.inverted ? 1 : 0);
|
||||
#ifdef USE_GPIO_HOLD
|
||||
refresh_hold(*this);
|
||||
#endif
|
||||
}
|
||||
void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(this->get_pin_num()); }
|
||||
|
||||
@@ -143,6 +213,8 @@ void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(this->ge
|
||||
|
||||
using namespace esp32;
|
||||
|
||||
// NOLINTBEGIN(clang-analyzer-core.FixedAddressDereference) -- some gpio_hal functions use MMIO at fixed addresses
|
||||
// internally
|
||||
bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
|
||||
auto *arg = reinterpret_cast<ISRPinArg *>(this->arg_);
|
||||
return bool(gpio_hal_get_level(&GPIO_HAL, arg->pin)) != arg->inverted;
|
||||
@@ -151,6 +223,9 @@ bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
|
||||
void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) {
|
||||
auto *arg = reinterpret_cast<ISRPinArg *>(this->arg_);
|
||||
gpio_hal_set_level(&GPIO_HAL, arg->pin, value != arg->inverted);
|
||||
#ifdef USE_GPIO_HOLD
|
||||
isr_refresh_hold(arg);
|
||||
#endif
|
||||
}
|
||||
|
||||
void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() {
|
||||
@@ -202,9 +277,13 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
|
||||
gpio_hal_input_disable(&GPIO_HAL, arg->pin);
|
||||
}
|
||||
}
|
||||
#ifdef USE_GPIO_HOLD
|
||||
isr_refresh_hold(arg);
|
||||
#endif
|
||||
arg->flags = flags;
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-core.FixedAddressDereference)
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32 && USE_ESP32_INTERNAL_GPIO
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/gpio_pin.h"
|
||||
#include <esp_system.h>
|
||||
#include <driver/gpio.h>
|
||||
|
||||
namespace esphome::esp32 {
|
||||
@@ -10,7 +11,7 @@ namespace esphome::esp32 {
|
||||
static_assert(GPIO_NUM_MAX <= 256, "gpio_num_t has too many values for uint8_t");
|
||||
static_assert(GPIO_DRIVE_CAP_MAX <= 4, "gpio_drive_cap_t has too many values for 2-bit field");
|
||||
|
||||
class ESP32InternalGPIOPin final : public InternalGPIOPin {
|
||||
class ESP32InternalGPIOPin final : public GPIOPin {
|
||||
public:
|
||||
void set_pin(gpio_num_t pin) { this->pin_ = static_cast<uint8_t>(pin); }
|
||||
void set_inverted(bool inverted) { this->pin_flags_.inverted = inverted; }
|
||||
@@ -24,16 +25,30 @@ class ESP32InternalGPIOPin final : public InternalGPIOPin {
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
void detach_interrupt() const override;
|
||||
ISRInternalGPIOPin to_isr() const override;
|
||||
uint8_t get_pin() const override { return this->pin_; }
|
||||
void detach_interrupt() const;
|
||||
template<typename T> void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const {
|
||||
this->attach_interrupt_(reinterpret_cast<void (*)(void *)>(func), arg, type);
|
||||
}
|
||||
ISRInternalGPIOPin to_isr() const;
|
||||
uint8_t get_pin() const { return this->pin_; }
|
||||
gpio::Flags get_flags() const override { return this->flags_; }
|
||||
bool is_inverted() const override { return this->pin_flags_.inverted; }
|
||||
bool is_inverted() const { return this->pin_flags_.inverted; }
|
||||
bool is_internal() override { return true; }
|
||||
gpio_num_t get_pin_num() const { return static_cast<gpio_num_t>(this->pin_); }
|
||||
gpio_drive_cap_t get_drive_strength() const { return static_cast<gpio_drive_cap_t>(this->pin_flags_.drive_strength); }
|
||||
#ifdef USE_GPIO_HOLD
|
||||
/// Apply or release the pad hold. Does nothing unless the pin is configured with hold_state.
|
||||
void set_hold(bool hold) const;
|
||||
bool is_held() const override {
|
||||
if (!this->get_hold())
|
||||
return false;
|
||||
esp_reset_reason_t reason = esp_reset_reason();
|
||||
return reason == ESP_RST_DEEPSLEEP || reason == ESP_RST_SW;
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
||||
void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const;
|
||||
|
||||
// Memory layout: 8 bytes total on 32-bit systems
|
||||
// - 3 bytes for members below
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Any
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_HOLD_STATE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
@@ -248,6 +249,7 @@ ESP32_PIN_SCHEMA = cv.All(
|
||||
cv.float_with_unit("current", "mA", optional_unit=True),
|
||||
cv.enum(DRIVE_STRENGTHS),
|
||||
),
|
||||
cv.Optional(CONF_HOLD_STATE, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
validate_gpio_pin,
|
||||
@@ -257,7 +259,6 @@ ESP32_PIN_SCHEMA = cv.All(
|
||||
|
||||
@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP32, ESP32_PIN_SCHEMA)
|
||||
async def esp32_pin_to_code(config):
|
||||
cg.add_define("USE_ESP32_INTERNAL_GPIO")
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
num = config[CONF_NUMBER]
|
||||
cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}")))
|
||||
@@ -267,7 +268,11 @@ async def esp32_pin_to_code(config):
|
||||
cg.add(var.set_inverted(True))
|
||||
if CONF_DRIVE_STRENGTH in config:
|
||||
cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH]))
|
||||
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
|
||||
flags = pins.gpio_flags_expr(config[CONF_MODE])
|
||||
if config[CONF_HOLD_STATE]:
|
||||
flags = flags | cg.gpio_Flags.FLAG_HOLD
|
||||
cg.add_define("USE_GPIO_HOLD")
|
||||
cg.add(var.set_flags(flags))
|
||||
return var
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Pin checks shared by the ESP32 variant validators."""
|
||||
|
||||
from logging import Logger
|
||||
from typing import Any
|
||||
|
||||
from esphome.components.const import CONF_HOLD_STATE
|
||||
from esphome.const import CONF_NUMBER
|
||||
|
||||
|
||||
def check_usb_jtag_pin(num: int, usb_jtag_pins: set[int], logger: Logger) -> None:
|
||||
if num in usb_jtag_pins:
|
||||
logger.warning(
|
||||
"GPIO%d is used by the USB-Serial-JTAG interface."
|
||||
" Using this pin as GPIO will conflict with USB-Serial-JTAG.",
|
||||
num,
|
||||
)
|
||||
|
||||
|
||||
def check_usb_jtag_hold(
|
||||
conf: dict[str, Any], usb_jtag_pins: set[int], logger: Logger
|
||||
) -> None:
|
||||
num = conf[CONF_NUMBER]
|
||||
if conf.get(CONF_HOLD_STATE) and num in usb_jtag_pins:
|
||||
logger.warning(
|
||||
"GPIO%d cannot hold at low level during wakeup from deep sleep.", num
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome.components.const import CONF_HOLD_STATE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_INPUT,
|
||||
@@ -53,6 +54,7 @@ def esp32_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
is_output = mode[CONF_OUTPUT]
|
||||
is_pullup = mode[CONF_PULLUP]
|
||||
is_pulldown = mode[CONF_PULLDOWN]
|
||||
is_hold = value.get(CONF_HOLD_STATE)
|
||||
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
@@ -70,6 +72,14 @@ def esp32_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{num} (34-39) does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN]
|
||||
)
|
||||
if is_hold and 34 <= num <= 39:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{num} (34-39) is input-only and cannot be held.",
|
||||
[CONF_HOLD_STATE],
|
||||
)
|
||||
if is_hold and num == 20:
|
||||
# Not in the digital hold mask and not an RTC pad, so the driver cannot hold it
|
||||
raise cv.Invalid("GPIO20 has no hold function.", [CONF_HOLD_STATE])
|
||||
|
||||
check_strapping_pin(value, _ESP32_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
@@ -5,6 +5,8 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
_ESP32C3_SPI_PSRAM_PINS = {
|
||||
12: "SPIHD",
|
||||
13: "SPIWP",
|
||||
@@ -28,12 +30,7 @@ def esp32_c3_validate_gpio_pin(value: int) -> int:
|
||||
raise cv.Invalid(
|
||||
f"This pin cannot be used on ESP32-C3s and is already used by the SPI/PSRAM interface (function: {_ESP32C3_SPI_PSRAM_PINS[value]})"
|
||||
)
|
||||
if value in _ESP32C3_USB_JTAG_PINS:
|
||||
_LOGGER.warning(
|
||||
"GPIO%d is used by the USB-Serial-JTAG interface."
|
||||
" Using this pin as GPIO will conflict with USB-Serial-JTAG.",
|
||||
value,
|
||||
)
|
||||
check_usb_jtag_pin(value, _ESP32C3_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
return value
|
||||
|
||||
@@ -49,6 +46,7 @@ def esp32_c3_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
check_usb_jtag_hold(value, _ESP32C3_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32C3_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
@@ -5,6 +5,8 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/esp_hal_i2c/esp32c5/include/hal/i2c_ll.h
|
||||
_ESP32C5_I2C_LP_PINS = {"SDA": 2, "SCL": 3}
|
||||
|
||||
@@ -20,6 +22,8 @@ _ESP32C5_SPI_PSRAM_PINS = {
|
||||
|
||||
_ESP32C5_STRAPPING_PINS = {2, 7, 27, 28}
|
||||
|
||||
_ESP32C5_USB_JTAG_PINS = {13, 14}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -31,6 +35,7 @@ def esp32_c5_validate_gpio_pin(value: int) -> int:
|
||||
f"This pin cannot be used on ESP32-C5s and is already used by the SPI/PSRAM interface (function: {_ESP32C5_SPI_PSRAM_PINS[value]})"
|
||||
)
|
||||
|
||||
check_usb_jtag_pin(value, _ESP32C5_USB_JTAG_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
@@ -45,6 +50,8 @@ def esp32_c5_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
|
||||
check_usb_jtag_hold(value, _ESP32C5_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32C5_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/esp_hal_i2c/esp32c6/include/hal/i2c_ll.h
|
||||
_ESP32C6_I2C_LP_PINS = {"SDA": 6, "SCL": 7}
|
||||
|
||||
@@ -32,12 +34,7 @@ def esp32_c6_validate_gpio_pin(value: int) -> int:
|
||||
raise cv.Invalid(
|
||||
f"This pin cannot be used on ESP32-C6s and is already used by the SPI/PSRAM interface (function: {_ESP32C6_SPI_PSRAM_PINS[value]})"
|
||||
)
|
||||
if value in _ESP32C6_USB_JTAG_PINS:
|
||||
_LOGGER.warning(
|
||||
"GPIO%d is used by the USB-Serial-JTAG interface."
|
||||
" Using this pin as GPIO will conflict with USB-Serial-JTAG.",
|
||||
value,
|
||||
)
|
||||
check_usb_jtag_pin(value, _ESP32C6_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
return value
|
||||
|
||||
@@ -52,6 +49,7 @@ def esp32_c6_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
check_usb_jtag_hold(value, _ESP32C6_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32C6_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
@@ -5,6 +5,8 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
# GPIO14-17, GPIO19-21 are used for SPI flash/PSRAM
|
||||
_ESP32C61_SPI_PSRAM_PINS = {
|
||||
14: "SPICS0",
|
||||
@@ -18,6 +20,8 @@ _ESP32C61_SPI_PSRAM_PINS = {
|
||||
|
||||
_ESP32C61_STRAPPING_PINS = {8, 9}
|
||||
|
||||
_ESP32C61_USB_JTAG_PINS = {12, 13}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -29,6 +33,7 @@ def esp32_c61_validate_gpio_pin(value: int) -> int:
|
||||
f"This pin cannot be used on ESP32-C61s and is already used by the SPI/PSRAM interface (function: {_ESP32C61_SPI_PSRAM_PINS[value]})"
|
||||
)
|
||||
|
||||
check_usb_jtag_pin(value, _ESP32C61_USB_JTAG_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
@@ -43,5 +48,7 @@ def esp32_c61_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
# All ESP32-C61 pins support input mode
|
||||
pass
|
||||
|
||||
check_usb_jtag_hold(value, _ESP32C61_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32C61_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
@@ -5,6 +5,8 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
_ESP32H2_SPI_FLASH_PINS = {6, 7, 15, 16, 17, 18, 19, 20, 21}
|
||||
|
||||
_ESP32H2_USB_JTAG_PINS = {26, 27}
|
||||
@@ -24,12 +26,7 @@ def esp32_h2_validate_gpio_pin(value: int) -> int:
|
||||
"See https://docs.espressif.com/projects/esp-idf/en/latest/esp32h2/api-reference/peripherals/gpio.html",
|
||||
value,
|
||||
)
|
||||
if value in _ESP32H2_USB_JTAG_PINS:
|
||||
_LOGGER.warning(
|
||||
"GPIO%d is used by the USB-Serial-JTAG interface."
|
||||
" Using this pin as GPIO will conflict with USB-Serial-JTAG.",
|
||||
value,
|
||||
)
|
||||
check_usb_jtag_pin(value, _ESP32H2_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
return value
|
||||
|
||||
@@ -44,5 +41,6 @@ def esp32_h2_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
check_usb_jtag_hold(value, _ESP32H2_USB_JTAG_PINS, _LOGGER)
|
||||
check_strapping_pin(value, _ESP32H2_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
@@ -5,17 +5,22 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
# Partial set from the ESP-IDF / esptool boot-mode docs:
|
||||
# https://docs.espressif.com/projects/esptool/en/latest/esp32h21/advanced-topics/boot-mode-selection.html
|
||||
# The full list awaits the ESP32-H21 datasheet's "Strapping Pins" section.
|
||||
_ESP32H21_STRAPPING_PINS: set[int] = {13, 14}
|
||||
|
||||
_ESP32H21_USB_JTAG_PINS = {17, 18}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def esp32_h21_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 25:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-25)")
|
||||
check_usb_jtag_pin(value, _ESP32H21_USB_JTAG_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
@@ -30,5 +35,7 @@ def esp32_h21_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
|
||||
check_usb_jtag_hold(value, _ESP32H21_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32H21_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
@@ -5,17 +5,22 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
# Partial set from the ESP-IDF / esptool boot-mode docs:
|
||||
# https://docs.espressif.com/projects/esptool/en/latest/esp32h4/advanced-topics/boot-mode-selection.html
|
||||
# The full list awaits the ESP32-H4 datasheet's "Strapping Pins" section.
|
||||
_ESP32H4_STRAPPING_PINS: set[int] = {13, 14}
|
||||
|
||||
_ESP32H4_USB_JTAG_PINS = {13, 14}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def esp32_h4_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 39:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)")
|
||||
check_usb_jtag_pin(value, _ESP32H4_USB_JTAG_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
@@ -30,5 +35,7 @@ def esp32_h4_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
|
||||
check_usb_jtag_hold(value, _ESP32H4_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32H4_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
@@ -5,6 +5,8 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
# https://documentation.espressif.com/esp32-p4-chip-revision-v1.3_datasheet_en.pdf
|
||||
_ESP32P4_LP_PINS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
|
||||
|
||||
@@ -18,12 +20,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def esp32_p4_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 54:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)")
|
||||
if value in _ESP32P4_USB_JTAG_PINS:
|
||||
_LOGGER.warning(
|
||||
"GPIO%d is used by the USB-Serial-JTAG interface."
|
||||
" Using this pin as GPIO will conflict with USB-Serial-JTAG.",
|
||||
value,
|
||||
)
|
||||
check_usb_jtag_pin(value, _ESP32P4_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
return value
|
||||
|
||||
@@ -38,6 +35,7 @@ def esp32_p4_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
check_usb_jtag_hold(value, _ESP32P4_USB_JTAG_PINS, _LOGGER)
|
||||
check_strapping_pin(value, _ESP32P4_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome.components.const import CONF_HOLD_STATE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_INPUT,
|
||||
@@ -70,6 +71,11 @@ def esp32_s2_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{num} does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN]
|
||||
)
|
||||
if value.get(CONF_HOLD_STATE) and num == 46:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{num} is input-only and cannot be held.",
|
||||
[CONF_HOLD_STATE],
|
||||
)
|
||||
|
||||
check_strapping_pin(value, _ESP32S2_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
@@ -12,6 +12,8 @@ from esphome.const import (
|
||||
from esphome.pins import PIN_SCHEMA_REGISTRY, check_strapping_pin
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
_ESP32S3_SPI_PSRAM_PINS = {
|
||||
26: "SPICS1",
|
||||
27: "SPIHD",
|
||||
@@ -53,12 +55,7 @@ def esp32_s3_validate_gpio_pin(value: int) -> int:
|
||||
# These pins are not exposed in GPIO mux (reason unknown)
|
||||
# but they're missing from IO_MUX list in datasheet
|
||||
raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32-S3s.")
|
||||
if value in _ESP32S3_USB_JTAG_PINS:
|
||||
_LOGGER.warning(
|
||||
"GPIO%d is used by the USB-Serial-JTAG interface."
|
||||
" Using this pin as GPIO will conflict with USB-Serial-JTAG.",
|
||||
value,
|
||||
)
|
||||
check_usb_jtag_pin(value, _ESP32S3_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
return value
|
||||
|
||||
@@ -73,6 +70,7 @@ def esp32_s3_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
check_usb_jtag_hold(value, _ESP32S3_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32S3_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
@@ -5,6 +5,8 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
# Per the ESP32-S31 IDF DOCS and datasheet:
|
||||
# https://docs.espressif.com/projects/esp-idf/en/v6.1/esp32s31/api-reference/peripherals/gpio.html
|
||||
# https://documentation.espressif.com/esp32-s31_datasheet_en.pdf
|
||||
@@ -16,6 +18,8 @@ _ESP32S31_STRAPPING_PINS: set[int] = {36, 37, 60, 61}
|
||||
# LP I2C is fixed to GPIO6 (SCL) / GPIO7 (SDA) per the datasheet IO MUX table.
|
||||
_ESP32S31_I2C_LP_PINS = {"SDA": 7, "SCL": 6}
|
||||
|
||||
_ESP32S31_USB_JTAG_PINS = {33, 34}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -28,6 +32,7 @@ def esp32_s31_validate_gpio_pin(value: int) -> int:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{value} is reserved for the SPI flash interface on ESP32-S31 and cannot be used."
|
||||
)
|
||||
check_usb_jtag_pin(value, _ESP32S31_USB_JTAG_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
@@ -46,6 +51,8 @@ def esp32_s31_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
|
||||
check_usb_jtag_hold(value, _ESP32S31_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32S31_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
#include "gpio.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::esp8266 {
|
||||
@@ -74,7 +75,7 @@ ISRInternalGPIOPin ESP8266GPIOPin::to_isr() const {
|
||||
return ISRInternalGPIOPin((void *) arg);
|
||||
}
|
||||
|
||||
void ESP8266GPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||
void ESP8266GPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||
uint8_t arduino_mode = 0;
|
||||
switch (type) {
|
||||
case gpio::INTERRUPT_RISING_EDGE:
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/gpio_pin.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace esphome::esp8266 {
|
||||
|
||||
class ESP8266GPIOPin final : public InternalGPIOPin {
|
||||
class ESP8266GPIOPin final : public GPIOPin {
|
||||
public:
|
||||
void set_pin(uint8_t pin) { pin_ = pin; }
|
||||
void set_inverted(bool inverted) { inverted_ = inverted; }
|
||||
@@ -18,14 +18,18 @@ class ESP8266GPIOPin final : public InternalGPIOPin {
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
void detach_interrupt() const override;
|
||||
ISRInternalGPIOPin to_isr() const override;
|
||||
uint8_t get_pin() const override { return pin_; }
|
||||
void detach_interrupt() const;
|
||||
template<typename T> void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const {
|
||||
this->attach_interrupt_(reinterpret_cast<void (*)(void *)>(func), arg, type);
|
||||
}
|
||||
ISRInternalGPIOPin to_isr() const;
|
||||
uint8_t get_pin() const { return pin_; }
|
||||
gpio::Flags get_flags() const override { return flags_; }
|
||||
bool is_inverted() const override { return inverted_; }
|
||||
bool is_inverted() const { return inverted_; }
|
||||
bool is_internal() override { return true; }
|
||||
|
||||
protected:
|
||||
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
||||
void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const;
|
||||
|
||||
uint8_t pin_;
|
||||
bool inverted_{};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -635,7 +635,7 @@ error:
|
||||
|
||||
this->cleanup_connection_();
|
||||
|
||||
this->status_momentary_error("err", 5000);
|
||||
this->status_momentary_error(5000);
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
this->notify_state_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
|
||||
#endif
|
||||
|
||||
@@ -406,7 +406,7 @@ esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payl
|
||||
if (packet == nullptr) {
|
||||
this->send_packet_queue_.increment_dropped_count();
|
||||
ESP_LOGE(TAG, "Failed to allocate send packet from pool");
|
||||
this->status_momentary_warning("send-packet-pool-full");
|
||||
this->status_momentary_warning();
|
||||
return ESP_ERR_ESPNOW_NO_MEM;
|
||||
}
|
||||
// Load the packet data
|
||||
@@ -434,7 +434,7 @@ void ESPNowComponent::send_() {
|
||||
if (packet->callback_ != nullptr) {
|
||||
packet->callback_(err);
|
||||
}
|
||||
this->status_momentary_warning("send-failed");
|
||||
this->status_momentary_warning();
|
||||
this->send_packet_pool_.release(packet);
|
||||
this->current_send_packet_ = nullptr; // Reset current packet
|
||||
return;
|
||||
@@ -447,7 +447,7 @@ esp_err_t ESPNowComponent::add_peer(const uint8_t *peer) {
|
||||
}
|
||||
|
||||
if (memcmp(peer, this->own_address_, ESP_NOW_ETH_ALEN) == 0) {
|
||||
this->status_momentary_warning("peer-add-failed");
|
||||
this->status_momentary_warning();
|
||||
return ESP_ERR_INVALID_MAC;
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@ esp_err_t ESPNowComponent::add_peer(const uint8_t *peer) {
|
||||
char peer_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
format_mac_addr_upper(peer, peer_buf);
|
||||
ESP_LOGE(TAG, "Failed to add peer %s - %s", peer_buf, LOG_STR_ARG(espnow_error_to_str(err)));
|
||||
this->status_momentary_warning("peer-add-failed");
|
||||
this->status_momentary_warning();
|
||||
return err;
|
||||
}
|
||||
}
|
||||
@@ -492,7 +492,7 @@ esp_err_t ESPNowComponent::del_peer(const uint8_t *peer) {
|
||||
char peer_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
format_mac_addr_upper(peer, peer_buf);
|
||||
ESP_LOGE(TAG, "Failed to delete peer %s - %s", peer_buf, LOG_STR_ARG(espnow_error_to_str(err)));
|
||||
this->status_momentary_warning("peer-del-failed");
|
||||
this->status_momentary_warning();
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import output
|
||||
from esphome.components.const import CONF_HOLD_STATE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_PIN
|
||||
from esphome.const import CONF_ID, CONF_PIN, CONF_POWER_SUPPLY
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import gpio_ns
|
||||
@@ -17,6 +19,27 @@ CONFIG_SCHEMA = output.BINARY_OUTPUT_SCHEMA.extend(
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
def _require_hold(ps_config: ConfigType) -> ConfigType:
|
||||
if not ps_config[CONF_PIN].get(CONF_HOLD_STATE):
|
||||
raise cv.Invalid(
|
||||
f"The power supply pin must also set {CONF_HOLD_STATE} "
|
||||
f"when an output it powers sets it",
|
||||
[CONF_PIN],
|
||||
)
|
||||
return ps_config
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
if not config[CONF_PIN].get(CONF_HOLD_STATE):
|
||||
return
|
||||
if (ps_id := config.get(CONF_POWER_SUPPLY)) is None:
|
||||
return
|
||||
fv.id_declaration_match_schema(_require_hold)(ps_id)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await output.register_output(var, config)
|
||||
|
||||
@@ -11,9 +11,12 @@ class GPIOBinaryOutput final : public output::BinaryOutput, public Component {
|
||||
void set_pin(GPIOPin *pin) { pin_ = pin; }
|
||||
|
||||
void setup() override {
|
||||
this->turn_off();
|
||||
const bool held = this->pin_->is_held();
|
||||
if (!held)
|
||||
this->turn_off();
|
||||
this->pin_->setup();
|
||||
this->turn_off();
|
||||
if (!held)
|
||||
this->turn_off();
|
||||
}
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
@@ -12,8 +12,9 @@ float GPIOSwitch::get_setup_priority() const { return setup_priority::HARDWARE;
|
||||
void GPIOSwitch::setup() {
|
||||
bool initial_state = this->get_initial_state_with_restore_mode().value_or(false);
|
||||
|
||||
// write state before setup
|
||||
this->control(initial_state);
|
||||
// write state before setup unless the pin kept it across the reset
|
||||
if (!this->pin_->is_held())
|
||||
this->control(initial_state);
|
||||
this->pin_->setup();
|
||||
// write after setup again for other IOs
|
||||
this->control(initial_state);
|
||||
|
||||
@@ -13,7 +13,6 @@ from esphome.const import (
|
||||
CONF_FONT,
|
||||
CONF_FOREGROUND_COLOR,
|
||||
CONF_ID,
|
||||
CONF_TRIGGER_ID,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -29,9 +28,6 @@ MenuItemValueArguments = graphical_display_menu_ns.struct("MenuItemValueArgument
|
||||
MenuItemValueArgumentsConstPtr = MenuItemValueArguments.operator("ptr").operator(
|
||||
"const"
|
||||
)
|
||||
GraphicalDisplayMenuOnRedrawTrigger = graphical_display_menu_ns.class_(
|
||||
"GraphicalDisplayMenuOnRedrawTrigger", automation.Trigger
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@MrMDavidson"]
|
||||
DOMAIN = "graphical_display_menu"
|
||||
@@ -49,13 +45,7 @@ CONFIG_SCHEMA = DISPLAY_MENU_BASE_SCHEMA.extend(
|
||||
cv.Optional(CONF_MENU_ITEM_VALUE): cv.templatable(cv.string),
|
||||
cv.Optional(CONF_FOREGROUND_COLOR): cv.use_id(color.ColorStruct),
|
||||
cv.Optional(CONF_BACKGROUND_COLOR): cv.use_id(color.ColorStruct),
|
||||
cv.Optional(CONF_ON_REDRAW): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
GraphicalDisplayMenuOnRedrawTrigger
|
||||
)
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_REDRAW): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -92,9 +82,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_background_color(background_color))
|
||||
|
||||
for conf in config.get(CONF_ON_REDRAW, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger, [(GraphicalDisplayMenuConstPtr, "it")], conf
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_redraw_callback", [(GraphicalDisplayMenuConstPtr, "it")], conf
|
||||
)
|
||||
|
||||
await display_menu_to_code(var, config)
|
||||
|
||||
@@ -241,6 +241,6 @@ void GraphicalDisplayMenu::draw_item(const display_menu_base::MenuItem *item, co
|
||||
"draw_item should be called.");
|
||||
}
|
||||
|
||||
void GraphicalDisplayMenu::update() { this->on_redraw_callbacks_.call(); }
|
||||
void GraphicalDisplayMenu::update() { this->on_redraw_callbacks_.call(this); }
|
||||
|
||||
} // namespace esphome::graphical_display_menu
|
||||
|
||||
@@ -70,17 +70,7 @@ class GraphicalDisplayMenu final : public display_menu_base::DisplayMenuComponen
|
||||
Color foreground_color_{COLOR_ON};
|
||||
Color background_color_{COLOR_OFF};
|
||||
|
||||
CallbackManager<void()> on_redraw_callbacks_{};
|
||||
};
|
||||
|
||||
class GraphicalDisplayMenuOnRedrawTrigger final : public Trigger<const GraphicalDisplayMenu *> {
|
||||
public:
|
||||
explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) : parent_(parent) {
|
||||
parent->add_on_redraw_callback([this]() { this->trigger(this->parent_); });
|
||||
}
|
||||
|
||||
protected:
|
||||
GraphicalDisplayMenu *parent_;
|
||||
LazyCallbackManager<void(const GraphicalDisplayMenu *)> on_redraw_callbacks_{};
|
||||
};
|
||||
|
||||
} // namespace graphical_display_menu
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -187,15 +187,18 @@ void HeatpumpIRClimate::transmit_state() {
|
||||
swing_h_cmd = HDIR_SWING;
|
||||
}
|
||||
|
||||
// MitsubishiMSCHeatpumpIR only knows FAN_1..FAN_3 and sends anything else as auto,
|
||||
// so low/medium/high have to start at FAN_1 there instead of FAN_2.
|
||||
const bool three_speed_fan = this->protocol_ == PROTOCOL_MITSUBISHI_MSC;
|
||||
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
|
||||
case climate::CLIMATE_FAN_LOW:
|
||||
fan_speed_cmd = FAN_2;
|
||||
fan_speed_cmd = three_speed_fan ? FAN_1 : FAN_2;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_MEDIUM:
|
||||
fan_speed_cmd = FAN_3;
|
||||
fan_speed_cmd = three_speed_fan ? FAN_2 : FAN_3;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
fan_speed_cmd = FAN_4;
|
||||
fan_speed_cmd = three_speed_fan ? FAN_3 : FAN_4;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_AUTO:
|
||||
default:
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifdef USE_HOST
|
||||
|
||||
#include "gpio.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -20,7 +21,7 @@ ISRInternalGPIOPin HostGPIOPin::to_isr() const {
|
||||
return ISRInternalGPIOPin((void *) arg);
|
||||
}
|
||||
|
||||
void HostGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||
void HostGPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||
ESP_LOGD(TAG, "Attaching interrupt %p to pin %d and mode %d", func, pin_, (uint32_t) type);
|
||||
}
|
||||
void HostGPIOPin::pin_mode(gpio::Flags flags) { ESP_LOGD(TAG, "Setting pin %d mode to %02X", pin_, (uint32_t) flags); }
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
#ifdef USE_HOST
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/gpio_pin.h"
|
||||
|
||||
namespace esphome::host {
|
||||
|
||||
class HostGPIOPin final : public InternalGPIOPin {
|
||||
class HostGPIOPin final : public GPIOPin {
|
||||
public:
|
||||
void set_pin(uint8_t pin) { pin_ = pin; }
|
||||
void set_inverted(bool inverted) { inverted_ = inverted; }
|
||||
@@ -17,14 +17,18 @@ class HostGPIOPin final : public InternalGPIOPin {
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
void detach_interrupt() const override;
|
||||
ISRInternalGPIOPin to_isr() const override;
|
||||
uint8_t get_pin() const override { return pin_; }
|
||||
void detach_interrupt() const;
|
||||
template<typename T> void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const {
|
||||
this->attach_interrupt_(reinterpret_cast<void (*)(void *)>(func), arg, type);
|
||||
}
|
||||
ISRInternalGPIOPin to_isr() const;
|
||||
uint8_t get_pin() const { return pin_; }
|
||||
gpio::Flags get_flags() const override { return flags_; }
|
||||
bool is_inverted() const override { return inverted_; }
|
||||
bool is_inverted() const { return inverted_; }
|
||||
bool is_internal() override { return true; }
|
||||
|
||||
protected:
|
||||
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
||||
void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const;
|
||||
|
||||
uint8_t pin_;
|
||||
bool inverted_{};
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const char *url, cons
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers) {
|
||||
if (!network::is_connected()) {
|
||||
this->status_momentary_error("failed", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
|
||||
return nullptr;
|
||||
}
|
||||
@@ -85,7 +85,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const char *url, cons
|
||||
if (!status) {
|
||||
ESP_LOGW(TAG, "HTTP Request failed; URL: %s", url);
|
||||
container->end();
|
||||
this->status_momentary_error("failed", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -143,13 +143,13 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const char *url, cons
|
||||
ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url,
|
||||
HTTPClient::errorToString(container->status_code).c_str());
|
||||
|
||||
this->status_momentary_error("failed", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
container->end();
|
||||
return nullptr;
|
||||
}
|
||||
if (!is_success(container->status_code)) {
|
||||
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, container->status_code);
|
||||
this->status_momentary_error("failed", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
// Still return the container, so it can be used to get the status code and error message
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const c
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers) {
|
||||
if (!network::is_connected()) {
|
||||
this->status_momentary_error("failed", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
|
||||
return nullptr;
|
||||
}
|
||||
@@ -101,7 +101,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const c
|
||||
if (!result) {
|
||||
ESP_LOGW(TAG, "HTTP Request failed; URL: %s, error code: %u", url, (unsigned) result.error());
|
||||
container->end();
|
||||
this->status_momentary_error("failed", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
return nullptr;
|
||||
}
|
||||
App.feed_wdt();
|
||||
@@ -109,7 +109,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const c
|
||||
container->status_code = response.status;
|
||||
if (!is_success(response.status)) {
|
||||
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, response.status);
|
||||
this->status_momentary_error("failed", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
// Still return the container, so it can be used to get the status code and error message
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers) {
|
||||
if (!network::is_connected()) {
|
||||
this->status_momentary_error("failed", ERROR_DURATION_MS);
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
ESP_LOGE(TAG, "HTTP Request failed; Not connected to network");
|
||||
return nullptr;
|
||||
}
|
||||
@@ -71,7 +71,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
|
||||
} else if (strcmp(method, "PATCH") == 0) {
|
||||
method_idf = HTTP_METHOD_PATCH;
|
||||
} else {
|
||||
this->status_momentary_error("failed", ERROR_DURATION_MS);
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
ESP_LOGE(TAG, "HTTP Request failed; Unsupported method");
|
||||
return nullptr;
|
||||
}
|
||||
@@ -110,7 +110,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
|
||||
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
if (client == nullptr) {
|
||||
this->status_momentary_error("failed", ERROR_DURATION_MS);
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
ESP_LOGE(TAG, "HTTP Request failed; client could not be initialized");
|
||||
return nullptr;
|
||||
}
|
||||
@@ -131,7 +131,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
|
||||
|
||||
esp_err_t err = esp_http_client_open(client, body_len);
|
||||
if (err != ESP_OK) {
|
||||
this->status_momentary_error("failed", ERROR_DURATION_MS);
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
|
||||
esp_http_client_cleanup(client);
|
||||
return nullptr;
|
||||
@@ -154,7 +154,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
|
||||
}
|
||||
|
||||
if (err != ESP_OK) {
|
||||
this->status_momentary_error("failed", ERROR_DURATION_MS);
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
|
||||
esp_http_client_cleanup(client);
|
||||
return nullptr;
|
||||
@@ -179,7 +179,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
|
||||
err = esp_http_client_set_redirection(client);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_http_client_set_redirection failed: %s", esp_err_to_name(err));
|
||||
this->status_momentary_error("failed", ERROR_DURATION_MS);
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
esp_http_client_cleanup(client);
|
||||
return nullptr;
|
||||
}
|
||||
@@ -192,7 +192,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
|
||||
err = esp_http_client_open(client, 0);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_http_client_open failed: %s", esp_err_to_name(err));
|
||||
this->status_momentary_error("failed", ERROR_DURATION_MS);
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
esp_http_client_cleanup(client);
|
||||
return nullptr;
|
||||
}
|
||||
@@ -220,7 +220,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, container->status_code);
|
||||
this->status_momentary_error("failed", ERROR_DURATION_MS);
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
return container;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -397,7 +397,7 @@ void I2SAudioMicrophone::loop() {
|
||||
|
||||
if (!this->start_driver_()) {
|
||||
ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second");
|
||||
this->status_momentary_error("driver_fail", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
this->stop_driver_(); // Stop/frees whatever possibly started
|
||||
break;
|
||||
}
|
||||
@@ -408,7 +408,7 @@ void I2SAudioMicrophone::loop() {
|
||||
|
||||
if (this->task_handle_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Task failed to start, retrying in 1 second");
|
||||
this->status_momentary_error("task_fail", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
this->stop_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ void I2SAudioSpeakerBase::loop() {
|
||||
|
||||
if (this->start_i2s_driver(this->audio_stream_info_) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second");
|
||||
this->status_momentary_error("driver-failure", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ void I2SAudioSpeakerBase::loop() {
|
||||
|
||||
if (this->speaker_task_handle_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Task failed to start, retrying in 1 second");
|
||||
this->status_momentary_error("task-failure", 1000);
|
||||
this->status_momentary_error(1000);
|
||||
this->stop_i2s_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -2,13 +2,7 @@ from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor, improv_base, output
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_ON_START,
|
||||
CONF_ON_STATE,
|
||||
CONF_TRIGGER_ID,
|
||||
PLATFORM_ESP32,
|
||||
)
|
||||
from esphome.const import CONF_ID, CONF_ON_START, CONF_ON_STATE, PLATFORM_ESP32
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -59,20 +53,35 @@ State = improv_ns.enum("State")
|
||||
|
||||
improv_ble_ns = cg.esphome_ns.namespace("improv_ble")
|
||||
ImprovBLEComponent = improv_ble_ns.class_("ImprovBLEComponent", cg.Component)
|
||||
ImprovBLEProvisionedTrigger = improv_ble_ns.class_(
|
||||
"ImprovBLEProvisionedTrigger", automation.Trigger.template()
|
||||
)
|
||||
ImprovBLEProvisioningTrigger = improv_ble_ns.class_(
|
||||
"ImprovBLEProvisioningTrigger", automation.Trigger.template()
|
||||
)
|
||||
ImprovBLEStartTrigger = improv_ble_ns.class_(
|
||||
"ImprovBLEStartTrigger", automation.Trigger.template()
|
||||
)
|
||||
ImprovBLEStateTrigger = improv_ble_ns.class_(
|
||||
"ImprovBLEStateTrigger", automation.Trigger.template()
|
||||
)
|
||||
ImprovBLEStoppedTrigger = improv_ble_ns.class_(
|
||||
"ImprovBLEStoppedTrigger", automation.Trigger.template()
|
||||
|
||||
_STATE_PARAMS = [(State, "state"), (Error, "error")]
|
||||
_NOT_FAILED = "!{parent}->is_failed()"
|
||||
|
||||
|
||||
def _state_automation(
|
||||
conf_key: str, args: list, states: str | None
|
||||
) -> automation.CallbackAutomation:
|
||||
# Every automation hangs off the state callback and is silent while the component has failed.
|
||||
return automation.CallbackAutomation(
|
||||
conf_key,
|
||||
"add_on_state_callback",
|
||||
args,
|
||||
params=_STATE_PARAMS,
|
||||
forward=[name for _, name in args],
|
||||
when=_NOT_FAILED if states is None else f"({states}) && {_NOT_FAILED}",
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
_state_automation(CONF_ON_PROVISIONED, [], "state == improv::STATE_PROVISIONED"),
|
||||
_state_automation(CONF_ON_PROVISIONING, [], "state == improv::STATE_PROVISIONING"),
|
||||
_state_automation(
|
||||
CONF_ON_START,
|
||||
[],
|
||||
"state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION",
|
||||
),
|
||||
_state_automation(CONF_ON_STATE, _STATE_PARAMS, None),
|
||||
_state_automation(CONF_ON_STOP, [], "state == improv::STATE_STOPPED"),
|
||||
)
|
||||
|
||||
|
||||
@@ -93,41 +102,11 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(
|
||||
CONF_WIFI_TIMEOUT, default=DEFAULT_WIFI_TIMEOUT
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ImprovBLEProvisionedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ImprovBLEProvisioningTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_START): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ImprovBLEStartTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ImprovBLEStateTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STOP): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ImprovBLEStoppedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_START): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_STOP): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
.extend(improv_base.IMPROV_SCHEMA)
|
||||
@@ -164,28 +143,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
status_indicator = await cg.get_variable(config[CONF_STATUS_INDICATOR])
|
||||
cg.add(var.set_status_indicator(status_indicator))
|
||||
|
||||
use_state_callback = False
|
||||
for conf in config.get(CONF_ON_PROVISIONED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
use_state_callback = True
|
||||
for conf in config.get(CONF_ON_PROVISIONING, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
use_state_callback = True
|
||||
for conf in config.get(CONF_ON_START, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
use_state_callback = True
|
||||
for conf in config.get(CONF_ON_STATE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger, [(State, "state"), (Error, "error")], conf
|
||||
)
|
||||
use_state_callback = True
|
||||
for conf in config.get(CONF_ON_STOP, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
use_state_callback = True
|
||||
if use_state_callback:
|
||||
if any(config.get(entry.conf_key) for entry in _CALLBACK_AUTOMATIONS):
|
||||
cg.add_define("USE_IMPROV_BLE_STATE_CALLBACK")
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
#pragma once
|
||||
#ifdef USE_ESP32
|
||||
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
|
||||
#include "improv_ble_component.h"
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
#include <improv.h>
|
||||
|
||||
namespace esphome::improv_ble {
|
||||
|
||||
class ImprovBLEProvisionedTrigger final : public Trigger<> {
|
||||
public:
|
||||
explicit ImprovBLEProvisionedTrigger(ImprovBLEComponent *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
|
||||
if (state == improv::STATE_PROVISIONED && !this->parent_->is_failed()) {
|
||||
this->trigger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
ImprovBLEComponent *parent_;
|
||||
};
|
||||
|
||||
class ImprovBLEProvisioningTrigger final : public Trigger<> {
|
||||
public:
|
||||
explicit ImprovBLEProvisioningTrigger(ImprovBLEComponent *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
|
||||
if (state == improv::STATE_PROVISIONING && !this->parent_->is_failed()) {
|
||||
this->trigger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
ImprovBLEComponent *parent_;
|
||||
};
|
||||
|
||||
class ImprovBLEStartTrigger final : public Trigger<> {
|
||||
public:
|
||||
explicit ImprovBLEStartTrigger(ImprovBLEComponent *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
|
||||
if ((state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION) &&
|
||||
!this->parent_->is_failed()) {
|
||||
this->trigger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
ImprovBLEComponent *parent_;
|
||||
};
|
||||
|
||||
class ImprovBLEStateTrigger final : public Trigger<improv::State, improv::Error> {
|
||||
public:
|
||||
explicit ImprovBLEStateTrigger(ImprovBLEComponent *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
|
||||
if (!this->parent_->is_failed()) {
|
||||
this->trigger(state, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
ImprovBLEComponent *parent_;
|
||||
};
|
||||
|
||||
class ImprovBLEStoppedTrigger final : public Trigger<> {
|
||||
public:
|
||||
explicit ImprovBLEStoppedTrigger(ImprovBLEComponent *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
|
||||
if (state == improv::STATE_STOPPED && !this->parent_->is_failed()) {
|
||||
this->trigger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
ImprovBLEComponent *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::improv_ble
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user