Merge remote-tracking branch 'origin/dev' into esp8266-entity-names-flash

# Conflicts:
#	esphome/components/current_based/current_based_cover.cpp
This commit is contained in:
J. Nick Koston
2026-10-09 09:37:51 -10:00
265 changed files with 2834 additions and 1567 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 {
+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,
),
}
+160 -81
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,6 +257,13 @@ def literal_with_length(config: ConfigType, value: str) -> str:
return f"{cg.safe_exp(value)}, {len(value.encode('utf-8'))}"
def string_ref_literal(config: ConfigType, value: str) -> str:
"""Renderer for a ``StringRef`` comparison: a flash literal on ESP8266, else ``StringRef(literal, length)``."""
if CORE.is_esp8266:
return str(cg.FlashStringLiteral(value))
return f"StringRef({literal_with_length(config, value)})"
@dataclass(frozen=True)
class ApplyCall:
"""One statement from config keys, e.g. ``"set_range({}, {})"`` with ``((CONF_LOW, cg.float_), ...)``.
@@ -365,9 +370,16 @@ def _check_key_in_schema(
schema = schema.schema[markers[part]]
def parent_ref(var: MockObj) -> MockObj:
"""``var`` named from global scope, so a trigger argument cannot shadow it.
Also how a generated callback names its Automation.
"""
return MockObj(f"::{var}", "->")
async def _apply_parent(config: ConfigType, id_key: str = CONF_ID) -> str:
# Global-scope qualified so a trigger arg named like the id cannot shadow it.
return f"::{await cg.get_variable(config[id_key])}"
return str(parent_ref(await cg.get_variable(config[id_key])))
def _apply_lambda_args(args: TemplateArgsType) -> TemplateArgsType:
@@ -399,7 +411,7 @@ async def _render_values(
members: list[tuple[Any, Any, Any]],
values: list[Any],
config: ConfigType,
parent: str,
parent: str | None,
lambda_args: TemplateArgsType,
compare: bool = False,
) -> list[str]:
@@ -477,7 +489,7 @@ def register_apply_action(
statements: list[str] = []
for target, members in statements_spec:
values = _apply_values(config, members)
if members and all(value is None for value in values):
if not _apply_call_active(members, values):
continue
exprs = await _render_values(
name, target, members, values, config, parent, lambda_args
@@ -496,6 +508,27 @@ def register_apply_action(
register_action(name, ApplyAction, schema, synchronous=True)(builder)
def _apply_call_active(members: list[tuple[Any, Any, Any]], values: list[Any]) -> bool:
"""An ``ApplyCall`` is emitted unless it has keys and none of them is set."""
return not members or any(value is not None for value in values)
async def _render_check(
name: str,
target: str,
members: list[tuple[Any, Any, Any]],
values: list[Any],
config: ConfigType,
parent: str | None,
lambda_args: TemplateArgsType,
) -> str:
"""Render a boolean check with ``values`` compared against config."""
exprs = await _render_values(
name, target, members, values, config, parent, lambda_args, compare=True
)
return target.format(*exprs)
def register_apply_condition(
name: str, schema: cv.Schema, check: str | ApplyCall, id_key: str = CONF_ID
) -> None:
@@ -521,22 +554,16 @@ def register_apply_condition(
) -> MockObj:
parent = await _apply_parent(config, id_key)
lambda_args = _apply_lambda_args(args)
exprs = await _render_values(
name,
call.target,
members,
_apply_values(config, members),
config,
parent,
lambda_args,
compare=True,
values = _apply_values(config, members)
check = await _render_check(
name, call.target, members, values, config, parent, lambda_args
)
return _apply_function(
condition_id,
cg.bool_,
template_arg,
lambda_args,
[f"return {parent}->{call.target.format(*exprs)};"],
[f"return {parent}->{check};"],
)
register_condition(name, ApplyCondition, schema)(builder)
@@ -672,52 +699,20 @@ XorCondition = cg.esphome_ns.class_("XorCondition", Condition)
@register_condition("and", AndCondition, validate_condition_list)
async def and_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
)
@register_condition("or", OrCondition, validate_condition_list)
async def or_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
)
@register_condition("all", AndCondition, validate_condition_list)
async def all_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
)
@register_condition("or", OrCondition, validate_condition_list)
@register_condition("any", OrCondition, validate_condition_list)
async def any_condition_to_code(
@register_condition("xor", XorCondition, validate_condition_list)
async def condition_group_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
# A group of one is that condition for and, or and xor; skip the wrapper.
if len(conditions) == 1:
return conditions[0]
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
)
@@ -734,19 +729,6 @@ async def not_condition_to_code(
return cg.new_Pvariable(condition_id, template_arg, condition)
@register_condition("xor", XorCondition, validate_condition_list)
async def xor_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
)
@register_condition("lambda", LambdaCondition, cv.returning_lambda)
async def lambda_condition_to_code(
config: ConfigType,
@@ -1056,23 +1038,77 @@ def has_non_synchronous_actions(actions: ConfigType) -> bool:
return False
async def build_automation(
trigger: MockObj, args: TemplateArgsType, config: ConfigType
async def _new_automation(
args: TemplateArgsType, config: ConfigType, *ctor_args: MockObj
) -> MockObj:
arg_types = [arg[0] for arg in args]
templ = cg.TemplateArguments(*arg_types)
obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, trigger)
"""Create the Automation for ``config`` with its actions."""
templ = cg.TemplateArguments(*(arg[0] for arg in args))
obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, *ctor_args)
actions = await build_action_list(config[CONF_THEN], templ, args)
cg.add(obj.add_actions(actions))
return obj
async def build_automation(
trigger: MockObj, args: TemplateArgsType, config: ConfigType
) -> MockObj:
return await _new_automation(args, config, trigger)
async def build_trigger_callback(
args: TemplateArgsType,
config: ConfigType,
params: TemplateArgsType,
forward: Sequence[str | Expression] | None = None,
when: str | ApplyCall | None = None,
) -> LambdaExpression:
"""Build the Automation for ``config`` and return a stateless callback that triggers it.
``params`` are the parent callback's parameters, ``forward`` the expressions passed to
``trigger()`` (default: the parameter names; write the parent as ``parent_ref(var)``),
``when`` a filter the callback returns early on, skipped like any ``ApplyCall`` when none
of its keys is set.
"""
members: list[tuple[Any, Any, Any]] = []
if when is not None:
call = when if isinstance(when, ApplyCall) else ApplyCall(when)
members = call.members
# A trigger callback has no parent for a str type to name.
if any(isinstance(t, str) and "{parent}" in t for _, t, _ in members):
raise ValueError(f"trigger filter {call.target!r}: a type names {{parent}}")
obj = await _new_automation(args, config)
lambda_args = _apply_lambda_args(params)
statements: list[str] = []
if when is not None:
values = _apply_values(config, members)
if _apply_call_active(members, values):
check = await _render_check(
"trigger filter",
call.target,
members,
values,
config,
None,
lambda_args,
)
statements.append(f"if (!({check}))\n return;")
if forward is None:
forward = [name for _, name in params]
statements.append(f"{parent_ref(obj)}->trigger({', '.join(map(str, forward))});")
return LambdaExpression(
["\n".join(statements)], lambda_args, capture="", return_type=cg.void
)
async def build_callback_automation(
parent: MockObj,
callback_method: str,
args: TemplateArgsType,
config: ConfigType,
forwarder: MockObj | MockObjClass | None = None,
params: TemplateArgsType | None = None,
forward: Sequence[str | Expression] | None = None,
when: str | ApplyCall | None = None,
) -> None:
"""Build an Automation and register it as a callback on the parent.
@@ -1084,6 +1120,9 @@ async def build_callback_automation(
pointer-sized (single Automation* field) to fit inline in Callback::ctx_
and avoid heap allocation.
With ``params``, ``forward`` or ``when`` the callback is instead the stateless
lambda of ``build_trigger_callback``; ``forwarder`` cannot be combined with them.
:param parent: The component object (e.g., button, sensor).
:param callback_method: Name of the callback method (e.g., "add_on_press_callback").
:param args: Automation template args as list of (type, name) tuples.
@@ -1092,22 +1131,56 @@ async def build_callback_automation(
TriggerForwarder<Ts...>. Pass any struct type whose aggregate init takes
a single Automation pointer (e.g., TriggerOnTrueForwarder).
"""
arg_types = [arg[0] for arg in args]
templ = cg.TemplateArguments(*arg_types)
obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ)
actions = await build_action_list(config[CONF_THEN], templ, args)
cg.add(obj.add_actions(actions))
if params is not None or forward is not None or when is not None:
if forwarder is not None:
raise ValueError(
"forwarder cannot be combined with params, forward or when"
)
callback = await build_trigger_callback(
args, config, args if params is None else params, forward, when
)
cg.add(getattr(parent, callback_method)(callback))
return
obj = await _new_automation(args, config)
# Use template forwarder structs for deduplication. The compiler generates
# one operator() per forwarder type; different automation pointers are just
# data in the struct.
if forwarder is None:
forwarder = TriggerForwarder.template(templ)
forwarder = TriggerForwarder.template(*(arg[0] for arg in args))
# RawExpression for aggregate init — both forwarder and obj are codegen
# MockObjs (not user input), and there's no Expression type for positional
# aggregate initialization (StructInitializer uses named fields).
cg.add(getattr(parent, callback_method)(cg.RawExpression(f"{forwarder}{{{obj}}}")))
async def build_parent_callback_automation(
parent: MockObj, callback_method: str, arg: tuple[Any, str], config: ConfigType
) -> None:
"""Register an Automation that receives ``parent`` from a callback that carries nothing.
``arg`` is the automation's ``(type, name)``, e.g. ``(Fan.operator("ptr"), "x")``.
"""
await build_callback_automation(
parent, callback_method, [arg], config, params=[], forward=[parent_ref(parent)]
)
async def build_trigger_automations(
parent: MockObj | None,
config: ConfigType,
entries: tuple[tuple[str, TemplateArgsType], ...],
) -> None:
"""Instantiate each entry's Trigger class, with ``parent`` when given, and build its automations.
``entries`` are ``(conf_key, args)`` pairs; the class comes from the entry's ``CONF_TRIGGER_ID``.
"""
ctor_args = () if parent is None else (parent,)
for conf_key, args in entries:
for conf in config.get(conf_key, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], *ctor_args)
await build_automation(trigger, args, conf)
@dataclass(frozen=True, slots=True)
class CallbackAutomation:
"""A single callback automation entry for build_callback_automations."""
@@ -1116,6 +1189,9 @@ class CallbackAutomation:
callback_method: str
args: TemplateArgsType = field(default_factory=list)
forwarder: MockObj | MockObjClass | None = None
params: TemplateArgsType | None = None
forward: Sequence[str | Expression] | None = None
when: str | ApplyCall | None = None
async def build_callback_automations(
@@ -1137,4 +1213,7 @@ async def build_callback_automations(
entry.args,
conf,
forwarder=entry.forwarder,
params=entry.params,
forward=entry.forward,
when=entry.when,
)
+38 -11
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 = (
@@ -554,10 +582,9 @@ def write_project(
)
# Snapshot the exclusion set so has_outdated_files() can trigger a
# discovery reconfigure when it changes. Excluded components never
# register in project_description.json, so re-including one (e.g. a
# config gains mqtt) requires a fresh discovery pass before the
# ESPHOME_PROJECT_BUILTIN_COMPONENTS property can list it.
# reconfigure when it changes. The ESPHOME_PROJECT_BUILTIN_COMPONENTS
# property drops excluded names, so re-including one (e.g. a config
# gains mqtt) needs a reconfigure to rebuild it from the discovery list.
write_file_if_changed(
CORE.relative_build_path("exclude_components.esphomeinternal"),
";".join(get_excluded_builtin_components()),
+1
View File
@@ -39,6 +39,7 @@ from esphome.cpp_generator import ( # noqa: F401
new_variable,
process_lambda,
progmem_array,
progmem_string,
safe_exp,
set_cpp_standard,
shared_progmem_array,
@@ -10,6 +10,9 @@ namespace esphome::airthings_wave_base {
ESPHOME_LOG_TAG(TAG, "airthings_wave_base");
static constexpr uint32_t BATTERY_READ_TIMEOUT_ID = 0;
static constexpr uint32_t RESPONSE_TIMEOUT_ID = 1;
void AirthingsWaveBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
switch (event) {
@@ -181,7 +184,7 @@ void AirthingsWaveBase::read_battery_(uint8_t *raw_value, uint16_t value_len) {
// read the battery again at the configured update interval
if (this->battery_update_interval_ != this->update_interval_) {
this->read_battery_next_update_ = false;
this->set_timeout("battery", this->battery_update_interval_,
this->set_timeout(BATTERY_READ_TIMEOUT_ID, this->battery_update_interval_,
[this]() { this->read_battery_next_update_ = true; });
}
}
@@ -204,7 +207,7 @@ void AirthingsWaveBase::response_received_() {
}
void AirthingsWaveBase::set_response_timeout_() {
this->set_timeout("response_timeout", 30 * 1000, [this]() {
this->set_timeout(RESPONSE_TIMEOUT_ID, 30 * 1000, [this]() {
this->responses_pending_ = 1;
this->response_received_();
});
+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;
@@ -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); });
});
}
}
+4 -2
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);
@@ -149,13 +151,13 @@ void CS5460AComponent::schedule_next_check_() {
this->status_momentary_warning("warning", 1000);
} else {
ESP_LOGCONFIG(TAG, "Device officially stuck, resetting");
this->cancel_timeout("status-check");
this->cancel_timeout(STATUS_CHECK_TIMEOUT_ID);
this->hw_init_();
return;
}
}
this->set_timeout("status-check", time_left, [this]() {
this->set_timeout(STATUS_CHECK_TIMEOUT_ID, time_left, [this]() {
if (!this->check_status_())
this->schedule_next_check_();
});
@@ -8,6 +8,8 @@ namespace esphome::ct_clamp {
ESPHOME_LOG_TAG(TAG, "ct_clamp");
static constexpr uint32_t READ_TIMEOUT_ID = 0;
void CTClampSensor::dump_config() {
LOG_SENSOR("", "CT Clamp Sensor", this);
ESP_LOGCONFIG(TAG, " Sample Duration: %.2fs", this->sample_duration_ / 1e3f);
@@ -21,7 +23,7 @@ void CTClampSensor::update() {
this->high_freq_.start();
// Set timeout for ending sampling phase
this->set_timeout("read", this->sample_duration_, [this]() {
this->set_timeout(READ_TIMEOUT_ID, this->sample_duration_, [this]() {
this->is_sampling_ = false;
this->high_freq_.stop();
@@ -8,6 +8,8 @@ namespace esphome::current_based {
ESPHOME_LOG_TAG(TAG, "current_based.cover");
static constexpr uint32_t ROLLBACK_TIMEOUT_ID = 0;
using namespace esphome::cover;
CoverTraits CurrentBasedCover::get_traits() {
@@ -73,7 +75,7 @@ void CurrentBasedCover::loop() {
ESP_LOGD(TAG, "'%s' - Obstacle detected during opening.", LOG_STR_ARG(this->get_log_name()));
this->direction_idle_();
if (this->obstacle_rollback_ != 0) {
this->set_timeout("rollback", 300, [this]() {
this->set_timeout(ROLLBACK_TIMEOUT_ID, 300, [this]() {
ESP_LOGD(TAG, "'%s' - Rollback.", LOG_STR_ARG(this->get_log_name()));
this->target_position_ = clamp(this->position - this->obstacle_rollback_, 0.0F, 1.0F);
this->start_direction_(COVER_OPERATION_CLOSING);
@@ -94,7 +96,7 @@ void CurrentBasedCover::loop() {
ESP_LOGD(TAG, "'%s' - Obstacle detected during closing.", LOG_STR_ARG(this->get_log_name()));
this->direction_idle_();
if (this->obstacle_rollback_ != 0) {
this->set_timeout("rollback", 300, [this]() {
this->set_timeout(ROLLBACK_TIMEOUT_ID, 300, [this]() {
ESP_LOGD(TAG, "'%s' - Rollback.", LOG_STR_ARG(this->get_log_name()));
this->target_position_ = clamp(this->position + this->obstacle_rollback_, 0.0F, 1.0F);
this->start_direction_(COVER_OPERATION_OPENING);
@@ -5,6 +5,8 @@ namespace esphome::dallas_temp {
ESPHOME_LOG_TAG(TAG, "dallas.temp.sensor");
static constexpr uint32_t CONVERSION_TIMEOUT_ID = 0;
static const uint8_t DALLAS_MODEL_DS18S20 = 0x10;
static const uint8_t DALLAS_MODEL_DS18B20 = 0x28;
static const uint8_t DALLAS_COMMAND_START_CONVERSION = 0x44;
@@ -44,7 +46,7 @@ void DallasTemperatureSensor::update() {
this->send_command_(DALLAS_COMMAND_START_CONVERSION);
this->set_timeout(this->get_address_name().c_str(), this->millis_to_wait_for_conversion_(), [this] {
this->set_timeout(CONVERSION_TIMEOUT_ID, this->millis_to_wait_for_conversion_(), [this] {
if (!this->read_scratch_pad_() || !this->check_scratch_pad_()) {
this->publish_state(NAN);
return;
@@ -1,6 +1,7 @@
#include "debug_component.h"
#ifdef USE_ZEPHYR
#include <climits>
#include "esphome/core/alloc_helpers.h"
#include "esphome/core/log.h"
#include <esphome/components/zephyr/reset_reason.h>
#include <zephyr/drivers/hwinfo.h>
+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();
+28 -95
View File
@@ -1,4 +1,3 @@
import logging
import re
from typing import Any
@@ -18,8 +17,6 @@ from esphome.const import (
from esphome.core import CORE
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@SimonFischer04", "@Tomer27cz", "@latonita", "@PolarGoose"]
DEPENDENCIES = ["uart"]
DOMAIN = "dlms_meter"
@@ -75,56 +72,6 @@ def validate_custom_pattern(value: ConfigType) -> ConfigType:
return value
def validate_provider_deprecation(config: ConfigType) -> ConfigType:
if CONF_PROVIDER in config:
provider = str(config[CONF_PROVIDER]).lower()
if provider == "netznoe":
_LOGGER.warning(
"The 'provider: netznoe' option is deprecated and will be removed in 2026.11.0. "
"The required custom patterns have been added automatically for this release, but you must update your configuration.\n"
"Please remove the 'provider' key and explicitly replace it with the following:\n\n"
"custom_patterns:\n"
' - pattern: "L, TSTR"\n'
' name: "MeterID"\n'
' default_obis: "0.0.96.1.0.255"\n'
' - pattern: "F, TDTM"\n'
' name: "DateTime"\n'
' default_obis: "0.0.1.0.0.255"\n'
)
patterns = config.get(CONF_CUSTOM_PATTERNS, [])
# Ensure "L, TSTR" for MeterID is present
if not any(p.get(CONF_PATTERN) == "L, TSTR" for p in patterns):
patterns.append(
{
CONF_PATTERN: "L, TSTR",
CONF_NAME: "MeterID",
CONF_DEFAULT_OBIS: [0, 0, 96, 1, 0, 255],
CONF_PRIORITY: 0,
}
)
# Ensure "F, TDTM" for DateTime is present
if not any(p.get(CONF_PATTERN) == "F, TDTM" for p in patterns):
patterns.append(
{
CONF_PATTERN: "F, TDTM",
CONF_NAME: "DateTime",
CONF_DEFAULT_OBIS: [0, 0, 1, 0, 0, 255],
CONF_PRIORITY: 0,
}
)
config[CONF_CUSTOM_PATTERNS] = patterns
else:
_LOGGER.warning(
"The 'provider' option is deprecated and will be removed in 2026.11.0. "
"The dlms_parser library now handles quirks dynamically. "
"Please remove this option from your configuration."
)
return config
CUSTOM_PATTERN_SCHEMA = cv.All(
custom_pattern_dict,
cv.Schema(
@@ -138,7 +85,7 @@ CUSTOM_PATTERN_SCHEMA = cv.All(
validate_custom_pattern,
)
CONFIG_SCHEMA = cv.All(
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(DlmsMeterComponent),
@@ -146,15 +93,27 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_AUTH_KEY): cv.bind_key(name="Authentication key"),
cv.Optional(CONF_CUSTOM_PATTERNS): cv.ensure_list(CUSTOM_PATTERN_SCHEMA),
cv.Optional(CONF_SKIP_CRC, default=False): cv.boolean,
cv.Optional(CONF_PROVIDER): cv.string,
# Removed in 2026.11.0 - kept to provide helpful error message
# Remove before 2027.5.0
cv.Optional(CONF_PROVIDER): cv.invalid(
"The 'provider' option has been removed in ESPHome 2026.11.0.\n"
"For 'provider: netznoe', replace it with:\n\n"
"custom_patterns:\n"
' - pattern: "L, TSTR"\n'
' name: "MeterID"\n'
' default_obis: "0.0.96.1.0.255"\n'
' - pattern: "F, TDTM"\n'
' name: "DateTime"\n'
' default_obis: "0.0.1.0.0.255"\n\n'
"For any other provider, remove the option"
),
cv.Optional(
CONF_RECEIVE_TIMEOUT, default="1000ms"
): cv.positive_time_period_milliseconds,
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
validate_provider_deprecation,
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema("dlms_meter", require_rx=True)
@@ -193,7 +152,7 @@ async def to_code(config: ConfigType) -> None:
cg.ArrayInitializer(
p[CONF_PATTERN],
name_expr,
p.get(CONF_PRIORITY, 0),
p[CONF_PRIORITY],
obis_expr,
)
)
@@ -213,43 +172,17 @@ async def to_code(config: ConfigType) -> None:
hub_id = config[CONF_ID].id
sensor_count = 0
for sens_conf in CORE.config.get(SENSOR_DOMAIN, []):
if (
sens_conf.get("platform") == DOMAIN
and sens_conf.get(CONF_DLMS_METER_ID).id == hub_id
):
if CONF_OBIS_CODE in sens_conf:
sensor_count += 1
else:
from .sensor import NUMERIC_KEYS
sensor_count += sum(1 for key in NUMERIC_KEYS if key in sens_conf)
text_sensor_count = 0
for sens_conf in CORE.config.get(TEXT_SENSOR_DOMAIN, []):
if (
sens_conf.get("platform") == DOMAIN
and sens_conf.get(CONF_DLMS_METER_ID).id == hub_id
):
if CONF_OBIS_CODE in sens_conf:
text_sensor_count += 1
else:
from .text_sensor import TEXT_KEYS
text_sensor_count += sum(1 for key in TEXT_KEYS if key in sens_conf)
binary_sensor_count = 0
for sens_conf in CORE.config.get(BINARY_SENSOR_DOMAIN, []):
if (
sens_conf.get("platform") == DOMAIN
and sens_conf.get(CONF_DLMS_METER_ID).id == hub_id
):
binary_sensor_count += 1
cg.add_define("DLMS_MAX_SENSORS", sensor_count)
cg.add_define("DLMS_MAX_TEXT_SENSORS", text_sensor_count)
cg.add_define("DLMS_MAX_BINARY_SENSORS", binary_sensor_count)
for domain, define in (
(SENSOR_DOMAIN, "DLMS_MAX_SENSORS"),
(TEXT_SENSOR_DOMAIN, "DLMS_MAX_TEXT_SENSORS"),
(BINARY_SENSOR_DOMAIN, "DLMS_MAX_BINARY_SENSORS"),
):
count = sum(
1
for conf in CORE.config.get(domain, [])
if conf.get("platform") == DOMAIN and conf[CONF_DLMS_METER_ID].id == hub_id
)
cg.add_define(define, count)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
+39 -139
View File
@@ -1,159 +1,59 @@
import logging
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_VOLT,
UNIT_WATT,
UNIT_WATT_HOURS,
)
from esphome.types import ConfigType
from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["dlms_meter"]
NUMERIC_KEYS = {
"voltage_l1": "1.0.32.7.0.255",
"voltage_l2": "1.0.52.7.0.255",
"voltage_l3": "1.0.72.7.0.255",
"current_l1": "1.0.31.7.0.255",
"current_l2": "1.0.51.7.0.255",
"current_l3": "1.0.71.7.0.255",
"active_power_plus": "1.0.1.7.0.255",
"active_power_minus": "1.0.2.7.0.255",
"active_energy_plus": "1.0.1.8.0.255",
"active_energy_minus": "1.0.2.8.0.255",
"reactive_energy_plus": "1.0.3.8.0.255",
"reactive_energy_minus": "1.0.4.8.0.255",
"power_factor": "1.0.13.7.0.255",
# Removed in 2026.11.0 - kept to provide helpful error message
# Remove before 2027.5.0
_MEASUREMENT = "state_class: measurement"
_VOLTAGE = f"unit_of_measurement: V, accuracy_decimals: 1, device_class: voltage, {_MEASUREMENT}"
_CURRENT = f"unit_of_measurement: A, accuracy_decimals: 2, device_class: current, {_MEASUREMENT}"
_POWER = (
f"unit_of_measurement: W, accuracy_decimals: 0, device_class: power, {_MEASUREMENT}"
)
_ENERGY = (
"unit_of_measurement: Wh, accuracy_decimals: 0, device_class: energy, "
"state_class: total_increasing"
)
_POWER_FACTOR = f"accuracy_decimals: 3, device_class: power_factor, {_MEASUREMENT}"
REMOVED_KEYS = {
"voltage_l1": ("1.0.32.7.0.255", _VOLTAGE),
"voltage_l2": ("1.0.52.7.0.255", _VOLTAGE),
"voltage_l3": ("1.0.72.7.0.255", _VOLTAGE),
"current_l1": ("1.0.31.7.0.255", _CURRENT),
"current_l2": ("1.0.51.7.0.255", _CURRENT),
"current_l3": ("1.0.71.7.0.255", _CURRENT),
"active_power_plus": ("1.0.1.7.0.255", _POWER),
"active_power_minus": ("1.0.2.7.0.255", _POWER),
"active_energy_plus": ("1.0.1.8.0.255", _ENERGY),
"active_energy_minus": ("1.0.2.8.0.255", _ENERGY),
"reactive_energy_plus": ("1.0.3.8.0.255", _ENERGY),
"reactive_energy_minus": ("1.0.4.8.0.255", _ENERGY),
"power_factor": ("1.0.13.7.0.255", _POWER_FACTOR),
}
DYNAMIC_SCHEMA = sensor.sensor_schema().extend(
CONFIG_SCHEMA = sensor.sensor_schema().extend(
{
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
cv.Required(CONF_OBIS_CODE): obis_code,
**{
cv.Optional(key): cv.invalid(
f"The predefined '{key}' key was removed in ESPHome 2026.11.0. "
f"Add a separate '- platform: dlms_meter' sensor with "
f'obis_code: "{obis}" and {settings}'
)
for key, (obis, settings) in REMOVED_KEYS.items()
},
}
)
def deprecation_warning(config: ConfigType) -> ConfigType:
_LOGGER.warning(
"The dlms_meter sensor schema using predefined keys (e.g., 'voltage_l1') is deprecated and will be removed in 2026.11.0. "
"Please update your configuration to use the new schema with 'obis_code'."
)
return config
OLD_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
cv.Optional("voltage_l1"): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("voltage_l2"): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("voltage_l3"): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("current_l1"): sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("current_l2"): sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("current_l3"): sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("active_power_plus"): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("active_power_minus"): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("active_energy_plus"): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("active_energy_minus"): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("reactive_energy_plus"): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("reactive_energy_minus"): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("power_factor"): sensor.sensor_schema(
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
}
).extend(cv.COMPONENT_SCHEMA),
deprecation_warning,
)
CONFIG_SCHEMA = cv.Any(DYNAMIC_SCHEMA, OLD_SCHEMA)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
if obis := config.get(CONF_OBIS_CODE):
var = await sensor.new_sensor(config)
cg.add(hub.register_sensor(obis, var))
else:
for key, obis_val in NUMERIC_KEYS.items():
if sensor_config := config.get(key):
sens = await sensor.new_sensor(sensor_config)
cg.add(hub.register_sensor(obis_val, sens))
var = await sensor.new_sensor(config)
cg.add(hub.register_sensor(config[CONF_OBIS_CODE], var))
@@ -1,5 +1,3 @@
import logging
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
@@ -7,54 +5,33 @@ from esphome.types import ConfigType
from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["dlms_meter"]
TEXT_KEYS = {
# Removed in 2026.11.0 - kept to provide helpful error message
# Remove before 2027.5.0
REMOVED_KEYS = {
"timestamp": "0.0.1.0.0.255",
"meternumber": "0.0.96.1.0.255",
}
DYNAMIC_SCHEMA = text_sensor.text_sensor_schema().extend(
CONFIG_SCHEMA = text_sensor.text_sensor_schema().extend(
{
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
cv.Required(CONF_OBIS_CODE): obis_code,
**{
cv.Optional(key): cv.invalid(
f"The predefined '{key}' key was removed in ESPHome 2026.11.0. "
f"Add a separate '- platform: dlms_meter' text sensor with "
f'obis_code: "{obis}" instead'
)
for key, obis in REMOVED_KEYS.items()
},
}
)
def deprecation_warning(config: ConfigType) -> ConfigType:
_LOGGER.warning(
"The dlms_meter text_sensor schema using predefined keys (e.g., 'timestamp') is deprecated and will be removed in 2026.11.0. "
"Please update your configuration to use the new schema with 'obis_code'."
)
return config
OLD_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
cv.Optional("timestamp"): text_sensor.text_sensor_schema(),
cv.Optional("meternumber"): text_sensor.text_sensor_schema(),
}
).extend(cv.COMPONENT_SCHEMA),
deprecation_warning,
)
CONFIG_SCHEMA = cv.Any(DYNAMIC_SCHEMA, OLD_SCHEMA)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
if obis := config.get(CONF_OBIS_CODE):
var = await text_sensor.new_text_sensor(config)
cg.add(hub.register_text_sensor(obis, var))
else:
for key, obis_val in TEXT_KEYS.items():
if text_sensor_config := config.get(key):
sens = await text_sensor.new_text_sensor(text_sensor_config)
cg.add(hub.register_text_sensor(obis_val, sens))
var = await text_sensor.new_text_sensor(config)
cg.add(hub.register_text_sensor(config[CONF_OBIS_CODE], var))
+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;
+5
View File
@@ -2926,6 +2926,11 @@ async def to_code(config):
# produce reproducible outputs and downstream tooling can reuse artifacts.
add_idf_sdkconfig_option("CONFIG_APP_REPRODUCIBLE_BUILD", True)
# Static destructors never run, so skip registering them. See atexit_stubs.cpp.
# --undefined: libsrc.a is scanned before the IDF libraries that also register them.
cg.add_build_flag("-Wl,--wrap=__cxa_atexit")
cg.add_build_flag("-Wl,--undefined=__wrap___cxa_atexit")
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_build_flag("-DUSE_ESP_IDF")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
+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
+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])
+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);
+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;
+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");
}
@@ -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() {
+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; }
@@ -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();
@@ -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,
),
+8 -72
View File
@@ -34,84 +34,36 @@ CONFIG_SCHEMA = cv.Schema(
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
icon=ICON_SIGNAL,
unit_of_measurement=UNIT_CENTIMETER,
),
cv.Optional(CONF_LIGHT): sensor.sensor_schema(
device_class=DEVICE_CLASS_ILLUMINANCE,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
icon=ICON_LIGHTBULB,
unit_of_measurement=UNIT_EMPTY, # No standard unit for this light sensor
),
cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
icon=ICON_SIGNAL,
unit_of_measurement=UNIT_CENTIMETER,
),
cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema(
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
icon=ICON_MOTION_SENSOR,
unit_of_measurement=UNIT_PERCENT,
),
cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
icon=ICON_SIGNAL,
unit_of_measurement=UNIT_CENTIMETER,
),
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
icon=ICON_FLASH,
unit_of_measurement=UNIT_PERCENT,
),
@@ -124,29 +76,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
{
cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
icon=ICON_MOTION_SENSOR,
unit_of_measurement=UNIT_PERCENT,
),
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
icon=ICON_FLASH,
unit_of_measurement=UNIT_PERCENT,
),
+12 -108
View File
@@ -46,41 +46,17 @@ CONFIG_SCHEMA = cv.Schema(
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema(
accuracy_decimals=0,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
icon=ICON_ACCOUNT_GROUP,
),
cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema(
accuracy_decimals=0,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
icon=ICON_HUMAN_GREETING_PROXIMITY,
),
cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema(
accuracy_decimals=0,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
icon=ICON_ACCOUNT_SWITCH,
),
}
@@ -92,85 +68,37 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
{
cv.Optional(CONF_X): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
icon=ICON_ALPHA_X_BOX_OUTLINE,
unit_of_measurement=UNIT_MILLIMETER,
),
cv.Optional(CONF_Y): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
icon=ICON_ALPHA_Y_BOX_OUTLINE,
unit_of_measurement=UNIT_MILLIMETER,
),
cv.Optional(CONF_SPEED): sensor.sensor_schema(
device_class=DEVICE_CLASS_SPEED,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
icon=ICON_SPEEDOMETER_SLOW,
unit_of_measurement=UNIT_MILLIMETER_PER_SECOND,
),
cv.Optional(CONF_ANGLE): sensor.sensor_schema(
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
icon=ICON_FORMAT_TEXT_ROTATION_ANGLE_UP,
unit_of_measurement=UNIT_DEGREES,
accuracy_decimals=1,
),
cv.Optional(CONF_DISTANCE): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
icon=ICON_MAP_MARKER_DISTANCE,
unit_of_measurement=UNIT_MILLIMETER,
),
cv.Optional(CONF_RESOLUTION): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
icon=ICON_RELATION_ZERO_OR_ONE_TO_ZERO_OR_ONE,
unit_of_measurement=UNIT_MILLIMETER,
),
@@ -183,41 +111,17 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
{
cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema(
accuracy_decimals=0,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
icon=ICON_MAP_MARKER_ACCOUNT,
),
cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema(
accuracy_decimals=0,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
icon=ICON_MAP_MARKER_ACCOUNT,
),
cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema(
accuracy_decimals=0,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
icon=ICON_MAP_MARKER_ACCOUNT,
),
}
+4 -3
View File
@@ -620,7 +620,7 @@ void LD6002BComponent::handle_frame_(uint16_t type, const uint8_t *data, uint16_
this->process_command_queue_();
if (refresh_areas) {
this->area_write_in_flight_ = false;
this->set_timeout(AREA_REFRESH_TIMEOUT, 50, [this]() { this->send_control_command_(CMD_GET_AREAS); });
this->set_timeout(AREA_REFRESH_TIMEOUT_ID, 50, [this]() { this->send_control_command_(CMD_GET_AREAS); });
}
return;
}
@@ -1215,7 +1215,7 @@ void LD6002BComponent::send_command_internal_(uint16_t type, const uint8_t *data
std::memcpy(this->wake_scratch_.data(), data, len);
}
// A button pulse must not raise the pin in the middle of this one.
this->cancel_timeout(WAKE_BUTTON_TIMEOUT);
this->cancel_timeout(WAKE_BUTTON_TIMEOUT_ID);
this->wake_pulse_pending_ = true;
this->wakeup_pin_->digital_write(false);
const uint8_t generation = this->send_generation_;
@@ -1364,7 +1364,8 @@ void LD6002BComponent::wake_() {
if (this->wakeup_pin_ == nullptr || this->wake_pulse_pending_)
return;
this->wakeup_pin_->digital_write(false);
this->set_timeout(WAKE_BUTTON_TIMEOUT, this->wakeup_pulse_ms_, [this]() { this->wakeup_pin_->digital_write(true); });
this->set_timeout(WAKE_BUTTON_TIMEOUT_ID, this->wakeup_pulse_ms_,
[this]() { this->wakeup_pin_->digital_write(true); });
}
void LD6002BComponent::set_number_value(NumberType type, float value) {
+4 -4
View File
@@ -413,12 +413,12 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
// How long the module stays awake after any frame, and so still answers the next one.
static constexpr uint32_t MODULE_AWAKE_MS = 10000;
static constexpr uint8_t CMD_MAX_RETRIES = 3;
// Named so a repeated press replaces its own pending timeout instead of stacking
// Keyed so a repeated press replaces its own pending timeout instead of stacking
// another, and so the command path can cancel it when it takes the pin over.
static constexpr const char *WAKE_BUTTON_TIMEOUT = "wake_button";
// Named so a burst of writes collapses to one read once they settle, rather than
static constexpr uint32_t WAKE_BUTTON_TIMEOUT_ID = 0;
// Keyed so a burst of writes collapses to one read once they settle, rather than
// one read per write.
static constexpr const char *AREA_REFRESH_TIMEOUT = "area_refresh";
static constexpr uint32_t AREA_REFRESH_TIMEOUT_ID = 1;
// A reply cannot trail the frame that earned it for longer than this; the field worst case is ~726ms.
static constexpr uint32_t STALE_ACK_MAX_AGE_MS = 1000;
+4 -16
View File
@@ -30,44 +30,32 @@ from .const import (
DEPENDENCIES = ["ld6002b"]
# The ld2450 defaults for a streamed value: hold the last reading for a second so a
# dropped frame does not read as absence, then rate-limit what reaches the frontend.
_VALUE_SENSOR_FILTERS = [
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
]
TARGET_SCHEMA = cv.Schema(
{
cv.Optional(CONF_X): sensor.sensor_schema(
unit_of_measurement=UNIT_METER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_DISTANCE,
filters=_VALUE_SENSOR_FILTERS,
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_Y): sensor.sensor_schema(
unit_of_measurement=UNIT_METER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_DISTANCE,
filters=_VALUE_SENSOR_FILTERS,
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_Z): sensor.sensor_schema(
unit_of_measurement=UNIT_METER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_DISTANCE,
filters=_VALUE_SENSOR_FILTERS,
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_DOPPLER_INDEX): sensor.sensor_schema(
accuracy_decimals=0,
filters=_VALUE_SENSOR_FILTERS,
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CLUSTER_ID): sensor.sensor_schema(
+3
View File
@@ -492,6 +492,9 @@ async def component_to_code(config):
# Not enabled on RTL87xx/LN882x — costs more heap than it saves there.
cg.add_build_flag("-DconfigSUPPORT_STATIC_ALLOCATION=1")
# Static destructors never run, so skip registering them. See atexit_stubs.cpp.
cg.add_build_flag("-Wl,--wrap=__cxa_atexit")
# LN882x: a zero-size allocation must not trip the SDK's assert (see ln882x_zero_malloc.c).
if config[CONF_COMPONENT_ID] == COMPONENT_LN882X:
cg.add_build_flag("-Wl,--wrap=pvPortMalloc")
@@ -0,0 +1,20 @@
/*
* Linker wrap stub for __cxa_atexit().
*
* GCC registers the destructor of every global and function local static
* object with __cxa_atexit(). The firmware never returns from main or calls
* exit(), so these destructors never run, but the first registration links
* newlib's exit handler tables.
*/
#include "esphome/core/defines.h"
#ifdef USE_LIBRETINY
namespace esphome::libretiny {}
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" int __wrap___cxa_atexit(void (*)(void *), void *, void *) { return 0; }
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif // USE_LIBRETINY
+5
View File
@@ -8,6 +8,7 @@
#include <FreeRTOS.h>
#include <task.h>
#include "esphome/core/main_task.h"
#include "esphome/core/time_64.h"
// IRAM_ATTR places a function in executable RAM so it is callable from an
@@ -84,6 +85,10 @@ __attribute__((always_inline)) inline bool in_isr_context() {
#endif
}
/// Before setup() stores the handle every task counts as background; the wake is a no-op until then.
__attribute__((always_inline)) inline bool is_main_loop_thread() {
return xTaskGetCurrentTaskHandle() == esphome_main_task_handle;
}
__attribute__((always_inline)) inline void yield() { ::yield(); }
__attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); }
__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(::micros()); }
+9 -17
View File
@@ -1,16 +1,10 @@
import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
from esphome.const import CONF_ON_RELEASE, CONF_RESTORE_VALUE
from esphome.const import CONF_RESTORE_VALUE
from esphome.cpp_generator import MockObj
from ..defines import (
CONF_ANIMATED,
CONF_TRIGGER,
CONF_UPDATE_ON_RELEASE,
CONF_WIDGET,
LOGGER,
)
from ..defines import CONF_ANIMATED, CONF_TRIGGER, CONF_UPDATE_ON_RELEASE, CONF_WIDGET
from ..lv_validation import animated
from ..lvcode import (
EVENT_ARG,
@@ -31,20 +25,18 @@ CONFIG_SCHEMA = number.number_schema(LVGLNumber).extend(
cv.Required(CONF_WIDGET): cv.use_id(LvNumber),
**VALUE_TRIGGER_SCHEMA,
cv.Optional(CONF_ANIMATED, default=True): animated,
cv.Optional(CONF_UPDATE_ON_RELEASE): cv.boolean,
# Removed in 2026.11.0 - kept to provide helpful error message
# Remove before 2027.5.0
cv.Optional(CONF_UPDATE_ON_RELEASE): cv.invalid(
"The 'update_on_release' option has been removed in ESPHome 2026.11.0.\n"
"Use 'trigger: on_release' instead."
),
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
}
)
async def to_code(config):
trigger = config[CONF_TRIGGER]
if CONF_UPDATE_ON_RELEASE in config:
LOGGER.warning(
"Option 'update_on_release' is deprecated and will be removed in 2026.11.0 - use 'trigger: on_release' instead"
)
if config[CONF_UPDATE_ON_RELEASE]:
trigger = CONF_ON_RELEASE
widget = await get_widgets(config, CONF_WIDGET)
widget = widget[0]
async with LambdaContext([], return_type=cg.float_) as value:
@@ -70,6 +62,6 @@ async def to_code(config):
lvgl_static.add_event_cb(
widget.obj,
await event.get_lambda(),
*TRIGGER_EVENT_MAP[trigger],
*TRIGGER_EVENT_MAP[config[CONF_TRIGGER]],
)
)
+3 -1
View File
@@ -7,6 +7,8 @@ namespace esphome::max31855 {
ESPHOME_LOG_TAG(TAG, "max31855");
static constexpr uint32_t READ_TIMEOUT_ID = 0;
void MAX31855Sensor::update() {
this->enable();
delay(1);
@@ -14,7 +16,7 @@ void MAX31855Sensor::update() {
this->disable();
// Conversion time typ: 170ms, max: 220ms
this->set_timeout("value", 220, [this]() { this->read_data_(); });
this->set_timeout(READ_TIMEOUT_ID, 220, [this]() { this->read_data_(); });
}
void MAX31855Sensor::setup() { this->spi_setup(); }
+3 -1
View File
@@ -7,6 +7,8 @@ namespace esphome::max31856 {
ESPHOME_LOG_TAG(TAG, "max31856");
static constexpr uint32_t READ_TIMEOUT_ID = 0;
// Based on Adafruit's library: https://github.com/adafruit/Adafruit_MAX31856
void MAX31856Sensor::setup() {
@@ -42,7 +44,7 @@ void MAX31856Sensor::update() {
this->one_shot_temperature_();
// Datasheet max conversion time for 1 shot is 155ms for 60Hz / 185ms for 50Hz
this->set_timeout("MAX31856Sensor::read_thermocouple_temperature_", filter_ == FILTER_60HZ ? 155 : 185,
this->set_timeout(READ_TIMEOUT_ID, filter_ == FILTER_60HZ ? 155 : 185,
[this]() { this->read_thermocouple_temperature_(); });
}
+3 -1
View File
@@ -8,6 +8,8 @@ namespace esphome::max31865 {
ESPHOME_LOG_TAG(TAG, "max31865");
static constexpr uint32_t READ_TIMEOUT_ID = 0;
void MAX31865Sensor::update() {
// Check new faults since last measurement
if (!has_fault_) {
@@ -59,7 +61,7 @@ void MAX31865Sensor::update() {
this->write_config_(0b11100000, 0b10100000);
// Datasheet max conversion time is 55ms for 60Hz / 66ms for 50Hz
this->set_timeout("value", filter_ == FILTER_60HZ ? 55 : 66, [this]() { this->read_data_(); });
this->set_timeout(READ_TIMEOUT_ID, filter_ == FILTER_60HZ ? 55 : 66, [this]() { this->read_data_(); });
}
void MAX31865Sensor::setup() {
+3 -1
View File
@@ -5,6 +5,8 @@ namespace esphome::max6675 {
ESPHOME_LOG_TAG(TAG, "max6675");
static constexpr uint32_t READ_TIMEOUT_ID = 0;
void MAX6675Sensor::update() {
this->enable();
delay(1);
@@ -12,7 +14,7 @@ void MAX6675Sensor::update() {
this->disable();
// Conversion time typ: 170ms, max: 220ms
this->set_timeout("value", 250, [this]() { this->read_data_(); });
this->set_timeout(READ_TIMEOUT_ID, 250, [this]() { this->read_data_(); });
}
void MAX6675Sensor::setup() { this->spi_setup(); }
@@ -4,6 +4,8 @@
namespace esphome::mcp23xxx_base {
void IRAM_ATTR mcp23xxx_gpio_intr(Component *arg) { arg->enable_loop_soon_any_context(); }
template<uint8_t N> void MCP23XXXGPIOPin<N>::setup() {
this->pin_mode(flags_);
// When interrupt_pin is configured, pin_mode() already auto-enables CHANGE
@@ -8,6 +8,10 @@ namespace esphome::mcp23xxx_base {
enum MCP23XXXInterruptMode : uint8_t { MCP23XXX_NO_INTERRUPT = 0, MCP23XXX_CHANGE, MCP23XXX_RISING, MCP23XXX_FALLING };
// Not a template member: GCC 10 (ESP8266) ignores section attributes on template instantiations, leaving the
// ISR in flash.
void IRAM_ATTR mcp23xxx_gpio_intr(Component *arg);
template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander::CachedGpioExpander<uint8_t, N> {
public:
virtual void pin_mode(uint8_t pin, gpio::Flags flags);
@@ -35,7 +39,8 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander:
void setup_interrupt_pin_() {
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&MCP23XXXBase::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
this->interrupt_pin_->attach_interrupt(&mcp23xxx_gpio_intr, static_cast<Component *>(this),
gpio::INTERRUPT_FALLING_EDGE);
this->set_invalidate_on_read_(false);
}
// Disable loop until an input pin is configured via pin_mode()
@@ -43,7 +48,6 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander:
// For polling mode, loop is re-enabled when pin_mode() registers an input pin
this->disable_loop();
}
static void IRAM_ATTR gpio_intr(MCP23XXXBase *arg) { arg->enable_loop_soon_any_context(); }
// read a given register
virtual bool read_reg(uint8_t reg, uint8_t *value) = 0;
+3 -1
View File
@@ -7,6 +7,8 @@ namespace esphome::mics_4514 {
ESPHOME_LOG_TAG(TAG, "mics_4514");
static constexpr uint32_t WARMUP_TIMEOUT_ID = 0;
static const uint8_t SENSOR_REGISTER = 0x04;
static const uint8_t POWER_MODE_REGISTER = 0x0a;
@@ -18,7 +20,7 @@ void MICS4514Component::setup() {
power_mode = 0x01;
this->write_register(POWER_MODE_REGISTER, &power_mode, 1);
delay(100); // NOLINT
this->set_timeout("warmup", 3 * 60 * 1000, [this]() { this->warmed_up_ = true; });
this->set_timeout(WARMUP_TIMEOUT_ID, 3 * 60 * 1000, [this]() { this->warmed_up_ = true; });
this->status_set_warning();
return;
}
@@ -7,6 +7,8 @@ namespace esphome::mlx90393 {
ESPHOME_LOG_TAG(TAG, "mlx90393");
static constexpr uint32_t VERIFY_SETTINGS_TIMEOUT_ID = 0;
const LogString *settings_to_string(MLX90393Setting setting) {
switch (setting) {
case MLX90393_GAIN_SEL:
@@ -114,7 +116,7 @@ void MLX90393Cls::setup() {
}
// start verify settings process
this->set_timeout("verify settings", 3000, [this]() { this->verify_settings_timeout_(MLX90393_GAIN_SEL); });
this->set_timeout(VERIFY_SETTINGS_TIMEOUT_ID, 3000, [this]() { this->verify_settings_timeout_(MLX90393_GAIN_SEL); });
}
void MLX90393Cls::dump_config() {
@@ -268,7 +270,8 @@ void MLX90393Cls::verify_settings_timeout_(MLX90393Setting stage) {
next_stage = static_cast<MLX90393Setting>(0);
}
this->set_timeout("verify settings", 3000, [this, next_stage]() { this->verify_settings_timeout_(next_stage); });
this->set_timeout(VERIFY_SETTINGS_TIMEOUT_ID, 3000,
[this, next_stage]() { this->verify_settings_timeout_(next_stage); });
}
} // namespace esphome::mlx90393
+4 -2
View File
@@ -11,6 +11,8 @@ namespace esphome::modbus {
ESPHOME_LOG_TAG(TAG, "modbus");
static constexpr uint32_t DEFERRED_SEND_TIMEOUT_ID = 0;
static constexpr size_t MODBUS_MAX_LOG_BYTES = 64;
static constexpr uint32_t US_PER_SEC = 1000000;
@@ -205,7 +207,7 @@ void ModbusServerHub::parse_modbus_frames() {
while (!this->rx_buffer_.empty()) {
if (this->deferred_payload_len_ != 0) {
// Another frame arrived before the deferred reply went out, so the client has moved on.
this->cancel_timeout("deferred_send");
this->cancel_timeout(DEFERRED_SEND_TIMEOUT_ID);
ESP_LOGD(TAG, "Dropped deferred reply to %" PRIu8 ": a new frame arrived first", this->deferred_payload_[0]);
this->deferred_payload_len_ = 0;
}
@@ -1216,7 +1218,7 @@ void ModbusServerHub::send_raw_(const uint8_t *payload, uint16_t len) {
std::memcpy(this->deferred_payload_.data(), payload, len);
this->deferred_payload_len_ = len;
// set_timeout() takes milliseconds; round the microsecond delay up so we never fire early.
this->set_timeout("deferred_send", (this->tx_delay_remaining() + US_PER_MS - 1) / US_PER_MS, [this]() {
this->set_timeout(DEFERRED_SEND_TIMEOUT_ID, (this->tx_delay_remaining() + US_PER_MS - 1) / US_PER_MS, [this]() {
ModbusFrame frame(this->deferred_payload_[0], this->deferred_payload_.data() + 1,
this->deferred_payload_len_ - 1);
this->deferred_payload_len_ = 0;
+2
View File
@@ -273,6 +273,8 @@ class MQTTComponent : public Component {
void subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos = 0);
protected:
static constexpr uint32_t SEND_DEFER_ID = 0;
/// Helper method to get the discovery topic for this component into a buffer.
StringRef get_discovery_topic_to_(std::span<char, MQTT_DISCOVERY_TOPIC_MAX_LEN> buf,
const MQTTDiscoveryInfo &discovery_info) const;
+1 -1
View File
@@ -121,7 +121,7 @@ void MQTTFanComponent::setup() {
});
}
this->state_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); });
this->state_->add_on_state_callback([this]() { this->defer(SEND_DEFER_ID, [this]() { this->publish_state(); }); });
}
void MQTTFanComponent::dump_config() {
+1 -1
View File
@@ -28,7 +28,7 @@ void MQTTJSONLightComponent::setup() {
}
void MQTTJSONLightComponent::on_light_remote_values_update() {
this->defer("send", [this]() { this->publish_state_(); });
this->defer(SEND_DEFER_ID, [this]() { this->publish_state_(); });
}
MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {}
+1 -1
View File
@@ -29,7 +29,7 @@ void MQTTLockComponent::setup() {
}
});
this->lock_->add_on_state_callback(
[this](LockState /*state*/) { this->defer("send", [this]() { this->publish_state(); }); });
[this](LockState /*state*/) { this->defer(SEND_DEFER_ID, [this]() { this->publish_state(); }); });
}
void MQTTLockComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Lock '%s': ", LOG_STR_ARG(this->lock_->get_log_name()));
+1 -1
View File
@@ -34,7 +34,7 @@ void MQTTSwitchComponent::setup() {
}
});
this->switch_->add_on_state_callback(
[this](bool enabled) { this->defer("send", [this, enabled]() { this->publish_state(enabled); }); });
[this](bool enabled) { this->defer(SEND_DEFER_ID, [this, enabled]() { this->publish_state(enabled); }); });
}
void MQTTSwitchComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Switch '%s': ", LOG_STR_ARG(this->switch_->get_log_name()));
+1 -1
View File
@@ -24,7 +24,7 @@ void MQTTUpdateComponent::setup() {
}
});
this->update_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); });
this->update_->add_on_state_callback([this]() { this->defer(SEND_DEFER_ID, [this]() { this->publish_state(); }); });
}
bool MQTTUpdateComponent::publish_state() {
+5 -2
View File
@@ -6,6 +6,9 @@ namespace esphome::ms5611 {
ESPHOME_LOG_TAG(TAG, "ms5611");
static constexpr uint32_t TEMPERATURE_TIMEOUT_ID = 0;
static constexpr uint32_t PRESSURE_TIMEOUT_ID = 1;
static const uint8_t MS5611_ADDRESS = 0x77;
static const uint8_t MS5611_CMD_ADC_READ = 0x00;
static const uint8_t MS5611_CMD_RESET = 0x1E;
@@ -44,7 +47,7 @@ void MS5611Component::update() {
return;
}
this->set_timeout("temperature", 10, [this]() { this->read_temperature_(); });
this->set_timeout(TEMPERATURE_TIMEOUT_ID, 10, [this]() { this->read_temperature_(); });
}
void MS5611Component::read_temperature_() {
uint8_t bytes[3];
@@ -60,7 +63,7 @@ void MS5611Component::read_temperature_() {
return;
}
this->set_timeout("pressure", 10, [this, raw_temperature]() { this->read_pressure_(raw_temperature); });
this->set_timeout(PRESSURE_TIMEOUT_ID, 10, [this, raw_temperature]() { this->read_pressure_(raw_temperature); });
}
void MS5611Component::read_pressure_(uint32_t raw_temperature) {
uint8_t bytes[3];
+12 -5
View File
@@ -9,6 +9,12 @@ namespace esphome::ms8607 {
/// TAG used for logging calls
ESPHOME_LOG_TAG(TAG, "ms8607");
static constexpr uint32_t RESET_TIMEOUT_ID = 0;
static constexpr uint32_t PROM_READ_TIMEOUT_ID = 1;
static constexpr uint32_t TEMPERATURE_TIMEOUT_ID = 2;
static constexpr uint32_t PRESSURE_TIMEOUT_ID = 3;
static constexpr uint32_t HUMIDITY_TIMEOUT_ID = 4;
/// Reset the Pressure/Temperature sensor
static const uint8_t MS8607_PT_CMD_RESET = 0x1E;
@@ -97,7 +103,7 @@ void MS8607Component::try_reset_() {
if (--this->reset_attempts_remaining_ > 0) {
uint32_t delay = this->reset_interval_;
this->reset_interval_ *= 5;
this->set_timeout("reset", delay, [this]() { this->try_reset_(); });
this->set_timeout(RESET_TIMEOUT_ID, delay, [this]() { this->try_reset_(); });
this->status_set_error();
} else {
this->mark_failed();
@@ -110,7 +116,7 @@ void MS8607Component::try_reset_() {
this->status_clear_error();
// 15ms delay matches datasheet, Adafruit_MS8607 & SparkFun_PHT_MS8607_Arduino_Library
this->set_timeout("prom-read", 15, [this]() {
this->set_timeout(PROM_READ_TIMEOUT_ID, 15, [this]() {
if (this->read_calibration_values_from_prom_()) {
this->setup_status_ = SetupStatus::SUCCESSFUL;
this->status_clear_error();
@@ -251,7 +257,7 @@ void MS8607Component::request_read_temperature_() {
}
// datasheet says 17.2ms max conversion time at OSR 8192
this->set_timeout("temperature", 20, [this]() { this->read_temperature_(); });
this->set_timeout(TEMPERATURE_TIMEOUT_ID, 20, [this]() { this->read_temperature_(); });
}
void MS8607Component::read_temperature_() {
@@ -272,7 +278,8 @@ void MS8607Component::request_read_pressure_(uint32_t d2_raw_temperature) {
}
// datasheet says 17.2ms max conversion time at OSR 8192
this->set_timeout("pressure", 20, [this, d2_raw_temperature]() { this->read_pressure_(d2_raw_temperature); });
this->set_timeout(PRESSURE_TIMEOUT_ID, 20,
[this, d2_raw_temperature]() { this->read_pressure_(d2_raw_temperature); });
}
void MS8607Component::read_pressure_(uint32_t d2_raw_temperature) {
@@ -293,7 +300,7 @@ void MS8607Component::request_read_humidity_(float temperature_float) {
}
// datasheet says 15.89ms max conversion time at OSR 8192
this->set_timeout("humidity", 20, [this, temperature_float]() { this->read_humidity_(temperature_float); });
this->set_timeout(HUMIDITY_TIMEOUT_ID, 20, [this, temperature_float]() { this->read_humidity_(temperature_float); });
}
void MS8607Component::read_humidity_(float temperature_float) {
+5 -16
View File
@@ -1,5 +1,3 @@
import logging
from esphome import automation
import esphome.codegen as cg
from esphome.components import display, esp32, uart
@@ -43,8 +41,6 @@ from .base_component import (
CONF_WAKE_UP_PAGE,
)
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"]
DEPENDENCIES = ["uart"]
@@ -56,15 +52,6 @@ def AUTO_LOAD() -> list[str]:
return base
def _deprecated_dump_device_info(value):
_LOGGER.warning(
"'dump_device_info' is deprecated and will be removed in ESPHome 2026.11.0. "
"Device info is now always logged at connection time. "
"Please remove this option from your configuration."
)
return value
def _validate_tft_upload(config):
has_tft_url = CONF_TFT_URL in config
for conf_key in (
@@ -91,9 +78,11 @@ CONFIG_SCHEMA = cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=TimePeriod(milliseconds=255)),
),
# Deprecated — device info is now always logged. Remove before 2026.11.0.
cv.Optional(CONF_DUMP_DEVICE_INFO): cv.All(
cv.boolean, _deprecated_dump_device_info
# Removed in 2026.11.0 - kept to provide helpful error message
# Remove before 2027.5.0
cv.Optional(CONF_DUMP_DEVICE_INFO): cv.invalid(
"The 'dump_device_info' option has been removed in ESPHome 2026.11.0.\n"
"Device info is now always logged, so remove this option from your configuration."
),
cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean,
cv.Optional(CONF_MAX_QUEUE_AGE, default="8000ms"): cv.All(
+37 -1
View File
@@ -12,6 +12,7 @@ from esphome.build_helpers import pch
from esphome.build_helpers.ccache import resolve_ccache_path
import esphome.codegen as cg
from esphome.components.zephyr import (
Section,
add_extra_script,
copy_files as zephyr_copy_files,
zephyr_add_overlay,
@@ -126,11 +127,37 @@ def set_core_data(config: ConfigType) -> ConfigType:
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = KEY_ZEPHYR
if config[KEY_BOOTLOADER] in BOOTLOADER_CONFIG:
zephyr_add_pm_static(BOOTLOADER_CONFIG[config[KEY_BOOTLOADER]])
sections = BOOTLOADER_CONFIG[config[KEY_BOOTLOADER]]
if CORE.testing_mode:
sections = _testing_mode_sections(sections)
zephyr_add_pm_static(sections)
return config
# In testing mode, fake a larger flash to allow linking grouped component
# tests. The nRF52840 has 1 MB and a mcumgr OTA config halves the app slot,
# which an openthread config alone fills; CI images are never flashed.
NRF52840_FLASH_SIZE = 0x100000
TESTING_FLASH_SIZE = 0x400000
def _testing_mode_sections(sections: list[Section]) -> list[Section]:
"""Move a bootloader pinned to the end of the real flash to the end of the
faked one, so the partition manager still sees a single gap for the app."""
return [
Section(
section.name,
section.address + TESTING_FLASH_SIZE - NRF52840_FLASH_SIZE,
section.size,
section.region,
)
if section.end_address == NRF52840_FLASH_SIZE
else section
for section in sections
]
_TOOLCHAINS = (Toolchain.PLATFORMIO, Toolchain.SDK_NRF)
_resolve_toolchain = cv.resolve_toolchain("nRF52", _TOOLCHAINS, Toolchain.SDK_NRF)
@@ -381,6 +408,15 @@ async def to_code(config: ConfigType) -> None:
zephyr_setup_preferences()
zephyr_to_code(config)
if CORE.testing_mode:
zephyr_add_overlay(
f"""
&flash0 {{
reg = <0x0 {TESTING_FLASH_SIZE:#x}>;
}};
"""
)
if dfu_config := config.get(CONF_DFU):
CORE.add_job(_dfu_to_code, dfu_config)
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
-13
View File
@@ -4,19 +4,6 @@ namespace esphome::one_wire {
ESPHOME_LOG_TAG(TAG, "one_wire");
const std::string &OneWireDevice::get_address_name() {
if (this->address_name_.empty()) {
char hex_buf[19]; // "0x" + 16 hex chars + null
this->address_name_ = format_hex_prefixed_to(hex_buf, this->address_);
}
return this->address_name_;
}
void OneWireDevice::set_address(uint64_t address) {
this->address_ = address;
this->address_name_.clear();
}
bool OneWireDevice::send_command_(uint8_t cmd) {
if (!this->bus_->select(this->address_))
return false;
+6 -7
View File
@@ -7,14 +7,17 @@
namespace esphome::one_wire {
#define LOG_ONE_WIRE_DEVICE(this) \
ESP_LOGCONFIG(TAG, " Address: %s (%s)", this->get_address_name().c_str(), \
LOG_STR_ARG(this->bus_->get_model_str(this->address_ & 0xff)));
do { \
char address_buf[format_hex_prefixed_size(sizeof(uint64_t))]; \
ESP_LOGCONFIG(TAG, " Address: %s (%s)", format_hex_prefixed_to(address_buf, (this)->address_), \
LOG_STR_ARG((this)->bus_->get_model_str((this)->address_ & 0xff))); \
} while (0)
class OneWireDevice {
public:
/// @brief store the address of the device
/// @param address of the device
void set_address(uint64_t address);
void set_address(uint64_t address) { this->address_ = address; }
void set_index(uint8_t index) { this->index_ = index; }
@@ -22,16 +25,12 @@ class OneWireDevice {
/// @param bus pointer to the OneWireBus object
void set_one_wire_bus(OneWireBus *bus) { this->bus_ = bus; }
/// Helper to create (and cache) the name for this sensor. For example "0xfe0000031f1eaf29".
const std::string &get_address_name();
protected:
static constexpr uint8_t INDEX_NOT_SET = 255;
uint64_t address_{0};
uint8_t index_{INDEX_NOT_SET};
OneWireBus *bus_{nullptr}; ///< pointer to OneWireBus instance
std::string address_name_;
/// @brief find an address if necessary
/// should be called from setup
+5 -2
View File
@@ -5,6 +5,9 @@ namespace esphome::opt3001 {
ESPHOME_LOG_TAG(TAG, "opt3001.sensor");
static constexpr uint32_t RESULT_RETRY_TIMEOUT_ID = 0;
static constexpr uint32_t READ_TIMEOUT_ID = 1;
static const uint8_t OPT3001_REG_RESULT = 0x00;
static const uint8_t OPT3001_REG_CONFIGURATION = 0x01;
// See datasheet for full description of each bit.
@@ -42,7 +45,7 @@ void OPT3001Sensor::read_result_(const std::function<void(float)> &f) {
if ((raw_value & OPT3001_CONFIGURATION_CONVERSION_MODE_MASK) != OPT3001_CONFIGURATION_CONVERSION_MODE_SHUTDOWN) {
// not ready; wait 10ms and try again
ESP_LOGW(TAG, "Data not ready; waiting 10ms");
this->set_timeout("opt3001_wait", 10, [this, f]() { read_result_(f); });
this->set_timeout(RESULT_RETRY_TIMEOUT_ID, 10, [this, f]() { read_result_(f); });
return;
}
@@ -70,7 +73,7 @@ void OPT3001Sensor::read_lx_(const std::function<void(float)> &f) {
return;
}
this->set_timeout("read", OPT3001_CONVERSION_TIME_800, [this, f]() {
this->set_timeout(READ_TIMEOUT_ID, OPT3001_CONVERSION_TIME_800, [this, f]() {
if (this->write(&OPT3001_REG_CONFIGURATION, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Starting configuration register read failed");
f(NAN);
@@ -5,6 +5,8 @@ namespace esphome::output {
ESPHOME_LOG_TAG(TAG, "output.button");
static constexpr uint32_t RESET_TIMEOUT_ID = 0;
void OutputButton::dump_config() {
LOG_BUTTON("", "Output Button", this);
ESP_LOGCONFIG(TAG, " Duration: %.1fs", this->duration_ / 1e3f);
@@ -12,8 +14,8 @@ void OutputButton::dump_config() {
void OutputButton::press_action() {
this->output_->turn_on();
// Use a named timeout so that it's automatically cancelled if button is pressed again before it's reset
this->set_timeout("reset", this->duration_, [this]() { this->output_->turn_off(); });
// One timeout id, so a second press restarts the reset instead of stacking another
this->set_timeout(RESET_TIMEOUT_ID, this->duration_, [this]() { this->output_->turn_off(); });
}
} // namespace esphome::output
+3 -1
View File
@@ -5,6 +5,8 @@ namespace esphome::pid {
ESPHOME_LOG_TAG(TAG, "pid.climate");
static constexpr uint32_t AUTOTUNE_PROGRESS_INTERVAL_ID = 0;
bool PIDClimate::set_deadband_thresholds(float threshold_low, float threshold_high) {
if (threshold_low > threshold_high) {
ESP_LOGW(TAG, "Deadband threshold low %.2f must not be greater than high %.2f", threshold_low, threshold_high);
@@ -191,7 +193,7 @@ void PIDClimate::start_autotune(std::unique_ptr<PIDAutotuner> &&autotune) {
"Until your sensor provides a reading, the autotuner may display \'nan\'",
LOG_STR_ARG(this->get_log_name()));
this->set_interval("autotune-progress", 10000, [this]() {
this->set_interval(AUTOTUNE_PROGRESS_INTERVAL_ID, 10000, [this]() {
if (this->autotuner_ != nullptr && !this->autotuner_->is_finished())
this->autotuner_->dump_config();
});
@@ -5,6 +5,8 @@ namespace esphome::power_supply {
ESPHOME_LOG_TAG(TAG, "power_supply");
static constexpr uint32_t POWER_OFF_TIMEOUT_ID = 0;
void PowerSupply::setup() {
this->pin_->setup();
this->pin_->digital_write(false);
@@ -31,7 +33,7 @@ bool PowerSupply::is_enabled() const { return this->active_requests_ != 0; }
void PowerSupply::request_high_power() {
if (this->active_requests_ == 0) {
this->cancel_timeout("power-supply-off");
this->cancel_timeout(POWER_OFF_TIMEOUT_ID);
ESP_LOGV(TAG, "Enabling");
this->pin_->digital_write(true);
delay(this->enable_time_);
@@ -46,7 +48,7 @@ void PowerSupply::unrequest_high_power() {
}
this->active_requests_--;
if (this->active_requests_ == 0) {
this->set_timeout("power-supply-off", this->keep_on_time_, [this]() {
this->set_timeout(POWER_OFF_TIMEOUT_ID, this->keep_on_time_, [this]() {
ESP_LOGV(TAG, "Disabling");
this->pin_->digital_write(false);
});
@@ -9,6 +9,8 @@ namespace esphome::pvvx_mithermometer {
ESPHOME_LOG_TAG(TAG, "display.pvvx_mithermometer");
static constexpr uint32_t DISCONNECT_TIMEOUT_ID = 0;
void PVVXDisplay::dump_config() {
char service_buf[ble_device_base::UUID_STR_LEN];
char char_buf[ble_device_base::UUID_STR_LEN];
@@ -39,7 +41,7 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t
case ESP_GATTC_DISCONNECT_EVT:
ESP_LOGV(TAG, "[%s] Disconnected", this->parent_->address_str());
this->connection_established_ = false;
this->cancel_timeout("disconnect");
this->cancel_timeout(DISCONNECT_TIMEOUT_ID);
this->char_handle_ = 0;
break;
case ESP_GATTC_SEARCH_CMPL_EVT: {
@@ -147,8 +149,8 @@ void PVVXDisplay::send_to_setup_char_(uint8_t *blk, size_t size) {
void PVVXDisplay::delayed_disconnect_() {
if (this->disconnect_delay_ms_ == 0)
return;
this->cancel_timeout("disconnect");
this->set_timeout("disconnect", this->disconnect_delay_ms_, [this]() { this->parent_->set_enabled(false); });
this->cancel_timeout(DISCONNECT_TIMEOUT_ID);
this->set_timeout(DISCONNECT_TIMEOUT_ID, this->disconnect_delay_ms_, [this]() { this->parent_->set_enabled(false); });
}
void PVVXDisplay::sync_time_and_display_() {
+57 -12
View File
@@ -1019,6 +1019,43 @@ async def nec_action(var, config, args):
cg.add(var.set_command_repeats(template_))
# Onkyo RI
OnkyoRIData, OnkyoRIBinarySensor, OnkyoRITrigger, OnkyoRIAction, OnkyoRIDumper = (
declare_protocol("OnkyoRI")
)
ONKYORI_SCHEMA = cv.Schema({cv.Required(CONF_DATA): cv.hex_int_range(0, 0xFFF)})
@register_binary_sensor("onkyori", OnkyoRIBinarySensor, ONKYORI_SCHEMA)
def onkyori_binary_sensor(var: MockObj, config: ConfigType) -> None:
cg.add(
var.set_data(
cg.StructInitializer(
OnkyoRIData,
("data", config[CONF_DATA]),
)
)
)
@register_trigger("onkyori", OnkyoRITrigger, OnkyoRIData)
def onkyori_trigger(var: MockObj, config: ConfigType) -> None:
"""The trigger takes no options beyond the automation."""
@register_dumper("onkyori", OnkyoRIDumper)
def onkyori_dumper(var: MockObj, config: ConfigType) -> None:
"""The dumper takes no options."""
@register_action("onkyori", OnkyoRIAction, ONKYORI_SCHEMA)
async def onkyori_action(
var: MockObj, config: ConfigType, args: TemplateArgsType
) -> None:
template_ = await cg.templatable(config[CONF_DATA], args, cg.uint16)
cg.add(var.set_data(template_))
# Pioneer
(
PioneerData,
@@ -2061,11 +2098,20 @@ def midea_dumper(var, config):
pass
async def _code_bytes(
value: Any, args: TemplateArgsType, set_template: MockObj, set_static: MockObj
) -> None:
"""Constant codes share one PROGMEM table pool across the remote protocols."""
await automation.templatable_bytes(
value, args, set_template, set_static, "remote_base_code"
)
@register_action("midea", MideaAction, MIDEA_SCHEMA)
async def midea_action(var, config, args):
vec_ = cg.std_vector.template(cg.uint8)
template_ = await cg.templatable(config[CONF_CODE], args, vec_, vec_)
cg.add(var.set_code(template_))
await _code_bytes(
config[CONF_CODE], args, var.set_code_template, var.set_code_static
)
# AEHA
@@ -2117,10 +2163,9 @@ def aeha_dumper(var, config):
async def aeha_action(var, config, args):
template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint16)
cg.add(var.set_address(template_))
template_ = await cg.templatable(
config[CONF_DATA], args, cg.std_vector.template(cg.uint8)
await _code_bytes(
config[CONF_DATA], args, var.set_data_template, var.set_data_static
)
cg.add(var.set_data(template_))
templ = await cg.templatable(config[CONF_CARRIER_FREQUENCY], args, cg.uint32)
cg.add(var.set_carrier_frequency(templ))
@@ -2216,9 +2261,9 @@ def haier_dumper(var, config):
@register_action("haier", HaierAction, HAIER_SCHEMA)
async def haier_action(var, config, args):
vec_ = cg.std_vector.template(cg.uint8)
template_ = await cg.templatable(config[CONF_CODE], args, vec_, vec_)
cg.add(var.set_code(template_))
await _code_bytes(
config[CONF_CODE], args, var.set_code_template, var.set_code_static
)
# ABBWelcome
@@ -2359,9 +2404,9 @@ def mirage_dumper(var, config):
@register_action("mirage", MirageAction, MIRAGE_SCHEMA)
async def mirage_action(var, config, args):
vec_ = cg.std_vector.template(cg.uint8)
template_ = await cg.templatable(config[CONF_CODE], args, vec_, vec_)
cg.add(var.set_code(template_))
await _code_bytes(
config[CONF_CODE], args, var.set_code_template, var.set_code_static
)
# Toto
@@ -28,10 +28,9 @@ DECLARE_REMOTE_PROTOCOL(AEHA)
template<typename... Ts> class AEHAAction : public RemoteTransmitterActionBase<Ts...> {
public:
TEMPLATABLE_VALUE(uint16_t, address)
TEMPLATABLE_VALUE(std::vector<uint8_t>, data)
TEMPLATABLE_BYTES(data)
TEMPLATABLE_VALUE(uint32_t, carrier_frequency);
void set_data(const std::vector<uint8_t> &data) { data_ = data; }
void encode(RemoteTransmitData *dst, Ts... x) override {
AEHAData data{};
data.address = this->address_.value(x...);
@@ -25,7 +25,7 @@ DECLARE_REMOTE_PROTOCOL(Haier)
template<typename... Ts> class HaierAction : public RemoteTransmitterActionBase<Ts...> {
public:
TEMPLATABLE_VALUE(std::vector<uint8_t>, code)
TEMPLATABLE_BYTES(code)
void encode(RemoteTransmitData *dst, Ts... x) override {
HaierData data{};
@@ -4,6 +4,7 @@
#include <vector>
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "remote_base.h"
@@ -21,6 +22,9 @@ class MideaData {
MideaData(const std::vector<uint8_t> &data) {
std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin());
}
MideaData(const uint8_t *data, size_t len) {
progmem_memcpy(this->data_.data(), data, std::min(len, this->data_.size()));
}
uint8_t *data() { return this->data_.data(); }
const uint8_t *data() const { return this->data_.data(); }
@@ -75,10 +79,12 @@ class MideaProtocol : public RemoteProtocol<MideaData> {
DECLARE_REMOTE_PROTOCOL(Midea)
template<typename... Ts> class MideaAction : public RemoteTransmitterActionBase<Ts...> {
TEMPLATABLE_VALUE(std::vector<uint8_t>, code)
public:
TEMPLATABLE_BYTES(code)
void encode(RemoteTransmitData *dst, Ts... x) override {
MideaData data(this->code_.value(x...));
MideaData data = this->code_.is_static() ? MideaData(this->code_.data(), this->code_.size())
: MideaData(this->code_.value(x...));
data.finalize();
MideaProtocol().encode(dst, data);
}
@@ -25,7 +25,7 @@ DECLARE_REMOTE_PROTOCOL(Mirage)
template<typename... Ts> class MirageAction : public RemoteTransmitterActionBase<Ts...> {
public:
TEMPLATABLE_VALUE(std::vector<uint8_t>, code)
TEMPLATABLE_BYTES(code)
void encode(RemoteTransmitData *dst, Ts... x) override {
MirageData data{};
@@ -0,0 +1,59 @@
#include "onkyori_protocol.h"
#include "esphome/core/log.h"
namespace esphome::remote_base {
ESPHOME_LOG_TAG(TAG, "remote.onkyori");
static constexpr uint8_t NBITS = 12;
static constexpr uint16_t MAX_DATA = (1U << NBITS) - 1;
static constexpr uint32_t HEADER_HIGH_US = 3000;
static constexpr uint32_t HEADER_LOW_US = 1000;
static constexpr uint32_t BIT_HIGH_US = 1000;
static constexpr uint32_t BIT_ONE_LOW_US = 2000;
static constexpr uint32_t BIT_ZERO_LOW_US = 1000;
static constexpr uint32_t TRAILER_HIGH_US = 1000;
void OnkyoRIProtocol::encode(RemoteTransmitData *dst, const OnkyoRIData &data) {
if (data.data > MAX_DATA) {
ESP_LOGE(TAG, "Data 0x%04X does not fit in a 12-bit RI frame, not sending", data.data);
return;
}
// RI is a baseband signal on a wire, so it has no carrier to modulate.
dst->set_carrier_frequency(0);
dst->reserve(2 + NBITS * 2u + 1);
dst->item(HEADER_HIGH_US, HEADER_LOW_US);
for (uint32_t mask = 1UL << (NBITS - 1); mask != 0; mask >>= 1) {
dst->item(BIT_HIGH_US, (data.data & mask) != 0 ? BIT_ONE_LOW_US : BIT_ZERO_LOW_US);
}
dst->mark(TRAILER_HIGH_US);
}
optional<OnkyoRIData> OnkyoRIProtocol::decode(RemoteReceiveData src) {
OnkyoRIData out{.data = 0};
if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US))
return {};
for (uint8_t i = 0; i < NBITS; i++) {
out.data <<= 1;
if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) {
out.data |= 1;
} else if (!src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) {
return {};
}
}
if (!src.expect_mark(TRAILER_HIGH_US))
return {};
return out;
}
void OnkyoRIProtocol::dump(const OnkyoRIData &data) { ESP_LOGI(TAG, "Received OnkyoRI: data=0x%03X", data.data); }
} // namespace esphome::remote_base
@@ -0,0 +1,33 @@
#pragma once
#include "remote_base.h"
namespace esphome::remote_base {
struct OnkyoRIData {
uint16_t data;
bool operator==(const OnkyoRIData &rhs) const { return data == rhs.data; }
};
class OnkyoRIProtocol : public RemoteProtocol<OnkyoRIData> {
public:
void encode(RemoteTransmitData *dst, const OnkyoRIData &data);
optional<OnkyoRIData> decode(RemoteReceiveData src);
void dump(const OnkyoRIData &data);
};
DECLARE_REMOTE_PROTOCOL(OnkyoRI)
template<typename... Ts> class OnkyoRIAction : public RemoteTransmitterActionBase<Ts...> {
public:
TEMPLATABLE_VALUE(uint16_t, data)
void encode(RemoteTransmitData *dst, Ts... x) override {
OnkyoRIData data{};
data.data = this->data_.value(x...);
OnkyoRIProtocol().encode(dst, data);
}
};
} // namespace esphome::remote_base
@@ -11,6 +11,8 @@ namespace esphome::remote_transmitter {
ESPHOME_LOG_TAG(TAG, "remote_transmitter");
static constexpr uint32_t COMPLETE_TIMEOUT_ID = 0;
// Maximum RMT symbol duration (15-bit field)
static constexpr uint32_t RMT_SYMBOL_DURATION_MAX = 0x7FFF;
@@ -206,7 +208,7 @@ void RemoteTransmitterComponent::wait_for_rmt_() {
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
void RemoteTransmitterComponent::flush_pending_completion() {
// a frame still on the wire is waited out, and its completion reported, before the next one
if (this->non_blocking_ && this->cancel_timeout("complete")) {
if (this->non_blocking_ && this->cancel_timeout(COMPLETE_TIMEOUT_ID)) {
this->wait_for_rmt_();
}
}
@@ -292,7 +294,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
this->status_clear_warning();
if (this->non_blocking_) {
this->set_timeout("complete", total_duration / 1000, [this]() { this->wait_for_rmt_(); });
this->set_timeout(COMPLETE_TIMEOUT_ID, total_duration / 1000, [this]() { this->wait_for_rmt_(); });
} else {
this->wait_for_rmt_();
}
+5
View File
@@ -4,6 +4,8 @@
#include <cstdint>
#include <pico/platform.h>
#include "esphome/core/time_conversion.h"
#define IRAM_ATTR __attribute__((noinline, long_call, section(".time_critical")))
@@ -40,6 +42,9 @@ __attribute__((always_inline)) inline bool in_isr_context() {
return ipsr != 0;
}
/// arduino-pico runs setup() and loop() on core 0; ESPHome never uses core 1.
__attribute__((always_inline)) inline bool is_main_loop_thread() { return !in_isr_context() && get_core_num() == 0; }
__attribute__((always_inline)) inline void yield() { ::yield(); }
__attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); }
__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(::micros()); }
+3 -1
View File
@@ -10,6 +10,8 @@ namespace esphome::scd30 {
ESPHOME_LOG_TAG(TAG, "scd30");
static constexpr uint32_t STATUS_CHECK_INTERVAL_ID = 0;
static const uint16_t SCD30_CMD_GET_FIRMWARE_VERSION = 0xd100;
static const uint16_t SCD30_CMD_START_CONTINUOUS_MEASUREMENTS = 0x0010;
static const uint16_t SCD30_CMD_ALTITUDE_COMPENSATION = 0x5102;
@@ -107,7 +109,7 @@ void SCD30Component::setup() {
}
// check each 500ms if data is ready, and read it in that case
this->set_interval("status-check", 500, [this]() {
this->set_interval(STATUS_CHECK_INTERVAL_ID, 500, [this]() {
if (this->is_data_ready_())
this->update();
});
@@ -10,6 +10,8 @@ namespace esphome::sendspin_ {
ESPHOME_LOG_TAG(TAG, "sendspin.image");
static constexpr uint32_t TRANSITION_ACK_TIMEOUT_ID = 0;
// How long a displayed frame may wait for sendspin.image.transition_finished before a warning
// names the missing ack. Generous next to a typical fade of a second or two.
static constexpr uint32_t TRANSITION_ACK_WARNING_MS = 10000;
@@ -163,7 +165,7 @@ void SendspinImageSlot::on_display_(uint32_t lateness_ms) {
// that never reaches the action stalls the slot with nothing in the log. Name the cause after
// a generous wait. Arming again replaces the previous timeout, so it cannot fire for a frame
// that was already acked and superseded.
this->set_timeout("transition_ack", TRANSITION_ACK_WARNING_MS, [this]() {
this->set_timeout(TRANSITION_ACK_TIMEOUT_ID, TRANSITION_ACK_WARNING_MS, [this]() {
if (this->transition_pending_) {
ESP_LOGW(TAG,
"Slot %u: displayed artwork was never acknowledged; no new artwork will arrive until "
+55 -2
View File
@@ -1,5 +1,6 @@
import logging
import math
from typing import Any
from esphome import automation
import esphome.codegen as cg
@@ -43,6 +44,7 @@ from esphome.const import (
CONF_TIMEOUT,
CONF_TO,
CONF_TRIGGER_ID,
CONF_TYPE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE,
CONF_WEB_SERVER,
@@ -222,6 +224,13 @@ def validate_send_first_at(value):
FILTER_REGISTRY = Registry()
validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
# Default for streamed readings (radar sensors): hold the last value for a second so a dropped
# frame doesn't read as absence, then rate limit. Codegen turns the pair into one TimeoutThrottleFilter.
TIMEOUT_THROTTLE_FILTERS = [
{"timeout": {"timeout": cv.TimePeriod(milliseconds=1000), "value": "last"}},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
]
def validate_datapoint(value):
if isinstance(value, dict):
@@ -293,6 +302,7 @@ ThrottleWithPriorityNanFilter = sensor_ns.class_(
TimeoutFilterBase = sensor_ns.class_("TimeoutFilterBase", Filter, cg.Component)
TimeoutFilterLast = sensor_ns.class_("TimeoutFilterLast", TimeoutFilterBase)
TimeoutFilterConfigured = sensor_ns.class_("TimeoutFilterConfigured", TimeoutFilterBase)
TimeoutThrottleFilter = sensor_ns.class_("TimeoutThrottleFilter", TimeoutFilterLast)
DebounceFilter = sensor_ns.class_("DebounceFilter", Filter)
HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter)
DeltaFilter = sensor_ns.class_("DeltaFilter", Filter)
@@ -683,6 +693,12 @@ THROTTLE_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value(
)
def _is_nan_only(values: Any) -> bool:
if not isinstance(values, list):
values = [values]
return bool(values) and all(isinstance(v, float) and math.isnan(v) for v in values)
@FILTER_REGISTRY.register(
"throttle_with_priority",
ThrottleWithPriorityFilter,
@@ -696,7 +712,7 @@ async def throttle_with_priority_filter_to_code(config, filter_id):
# omits `value:`) to avoid the TemplatableFn<float> array + NaN lambda the
# generic ValueListFilter path requires. Behavior is identical: NaN sensor
# readings always bypass the throttle.
if values and all(isinstance(v, float) and math.isnan(v) for v in values):
if _is_nan_only(values):
filter_id = filter_id.copy()
filter_id.type = ThrottleWithPriorityNanFilter
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT])
@@ -941,6 +957,43 @@ async def build_filters(config):
return await cg.build_registry_list(FILTER_REGISTRY, config)
def _timeout_throttle_period(
first: ConfigType, second: ConfigType
) -> cv.TimePeriod | None:
"""Period of a `timeout` (value `last`) directly followed by a NaN only `throttle_with_priority`."""
timeout = first.get("timeout")
throttle = second.get("throttle_with_priority")
if not isinstance(timeout, dict) or not isinstance(throttle, dict):
return None
if timeout[CONF_VALUE] != "last" or timeout[CONF_TIMEOUT] != throttle[CONF_TIMEOUT]:
return None
if not _is_nan_only(throttle[CONF_VALUE]):
return None
return timeout[CONF_TIMEOUT]
async def _build_chain_filters(config: list[ConfigType]) -> list:
"""Like build_filters, but merges the radar `timeout` + `throttle_with_priority` pair into one filter."""
filters = []
i = 0
while i < len(config):
if (
i + 1 < len(config)
and (period := _timeout_throttle_period(config[i], config[i + 1]))
is not None
):
filter_id = config[i][CONF_TYPE_ID].copy()
filter_id.type = TimeoutThrottleFilter
var = cg.new_Pvariable(filter_id, period)
await cg.register_component(var, {})
filters.append(var)
i += 2
continue
filters.append(await cg.build_registry_entry(FILTER_REGISTRY, config[i]))
i += 1
return filters
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_VALUE, "add_on_state_callback", [(float, "x")]
@@ -979,7 +1032,7 @@ async def setup_sensor_core_(var, config):
cg.add(var.set_force_update(True))
if config.get(CONF_FILTERS): # must exist and not be empty
cg.add_define("USE_SENSOR_FILTER")
filters = await build_filters(config[CONF_FILTERS])
filters = await _build_chain_filters(config[CONF_FILTERS])
cg.add(var.set_filters(filters))
CORE.add_job(_build_sensor_automations, var, config)
+29 -4
View File
@@ -13,6 +13,17 @@ namespace esphome::sensor {
ESPHOME_LOG_TAG(TAG, "sensor.filter");
/// Shared pass check for throttle_with_priority (NaN only): passes and stamps `last_input` when the
/// period has elapsed, on the first value, or for NaN.
static bool throttle_nan_passes(uint32_t &last_input, uint32_t period, float value) {
const uint32_t now = App.get_loop_component_start_time();
if (last_input == 0 || now - last_input >= period || std::isnan(value)) {
last_input = now;
return true;
}
return false;
}
// Filter
void Filter::input(float value) {
ESP_LOGVV(TAG, "Filter(%p)::input(%f)", this, value);
@@ -266,11 +277,8 @@ optional<float> throttle_with_priority_new_value(Sensor *parent, float value, co
ThrottleWithPriorityNanFilter::ThrottleWithPriorityNanFilter(uint32_t min_time_between_inputs)
: min_time_between_inputs_(min_time_between_inputs) {}
optional<float> ThrottleWithPriorityNanFilter::new_value(float value) {
const uint32_t now = App.get_loop_component_start_time();
if (this->last_input_ == 0 || now - this->last_input_ >= this->min_time_between_inputs_ || std::isnan(value)) {
this->last_input_ = now;
if (throttle_nan_passes(this->last_input_, this->min_time_between_inputs_, value))
return value;
}
return {};
}
@@ -355,6 +363,23 @@ optional<float> TimeoutFilterConfigured::new_value(float value) {
return value;
}
// TimeoutThrottleFilter
optional<float> TimeoutThrottleFilter::new_value(float value) {
TimeoutFilterLast::new_value(value);
if (throttle_nan_passes(this->last_input_, this->time_period_, value))
return value;
return {};
}
void TimeoutThrottleFilter::loop() {
const uint32_t now = App.get_loop_component_start_time();
if (now - this->timeout_start_time_ >= this->time_period_) {
if (throttle_nan_passes(this->last_input_, this->time_period_, this->pending_value_))
this->output(this->pending_value_);
this->disable_loop();
}
}
// DebounceFilter
optional<float> DebounceFilter::new_value(float value) {
App.scheduler.set_timeout(this, this->time_period_, [this, value]() { this->output(value); });
+13
View File
@@ -479,6 +479,19 @@ class TimeoutFilterConfigured : public TimeoutFilterBase {
// Total: 8 (base) + 4 = 12 bytes + vtable ptr + Component overhead
};
/// Codegen use only: `timeout` (value `last`) followed by `throttle_with_priority` (NaN only) with the
/// same period, as one object. Output matches the two filter chain exactly.
class TimeoutThrottleFilter : public TimeoutFilterLast {
public:
explicit TimeoutThrottleFilter(uint32_t time_period) : TimeoutFilterLast(time_period) {}
optional<float> new_value(float value) override;
void loop() override;
protected:
uint32_t last_input_{0}; // When the throttle stage last let a value through
};
class DebounceFilter : public Filter {
public:
explicit DebounceFilter(uint32_t time_period);
+3 -1
View File
@@ -8,6 +8,8 @@ namespace esphome::spa06_base {
ESPHOME_LOG_TAG(TAG, "spa06");
static constexpr uint32_t MEASUREMENT_TIMEOUT_ID = 0;
// Sign extension function for <=16 bit types
inline int16_t decode16(uint8_t msb, uint8_t lsb, size_t bits, size_t head = 0) {
return static_cast<int16_t>(encode_uint16(msb, lsb) << head) >> (16 - bits);
@@ -243,7 +245,7 @@ void SPA06Component::update() {
}
// Queue a background task for retrieving the measurement
this->set_timeout("measurement", this->conversion_time_, [this]() {
this->set_timeout(MEASUREMENT_TIMEOUT_ID, this->conversion_time_, [this]() {
float raw_temperature;
float temperature = 0.0;
float pressure = 0.0;
@@ -52,6 +52,11 @@ static const UBaseType_t ANNOUNCEMENT_PIPELINE_TASK_PRIORITY = 1;
ESPHOME_LOG_TAG(TAG, "speaker_media_player");
static constexpr uint32_t UNPAUSE_MEDIA_INTERVAL_ID = 0;
static constexpr uint32_t NEXT_ANNOUNCEMENT_TIMEOUT_ID = 1;
static constexpr uint32_t NEXT_MEDIA_TIMEOUT_ID = 2;
static constexpr uint32_t UNPAUSE_ANNOUNCEMENT_INTERVAL_ID = 3;
void SpeakerMediaPlayer::setup() {
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
state = media_player::MEDIA_PLAYER_STATE_OFF;
@@ -112,13 +117,13 @@ void SpeakerMediaPlayer::set_playlist_delay_ms(AudioPipelineType pipeline_type,
void SpeakerMediaPlayer::stop_and_unpause_media_() {
this->media_pipeline_->stop();
this->unpause_media_remaining_ = 3;
this->set_interval("unpause_med", 50, [this]() {
this->set_interval(UNPAUSE_MEDIA_INTERVAL_ID, 50, [this]() {
if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) {
this->cancel_interval("unpause_med");
this->cancel_interval(UNPAUSE_MEDIA_INTERVAL_ID);
this->media_pipeline_->set_pause_state(false);
this->is_paused_ = false;
} else if (--this->unpause_media_remaining_ == 0) {
this->cancel_interval("unpause_med");
this->cancel_interval(UNPAUSE_MEDIA_INTERVAL_ID);
}
});
}
@@ -152,7 +157,7 @@ void SpeakerMediaPlayer::watch_media_commands_() {
if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
if (!enqueue) {
// Ensure the loaded next item doesn't start playing, clear the queue, start the file, and unpause
this->cancel_timeout("next_ann");
this->cancel_timeout(NEXT_ANNOUNCEMENT_TIMEOUT_ID);
this->announcement_playlist_.clear();
this->announcement_item_failed_ = false;
if (media_command.file.has_value()) {
@@ -166,7 +171,7 @@ void SpeakerMediaPlayer::watch_media_commands_() {
} else {
if (!enqueue) {
// Ensure the loaded next item doesn't start playing, clear the queue, start the file, and unpause
this->cancel_timeout("next_media");
this->cancel_timeout(NEXT_MEDIA_TIMEOUT_ID);
this->media_playlist_.clear();
this->media_item_failed_ = false;
if (this->is_paused_) {
@@ -236,23 +241,23 @@ void SpeakerMediaPlayer::watch_media_commands_() {
if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
#endif
if (this->announcement_pipeline_ != nullptr) {
this->cancel_timeout("next_ann");
this->cancel_timeout(NEXT_ANNOUNCEMENT_TIMEOUT_ID);
this->announcement_playlist_.clear();
this->announcement_item_failed_ = false;
this->announcement_pipeline_->stop();
this->unpause_announcement_remaining_ = 3;
this->set_interval("unpause_ann", 50, [this]() {
this->set_interval(UNPAUSE_ANNOUNCEMENT_INTERVAL_ID, 50, [this]() {
if (this->announcement_pipeline_state_ == AudioPipelineState::STOPPED) {
this->cancel_interval("unpause_ann");
this->cancel_interval(UNPAUSE_ANNOUNCEMENT_INTERVAL_ID);
this->announcement_pipeline_->set_pause_state(false);
} else if (--this->unpause_announcement_remaining_ == 0) {
this->cancel_interval("unpause_ann");
this->cancel_interval(UNPAUSE_ANNOUNCEMENT_INTERVAL_ID);
}
});
}
} else {
if (this->media_pipeline_ != nullptr) {
this->cancel_timeout("next_media");
this->cancel_timeout(NEXT_MEDIA_TIMEOUT_ID);
this->media_playlist_.clear();
this->media_item_failed_ = false;
this->stop_and_unpause_media_();
@@ -408,7 +413,8 @@ void SpeakerMediaPlayer::loop() {
this->announcement_pipeline_->set_pause_state(true);
// Internally unpause the pipeline after the delay between playlist items. Announcements do not follow the
// media player's pause state.
this->set_timeout("next_ann", timeout_ms, [this]() { this->announcement_pipeline_->set_pause_state(false); });
this->set_timeout(NEXT_ANNOUNCEMENT_TIMEOUT_ID, timeout_ms,
[this]() { this->announcement_pipeline_->set_pause_state(false); });
}
}
} else {
@@ -454,7 +460,7 @@ void SpeakerMediaPlayer::loop() {
this->media_pipeline_->set_pause_state(true);
// Internally unpause the pipeline after the delay between playlist items, if the media player state is
// not paused.
this->set_timeout("next_media", timeout_ms,
this->set_timeout(NEXT_MEDIA_TIMEOUT_ID, timeout_ms,
[this]() { this->media_pipeline_->set_pause_state(this->is_paused_); });
}
}
+7 -9
View File
@@ -322,13 +322,11 @@ optional<size_t> SprinklerValveRunRequest::valve_as_opt() {
SprinklerValveOperator *SprinklerValveRunRequest::valve_operator() { return this->valve_op_; }
Sprinkler::Sprinkler() : Sprinkler("") {}
Sprinkler::Sprinkler(const char *name) : name_(name) {
// The `name` is stored for dump_config logging
this->timer_.init(2);
// Timer names only need to be unique within this component instance
this->timer_.push_back({"sm", false, 0, 0, [this]() { this->sm_timer_callback_(); }});
this->timer_.push_back({"vs", false, 0, 0, [this]() { this->valve_selection_callback_(); }});
}
// The `name` is stored for dump_config logging; the scheduler keys each timer by its SprinklerTimerIndex
Sprinkler::Sprinkler(const char *name)
: name_(name),
timer_{{{false, 0, 0, [this]() { this->sm_timer_callback_(); }},
{false, 0, 0, [this]() { this->valve_selection_callback_(); }}}} {}
void Sprinkler::setup() {
this->all_valves_off_(true);
@@ -1520,7 +1518,7 @@ const LogString *Sprinkler::state_as_str_(SprinklerState state) {
void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) {
if (this->timer_duration_(timer_index) > 0) {
this->set_timeout(this->timer_[timer_index].name, this->timer_duration_(timer_index),
this->set_timeout(static_cast<uint32_t>(timer_index), this->timer_duration_(timer_index),
this->timer_cbf_(timer_index));
this->timer_[timer_index].start_time = millis();
this->timer_[timer_index].active = true;
@@ -1531,7 +1529,7 @@ void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) {
bool Sprinkler::cancel_timer_(const SprinklerTimerIndex timer_index) {
this->timer_[timer_index].active = false;
return this->cancel_timeout(this->timer_[timer_index].name);
return this->cancel_timeout(static_cast<uint32_t>(timer_index));
}
bool Sprinkler::timer_active_(const SprinklerTimerIndex timer_index) { return this->timer_[timer_index].active; }
+3 -3
View File
@@ -7,6 +7,7 @@
#include "esphome/components/number/number.h"
#include "esphome/components/switch/switch.h"
#include <array>
#include <vector>
namespace esphome::sprinkler {
@@ -49,7 +50,6 @@ struct SprinklerQueueItem {
};
struct SprinklerTimer {
const char *name;
bool active;
uint32_t time;
uint32_t start_time;
@@ -567,8 +567,8 @@ class Sprinkler final : public Component {
/// Sprinkler valve operator objects
std::vector<SprinklerValveOperator> valve_op_{2};
/// Valve control timers - FixedVector enforces that this can never grow beyond init() size
FixedVector<SprinklerTimer> timer_;
/// Valve control timers, indexed by SprinklerTimerIndex (also used as the scheduler id)
std::array<SprinklerTimer, 2> timer_;
/// Other Sprinkler instances we should be aware of (used to check if pumps are in use)
std::vector<Sprinkler *> other_controllers_;
@@ -117,8 +117,13 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
var = await alarm_control_panel.new_alarm_control_panel(config)
await cg.register_component(var, config)
if CONF_CODES in config:
cg.add(var.set_codes(config[CONF_CODES]))
# Shared flash table ended by nullptr, so the panel stores only a pointer.
# requires_code_to_arm needs codes (validate_config) and defaults to false.
if codes := config.get(CONF_CODES):
table = cg.shared_progmem_array(
"alarm_codes", cg.const_char_ptr, [*codes, cg.nullptr]
)
cg.add(var.set_codes(table))
if CONF_REQUIRES_CODE_TO_ARM in config:
cg.add(var.set_requires_code_to_arm(config[CONF_REQUIRES_CODE_TO_ARM]))
@@ -36,6 +36,11 @@ static const LogString *sensor_type_to_string(AlarmSensorType type) {
#endif
void TemplateAlarmControlPanel::dump_config() {
size_t num_codes = 0;
if (this->codes_ != nullptr) {
while (this->codes_[num_codes] != nullptr)
num_codes++;
}
ESP_LOGCONFIG(TAG,
"TemplateAlarmControlPanel:\n"
" Current State: %s\n"
@@ -47,8 +52,8 @@ void TemplateAlarmControlPanel::dump_config() {
" Pending Time: %" PRIu32 "s\n"
" Trigger Time: %" PRIu32 "s\n"
" Supported Features: %" PRIu32,
LOG_STR_ARG(alarm_control_panel_state_to_string(this->current_state_)), this->codes_.size(),
YESNO(!this->codes_.empty() && this->requires_code_to_arm_), (this->arming_away_time_ / 1000),
LOG_STR_ARG(alarm_control_panel_state_to_string(this->current_state_)), num_codes,
YESNO(this->codes_ != nullptr && this->requires_code_to_arm_), (this->arming_away_time_ / 1000),
(this->arming_home_time_ / 1000), (this->arming_night_time_ / 1000), (this->pending_time_ / 1000),
(this->trigger_time_ / 1000), this->get_supported_features());
#ifdef USE_BINARY_SENSOR
@@ -195,13 +200,12 @@ void TemplateAlarmControlPanel::loop() {
}
bool TemplateAlarmControlPanel::is_code_valid_(optional<std::string> code) {
if (!this->codes_.empty()) {
if (this->codes_ != nullptr) {
if (code.has_value()) {
ESP_LOGVV(TAG, "Checking code: %s", code.value().c_str());
// Use strcmp for const char* comparison
const char *code_cstr = code.value().c_str();
for (const char *stored_code : this->codes_) {
if (strcmp(stored_code, code_cstr) == 0)
for (const char *const *stored = this->codes_; *stored != nullptr; stored++) {
if (strcmp(*stored, code_cstr) == 0)
return true;
}
return false;

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