Merge remote-tracking branch 'origin/dev' into HEAD

This commit is contained in:
J. Nick Koston
2026-10-09 18:28:25 -10:00
405 changed files with 4976 additions and 3049 deletions
+12 -16
View File
@@ -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
+26
View File
@@ -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
View File
@@ -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 \
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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()),
+2
View File
@@ -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_();
});
+1 -1
View File
@@ -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;
+1 -10
View File
@@ -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);
}
+4 -4
View File
@@ -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);
+1 -1
View File
@@ -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
+4 -2
View File
@@ -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");
+3 -1
View File
@@ -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 -2
View File
@@ -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); });
});
}
}
+1
View File
@@ -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"
+16 -14
View File
@@ -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;
+10 -10
View File
@@ -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
+1 -1
View File
@@ -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
+10 -9
View File
@@ -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;
}
}
+4 -2
View File
@@ -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
+64 -160
View File
@@ -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)
+79 -86
View File
@@ -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
+33 -48
View File
@@ -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
+47 -141
View File
@@ -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)
+3 -1
View File
@@ -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_(); });
}
}
+3 -1
View File
@@ -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;
+2 -2
View File
@@ -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) {
+5 -8
View File
@@ -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"}
)
+20
View File
@@ -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
+84 -5
View File
@@ -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
+22 -7
View File
@@ -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
+7 -2
View File
@@ -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
+26
View File
@@ -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
)
+10
View File
@@ -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
+4 -6
View File
@@ -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
+4 -6
View File
@@ -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
+4 -6
View File
@@ -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
+4 -6
View File
@@ -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
+4 -6
View File
@@ -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
+5
View File
@@ -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
+2
View File
@@ -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])
+2 -1
View File
@@ -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:
+11 -7
View File
@@ -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
View File
@@ -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;
}
}
+10 -29
View File
@@ -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;
+24 -1
View File
@@ -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
+5 -3
View File
@@ -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");
}
+6 -3
View File
@@ -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() {
+2 -1
View File
@@ -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); }
+11 -7
View File
@@ -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_{};
+12 -1
View File
@@ -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());
+3 -1
View File
@@ -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;
+37 -80
View File
@@ -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.
+8 -72
View File
@@ -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