Compare commits

..
Author SHA1 Message Date
J. Nick Koston 4770f4a82c Locate a while warning inside a list condition and restore App state in its test
A list condition is wrapped in an and: with no place in the file, so the
warning now names and locates the first condition in it, and leaves the
location out rather than pointing at the main file. The shutdown test
restores App's state bits, so later tests still yield.
2026-11-09 00:56:50 -08:00
J. Nick Koston 932f468ddb Keep shutdown as an app state bit and give the while warning its file and line
Shutdown is bit 7 of App.app_state_, set by reboot() and
run_safe_shutdown_hooks(), so every shutdown path stops while yields at
no RAM cost instead of a flag set by ShutdownTrigger. The warning gives
the file, line and column of the loop's condition. The wait check and
the registry key lookup are shared.
2026-11-09 00:43:24 -08:00
J. Nick Koston 60e7d78092 Run a while to its end during shutdown and name the loop in the warning
Nothing runs a timeout once shutdown started, so a while in on_shutdown
that yielded never finished and the actions after it were dropped; it
now runs to its end, as on dev. The warning names the condition and
the component of the loop it means.
2026-11-09 00:38:08 -08:00
J. Nick Koston 90ee1626c1 Restore the while tests for runs without arguments
The trim of the one argument hand-off test also removed the tests for a
wait_until timeout releasing another run inside a pass and for two ends
in one pass, which cover runs without arguments.
2026-11-08 19:26:56 -08:00
J. Nick Koston 61f8e675cd Count a lambda action in a while body only by the ids it names
A lambda action no longer turns the warning off on its own; like other
actions it counts when it names an id the condition reads, such as a
counter both use.
2026-11-08 19:22:10 -08:00
J. Nick Koston e920203064 Walk a while body once for the warning and trim comments and tests
The warning walks the body's actions once for both the wait and the
lambda check, the id filter is shared, and the message is shorter. The
C++ test for one argument is covered by the mixed argument test, and
warning cases that repeated another shape are gone. The climate test
loop uses a lambda body, so it does not warn.
2026-11-08 19:04:07 -08:00
J. Nick Koston 6938ef121a Skip condition trees and unassigned ids in the while warning
The action walker no longer descends into an if's any: or all: or a
choice's if:, which hold conditions. Ids generated later are still None
during validation, so they no longer all match each other.
2026-11-08 18:45:20 -08:00
J. Nick Koston a2b5b61acf Say in the while warning that a body changing the condition is fine
The check cannot see a change made through another script or a later
state update, so the message no longer claims the loop holds up the
device in every case.
2026-11-08 18:03:35 -08:00
J. Nick Koston dc8eb68026 Skip conditions when walking a body's actions
A condition holds no actions, but its and: or or: holds a list, which
was walked as an action list, so a lambda condition there turned the
while warning off.
2026-11-08 17:52:03 -08:00
J. Nick Koston 6544007c7d Count only actions of the body for the while warning
The warning walks the body's action lists: only a lambda action, or an
action on an id the condition reads, turns it off. A logger.log reading
the condition's entity or a lambda condition inside the body only reads.
has_non_synchronous_actions uses the same action walker.
2026-11-08 17:40:25 -08:00
J. Nick Koston 1674121bfe Keep the while body marker per action and key runs on all arguments
Automations can run on other tasks (microphone on_data), so one marker
shared by every while could be overwritten by another task's loop; each
WhileAction now keeps its own. A run is told apart by the storage of
all its arguments, as a wait keeps an entity by reference and copies
the rest. Only a lambda action in the body turns the warning off;
templated values such as logger.log args only read.
2026-11-08 17:28:09 -08:00
J. Nick Koston cb229c509c Warn for a lambda condition the body cannot change
A lambda in the condition no longer turns the warning off on its own.
The warning is skipped only when the body has a lambda, which may change
anything, or acts on an id the condition or its lambda reads.
2026-11-08 17:08:30 -08:00
J. Nick Koston 0a7b302663 Share the while yield callback and drop leftovers from number arguments
Both while actions yield through one non-template ActionTimer helper that
resumes them through play(), skipping the cancel scan a yielding run never
needs. ApplyWhileAction keeps only play() per step function. The tests
drop the cases written for yielding with number arguments.
2026-11-08 17:05:14 -08:00
J. Nick Koston 873e0d8fe1 Yield only from a while without trigger arguments again
Keeping a WhileAction for number arguments turned an inline repeat
around a while back into a recursing RepeatAction, missed number types
the Python check did not know, and allocated a closure per yield with
two or more arguments. A while with any arguments loops in place again,
and an all inline body stays one C++ loop.

The public Scheduler::set_timeout(self) no longer records the running
script as its source, ApplyWhileActionBase checks the num_running_ type
again, and base_automation.h includes <memory> for std::addressof.
2026-11-08 16:58:40 -08:00
J. Nick Koston 9162cbc25a Continue a second while body end in one pass, and fix the clang-tidy name
When one pass of a while without arguments both releases another run's
wait and ends itself, the second end now loops on its own instead of
being lost. args_key is a static method, so it has no trailing
underscore.
2026-11-08 11:09:38 -08:00
J. Nick Koston a0f952f0c3 Pin the no-argument while runs and match number arguments without qualifiers
A wait_until timeout of one run can end inside another run's pass; for a
while without arguments the frame that takes that end loops once for
it, so every run still continues once. A test with a real wait_until
timeout covers it. The Python check for number arguments now ignores
const and reference, like WHILE_CAN_YIELD.
2026-11-08 11:02:39 -08:00
pre-commit-ci-lite[bot] 85b5a2321a apply automatic formatting fixes 2026-11-08 18:54:20 +00:00
J. Nick Koston cae0d26d82 Tie the while body marker to the run and yield with number arguments
A wait of another run that ends synchronously inside this run's body is
now told apart by the address of its arguments, instead of being taken
for the end of this body. A while whose arguments are all numbers, such
as a repeat iteration or a script parameter, keeps its WhileAction and
yields too, copying them.

The step function returns false when its last action stopped the run,
an empty body evaluates its condition once again, the warning skips a
body that acts on the condition's entity, and the timer helper is
private to the actions that use it. Scheduler::set_timeout(self) reuses
set_action_timeout_. The object loop tests now use a body with its own
action object.
2026-11-08 10:52:51 -08:00
J. Nick Koston 7a83dd3daa Share one config walker for the lambda and deferring action checks 2026-11-08 12:28:11 -06:00
pre-commit-ci-lite[bot] 79157daf80 apply automatic formatting fixes 2026-11-08 18:21:56 +00:00
J. Nick Koston adba5f12f5 Drop the USE_WHILE_ACTION gate
Builds with and without it are the same size: an unused template and
inline variable produce no code.
2026-11-08 12:19:49 -06:00
J. Nick Koston 742de9c1fc Share the while loop code and simplify the step function
ApplyWhileAction keeps only its play_complex in the template; the loop
lives in a shared base. Both while actions yield through one helper,
the only new scheduler friend. The step function returns false on a
stop between body actions instead of wrapping the body in a lambda,
and the body-ended flag is one global pointer.
2026-11-08 12:14:31 -06:00
J. Nick Koston 1fa3f4ebac Yield only from a while without trigger arguments
Copying the trigger arguments into the yield timeout broke the build for
a while in climate on_state (an abstract Climate &), changed what an
on_control body edits, and could keep views that are only valid while
the trigger runs. A while with arguments now loops in place and never
yields.

A while without arguments whose body is all inline becomes a 12 byte
ApplyWhileAction that runs one generated step function, instead of a
WhileAction with its own condition and body objects. The flag that marks
a body ending synchronously is one global stack frame pointer instead of
a member, and WhileAction is only compiled in when a config uses it.

Adds tests for every while shape.
2026-11-08 12:07:27 -06:00
J. Nick Koston 64fcde1178 Share the action timeout helper and simplify the while loop
DelayAction and WhileAction arm their timeouts through one Scheduler
helper. The while loop checks its condition in one place and keeps the
body-ended flag on the stack. The warning describes the busy loop.
2026-11-08 11:51:38 -06:00
J. Nick Koston 03deae79d0 [core] Let the main loop run when a while body never waits
A while whose body never waits started its body again from inside the
previous run, so each pass used more stack until the device crashed. On
2026.11 an all-inline body became one C++ loop that never returned.

WhileAction now loops instead of recursing and gives the main loop a turn
after 20 ms. Every while keeps its WhileAction, a false start no longer
stops the body of another run, and validation warns when the body never
waits and the condition has no lambda.
2026-11-08 11:46:46 -06:00
106 changed files with 1328 additions and 1323 deletions

No files matched your search

+4 -2
View File
@@ -210,8 +210,10 @@ run, so prefer `register_apply_action` wherever the action allows it. Because of
return fewer objects than the config has entries. A builder that wraps `apply_action_builder` may do other work
first, but must return its result unchanged and not use it. A builder that must generate its own statements, and
is registered with `automation.ApplyAction`, can call `apply_statements_action` and still merge; `script.execute`
is an example. `if`, `choose`, `while` and `repeat` whose bodies are all inline become C++ control flow in the
same function, and `register_apply_condition` and `lambda:` conditions become plain expressions there.
is an example. `if`, `choose`, `repeat` and a `while` with trigger arguments whose bodies are all inline become
C++ control flow in the same function, and `register_apply_condition` and `lambda:` conditions become plain
expressions there. A `while` without trigger arguments becomes an `ApplyWhileAction` instead, which can yield to the
main loop; with arguments a loop never yields.
## Conditions
+1 -7
View File
@@ -3,11 +3,7 @@ name: Stale
on:
schedule:
# Full pass once a day
- cron: "30 0 * * *"
# Conflicts only, every 3 hours in between, so authors hear about new
# merge conflicts the same day
- cron: "30 3-21/3 * * *"
workflow_dispatch:
# The reusable workflow authenticates as the ESPHome GitHub App, so GITHUB_TOKEN
@@ -20,14 +16,12 @@ jobs:
# No GITHUB_TOKEN permissions: the reusable workflow mints an ESPHome
# GitHub App token so the labels, comments and closures come from
# esphome[bot] instead of github-actions[bot].
uses: esphome/workflows/.github/workflows/stale.yml@2026.11.0
uses: esphome/workflows/.github/workflows/stale.yml@e1432c7b31fec7d24a13dc192953ebac836ed98a # main
secrets:
ESPHOME_GITHUB_APP_PRIVATE_KEY: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
with:
# Live only on dev: a workflow_dispatch from any other branch is a dry run
dry-run: ${{ github.ref != 'refs/heads/dev' }}
# Must match the conflicts-only cron above; workflow_dispatch runs a full pass
conflicts-only: ${{ github.event.schedule == '30 3-21/3 * * *' }}
days-before-stale: 90
days-before-stale-conflicting: 30
days-before-close: 7
+1 -3
View File
@@ -98,10 +98,8 @@ venv-*/
.pioenvs
.piolibdeps
.pio
.vscode/*
.vscode/
!.vscode/tasks.json
!.vscode/extensions.json
!.vscode/settings.default.json
CMakeListsPrivate.txt
CMakeLists.txt
-14
View File
@@ -1,14 +0,0 @@
{
"recommendations": [
// python
"ms-python.python",
"ms-python.pylint",
"charliermarsh.ruff",
// yaml
"redhat.vscode-yaml",
// cpp
"ms-vscode.cpptools",
// editorconfig
"editorconfig.editorconfig"
]
}
-32
View File
@@ -1,32 +0,0 @@
{
// Default workspace settings. script/setup copies this file to settings.json
// when that file does not exist yet; settings.json is not tracked, so change it freely.
"pylint.args": [
"--rcfile=${workspaceFolder}/pyproject.toml"
],
"ruff.configuration": "${workspaceFolder}/pyproject.toml",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true
},
"files.trimTrailingWhitespace": true,
"yaml.customTags": [
"!secret scalar",
"!env_var scalar",
"!include scalar",
"!include mapping",
"!lambda scalar",
"!literal scalar",
"!literal sequence",
"!literal mapping",
"!extend scalar",
"!remove scalar",
"!include_dir_named scalar",
"!include_dir_list scalar",
"!include_dir_merge_list scalar",
"!include_dir_merge_named scalar"
],
"files.associations": {
"**/.vscode/*.json": "jsonc"
}
}
-30
View File
@@ -1,36 +1,6 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Set up dev environment",
"type": "shell",
"command": "script/setup",
"windows": {
"command": "script\\setup.bat"
},
"problemMatcher": []
},
{
"label": "Check dev environment",
"detail": "Runs script/setup when the folder is opened, if it has not been run yet",
"type": "shell",
"command": "script/setup",
"args": [
"--if-needed"
],
"windows": {
"command": "script\\setup.bat"
},
"runOptions": {
"runOn": "folderOpen"
},
"presentation": {
"panel": "dedicated",
"focus": false,
"showReuseMessage": false
},
"problemMatcher": []
},
{
"label": "Run Dashboard",
"type": "shell",
+82 -81
View File
@@ -18,7 +18,6 @@ from esphome.const import (
CONF_CONDITION,
CONF_COUNT,
CONF_ELSE,
CONF_FOR,
CONF_ID,
CONF_IF,
CONF_LAMBDA,
@@ -326,8 +325,10 @@ class _CollectedAction(MockObj):
_COLLECTED_ACTION = _CollectedAction("esphome__collected_action", "->")
_RUNNING_PARAM = "esphome__running"
# One statement of its own, so a function that nothing can stop can leave them all out
# Its own statement, matched exactly: _direct_function drops it and a while step rewrites it to _STOP_STEP
_STOP_CHECK = f"if (!{_RUNNING_PARAM})\n return;"
# The same for a while step, where a stop ends the loop like a false condition
_STOP_STEP = f"if (!{_RUNNING_PARAM})\n return false;"
def _is_collecting(id_: ID) -> bool:
@@ -991,6 +992,7 @@ LambdaAction = cg.esphome_ns.class_("LambdaAction", Action)
StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action)
IfAction = cg.esphome_ns.class_("IfAction", Action)
WhileAction = cg.esphome_ns.class_("WhileAction", Action)
ApplyWhileAction = cg.esphome_ns.class_("ApplyWhileAction", Action)
RepeatAction = cg.esphome_ns.class_("RepeatAction", Action)
WaitUntilAction = cg.esphome_ns.class_("WaitUntilAction", Action, cg.Component)
UpdateComponentAction = cg.esphome_ns.class_("UpdateComponentAction", Action)
@@ -1390,6 +1392,7 @@ def _validate_choose_else(choices: list[ConfigType]) -> list[ConfigType]:
return choices
# _iter_actions knows this shape: a list of choices whose if: holds a condition
@register_action(
CONF_CHOOSE,
IfAction,
@@ -1462,10 +1465,6 @@ async def choose_action_to_code(
return var
# Body actions that run code, which may change anything the condition reads
_WHILE_CODE_ACTIONS = frozenset((CONF_LAMBDA, "script.execute", "component.update"))
def _is_id_reference(id_: Any) -> bool:
# An id generated later is still None during validation
return isinstance(id_, ID) and not id_.is_declaration and id_.id is not None
@@ -1491,69 +1490,32 @@ def _referenced_ids(value: Any) -> set[str]:
return {id_.id for id_, _ in iter_ids(value) if _is_id_reference(id_)}
def _condition_has_code(value: Any) -> bool:
"""Whether a condition runs a lambda or depends on time (``for:``), so it can change on its own."""
if isinstance(value, Lambda):
return True
if isinstance(value, dict):
return CONF_FOR in value or any(_condition_has_code(v) for v in value.values())
if isinstance(value, list):
return any(_condition_has_code(v) for v in value)
return False
# The fix for a while whose body never waits, for the error and the warning
_WHILE_FIX = "Add '- delay: 100ms' as the last action under this loop's 'then:'."
def _condition_name(condition: ConfigType) -> str:
"""The condition's name, with its place in the file when known.
A list condition is wrapped in an `and:` that has no place in the file, so its first entry names it.
"""
key = _registry_key(condition)
if (
not hasattr(key, "esp_range")
and isinstance(items := condition[key], list)
and items
):
key = _registry_key(items[0])
if (location := getattr(key, "esp_range", None)) is None:
return f"'{key}'"
mark = location.start_mark
return f"'{key}' ({mark.document}:{mark.line + 1}:{mark.column + 1})"
def _validate_while_waits(config: ConfigType) -> ConfigType:
"""Reject or warn about a while whose body never waits and cannot change its condition.
Such a loop runs its body again from inside the previous pass and never lets other components run, so
a condition only they can change stays true until the device runs out of stack or the watchdog resets it.
"""
def _warn_while_without_wait(config: ConfigType) -> ConfigType:
# Warn when the body never waits and acts on nothing the condition (or its lambda) reads, the
# ids a lambda action names included. An empty body ends the loop at once.
body = config[CONF_THEN]
# An empty body ends the loop at once
if not body or has_non_synchronous_actions(body):
if not body:
return config
actions = list(_iter_actions(body))
condition = config[CONF_CONDITION]
# An action on an id the condition (or its lambda) reads, or a lambda action naming one, may change it
if (read := _referenced_ids(condition)) and read & _action_ids(actions):
if any(_is_deferring(key) for key, _ in actions):
return config
if not _condition_has_code(condition) and all(
action not in _WHILE_CODE_ACTIONS for action, _ in actions
):
raise cv.Invalid(
f"The body of this 'while' loop on {_condition_name(condition)} never waits and "
"cannot change the condition, so the condition never becomes false and the device "
f"would crash. {_WHILE_FIX}",
[CONF_THEN],
if not _referenced_ids(config[CONF_CONDITION]) & _action_ids(actions):
# Lazy, see _referenced_ids
from esphome.config import _format_location
condition = config[CONF_CONDITION]
# A list condition is wrapped in an `and:` that has no place in the file
while not hasattr(key := _registry_key(condition), "esp_range") and (
isinstance(items := condition[key], list) and items
):
condition = items[0]
location = getattr(key, "esp_range", None)
_LOGGER.warning(
"%sthe body of this 'while' loop on '%s' never waits, so the loop holds up "
"the device until the body changes the condition. Add a 'delay' inside the loop.",
f"{_format_location(location.start_mark)}: " if location else "",
key,
)
_LOGGER.warning(
"The body of the 'while' loop on %s never waits, so nothing else runs until the body "
"changes the condition; if it does not, the device crashes. %s",
_condition_name(condition),
_WHILE_FIX,
)
return config
@@ -1567,7 +1529,7 @@ def _validate_while_waits(config: ConfigType) -> ConfigType:
cv.Required(CONF_THEN): validate_action_list,
}
),
_validate_while_waits,
_warn_while_without_wait,
),
synchronous=True,
)
@@ -1582,17 +1544,41 @@ async def while_action_to_code(
actions = await build_list(config[CONF_THEN], template_arg, args)
# An empty body exits the loop at once, unlike an empty C++ loop body
if _is_collecting(action_id) and actions and _plans_inline(actions):
return _collect_control_flow(
check = (
f"const bool esphome__condition = {_condition_expression(condition, args)};"
)
if args:
# Never yields, see WHILE_YIELD_AFTER_MS
return _collect_control_flow(
action_id,
[
"while (true) {",
check,
_STOP_CHECK,
"if (!esphome__condition)\n break;",
*_plan_statements(actions),
_STOP_CHECK,
"}",
],
)
# A step function the ApplyWhileAction runs once per pass
return _new_running_action(
action_id,
ApplyWhileAction,
cg.bool_,
[
"while (true) {",
f"const bool esphome__condition = {_condition_expression(condition, args)};",
_STOP_CHECK,
"if (!esphome__condition)\n break;",
*_plan_statements(actions),
_STOP_CHECK,
"}",
check,
f"if (!{_RUNNING_PARAM} || !esphome__condition)\n return false;",
# Every stop check, also those of nested inline if and repeat, is its own line
*(
_STOP_STEP if line == _STOP_CHECK else line
for line in _plan_statements(actions)
),
# The last body action can stop the automation too
f"return {_RUNNING_PARAM} != 0;",
],
template_arg,
args,
)
var = cg.new_Pvariable(action_id, template_arg, _condition_object(condition))
for action in _emit_action_plan(actions, template_arg, args):
@@ -1847,16 +1833,31 @@ def _emit_inline_run(
"""
if len(run) == 1 and run[0].emit is not None:
return run[0].emit()
statements = _run_statements(run)
id_ = run[0].id_.copy()
id_.type = ApplyRunAction
return _new_running_action(
run[0].id_,
ApplyRunAction,
cg.void,
_run_statements(run),
template_arg,
args,
)
def _new_running_action(
source_id: ID,
action_type: MockObjClass,
return_type: SafeExpType,
statements: list[str],
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
"""An ``action_type`` object running a generated function that reads ``num_running_``."""
id_ = source_id.copy()
id_.type = action_type
fn = apply_static_function(
id_,
cg.void,
[
(cg.RawExpression("const int &"), _RUNNING_PARAM),
*_apply_params(args),
],
return_type,
[(cg.RawExpression("const int &"), _RUNNING_PARAM), *_apply_params(args)],
statements,
)
return cg.new_Pvariable(id_, cg.TemplateArguments(fn, *template_arg))
+5 -1
View File
@@ -50,7 +50,11 @@ void Animation::set_frame(int frame) {
unsigned abs_frame = abs(frame);
if (abs_frame < this->animation_frame_count_) {
this->current_frame_ = frame >= 0 ? frame : this->animation_frame_count_ - abs_frame;
if (frame >= 0) {
this->current_frame_ = frame;
} else {
this->current_frame_ = this->animation_frame_count_ - abs_frame;
}
}
this->update_data_start_();
+5 -1
View File
@@ -726,7 +726,11 @@ class DumpBuffer {
size_t pos() const { return pos_; }
/// Update position after buf_append_printf call
void set_pos(size_t pos) {
pos_ = pos >= CAPACITY ? CAPACITY - 1 : pos;
if (pos >= CAPACITY) {
pos_ = CAPACITY - 1;
} else {
pos_ = pos;
}
buf_[pos_] = '\0';
}
+6 -1
View File
@@ -253,7 +253,12 @@ template<typename... Ts> class BLEClientPasskeyReplyAction final : public Action
BLEClientPasskeyReplyAction(BLEClient *ble_client) { parent_ = ble_client; }
void play(const Ts &...x) override {
const uint32_t passkey = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...);
uint32_t passkey;
if (has_simple_value_) {
passkey = this->value_.simple;
} else {
passkey = this->value_.template_func(x...);
}
if (passkey > 999999)
return;
esp_bd_addr_t remote_bda;
+5 -1
View File
@@ -91,7 +91,11 @@ void BM8563::get_date_(ESPTime &time) {
uint8_t year_byte = bcd_to_uint8(buf[3] & 0xff);
time.year = buf[2] & 0x80 ? 1900 + year_byte : 2000 + year_byte;
if (buf[2] & 0x80) {
time.year = 1900 + year_byte;
} else {
time.year = 2000 + year_byte;
}
}
void BM8563::set_date_(const ESPTime &time) {
+6 -6
View File
@@ -17,7 +17,7 @@ DEPENDENCIES = ["bq76972"]
CONF_STACK_VOLTAGE = "stack_voltage"
# Ordered as the chip reports them, starting at command 0x68
TEMPERATURE_KEYS = [
CONF_TEMPERATURES = [
"internal_temperature",
"cfetoff_temperature",
"dfetoff_temperature",
@@ -30,7 +30,7 @@ TEMPERATURE_KEYS = [
"ddsg_temperature",
]
CELL_VOLTAGE_KEYS = [f"cell_{i}_voltage" for i in range(1, 17)]
CONF_CELL_VOLTAGES = [f"cell_{i}_voltage" for i in range(1, 17)]
voltage_sensor_schema = sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
@@ -50,8 +50,8 @@ CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_BQ76972_ID): cv.use_id(BQ76972Component),
cv.Optional(CONF_STACK_VOLTAGE): voltage_sensor_schema,
**{cv.Optional(key): voltage_sensor_schema for key in CELL_VOLTAGE_KEYS},
**{cv.Optional(key): temp_sensor_schema for key in TEMPERATURE_KEYS},
**{cv.Optional(key): voltage_sensor_schema for key in CONF_CELL_VOLTAGES},
**{cv.Optional(key): temp_sensor_schema for key in CONF_TEMPERATURES},
}
)
@@ -62,12 +62,12 @@ async def to_code(config: ConfigType) -> None:
sensors = sensor.sub_sensors(config)
await sensors(CONF_STACK_VOLTAGE, hub.set_stack_voltage_sensor)
for idx, key in enumerate(TEMPERATURE_KEYS):
for idx, key in enumerate(CONF_TEMPERATURES):
if (conf := config.get(key)) is not None:
sens = await sensor.new_sensor(conf)
cg.add(hub.set_temperature_sensor(sens, idx))
for idx, key in enumerate(CELL_VOLTAGE_KEYS):
for idx, key in enumerate(CONF_CELL_VOLTAGES):
if (conf := config.get(key)) is not None:
sens = await sensor.new_sensor(conf)
cg.add(hub.set_cell_sensor(sens, idx))
+5 -1
View File
@@ -203,7 +203,11 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
// USB power state
const char *usb_state;
if (nrf_power_usbregstatus_vbusdet_get(NRF_POWER)) {
usb_state = nrf_power_usbregstatus_outrdy_get(NRF_POWER) ? "ready" : "connected (regulator is not ready)";
if (nrf_power_usbregstatus_outrdy_get(NRF_POWER)) {
usb_state = "ready";
} else {
usb_state = "connected (regulator is not ready)";
}
} else {
usb_state = "disconnected";
}
+10 -2
View File
@@ -304,7 +304,11 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2,
// side parts
int lhline_width = -(dxmax - dxmin) + 1;
if (progress >= 50) {
upd_dxmax = float(dymax) < float(-dxmax) * tan_a ? std::ceil(float(dymax) / tan_a) : -dxmax;
if (float(dymax) < float(-dxmax) * tan_a) {
upd_dxmax = std::ceil(float(dymax) / tan_a);
} else {
upd_dxmax = -dxmax;
}
this->horizontal_line(center_x + dxmax, center_y - dymax, lhline_width, color); // left
if (!dymax)
this->horizontal_line(center_x - dxmin, center_y, lhline_width, color); // right horizontal border
@@ -315,7 +319,11 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2,
rhline_width > lhline_width ? lhline_width : rhline_width, color);
}
} else {
upd_dxmin = float(dymin) > float(-dxmin) * tan_a ? std::ceil(float(dymin) / tan_a) : -dxmin;
if (float(dymin) > float(-dxmin) * tan_a) {
upd_dxmin = std::ceil(float(dymin) / tan_a);
} else {
upd_dxmin = -dxmin;
}
lhline_width = -(dxmax + upd_dxmin) + 1;
if (!dymax)
this->horizontal_line(center_x - dxmin, center_y, lhline_width, color); // right horizontal border
+5 -1
View File
@@ -432,7 +432,11 @@ void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) {
const uint8_t bit_position = x % 8;
const uint8_t pixel_bit = 0x80u >> bit_position;
const auto original = this->buffer_[byte_position];
this->buffer_[byte_position] = color_to_mono(color) == 0 ? original & ~pixel_bit : original | pixel_bit;
if (color_to_mono(color) == 0) {
this->buffer_[byte_position] = original & ~pixel_bit;
} else {
this->buffer_[byte_position] = original | pixel_bit;
}
}
namespace {
@@ -2,8 +2,6 @@
Supported models:
- waveshare-7.5in-v2: 7.5" mono display, 800x480 pixels (EPD_7in5_V2)
- goodisplay-gdew075t7-7.5: Good Display GDEW075T7, the same 7.5" 800x480
panel, also sold by Seeed as 075BN-T7-D2
- seeed-reterminal-e1001: Seeed reTerminal E1001, which uses the same
7.5" 800x480 panel on an integrated ESP32-S3 board
- goodisplay-gdey0583t81-5.83: Good Display GDEY0583T81 5.83" mono display,
@@ -79,13 +77,6 @@ waveshare_7_5_v2 = uc8179.extend(
height=480,
)
# Good Display GDEW075T7, 7.5" mono 800x480, UC8179; the panel used in the Waveshare 7.5" V2.
# Replaces the waveshare_epaper models 7.50inv2 and 7.50inv2p (use full_update_every for partial updates).
goodisplay_gdew075t7 = waveshare_7_5_v2.extend("goodisplay-gdew075t7-7.5")
# Name printed on the panel's label as sold by Seeed
goodisplay_gdew075t7.extend("075bn-t7-d2")
# Good Display GDEY0583T81, 5.83" mono 648x480, UC8179; also sold by Seeed as 0583BN-T81-D2.
# Replaces the waveshare_epaper model gdey0583t81. Busy is low while busy, so set inverted: true.
goodisplay_gdey0583t81 = uc8179.extend(
@@ -392,7 +392,11 @@ void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param &param) {
// Called from main loop context via gap_event_handler after being queued from BT task
ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status);
this->scan_set_param_failed_ = param.status == ESP_BT_STATUS_DONE ? ESP_BT_STATUS_SUCCESS : param.status;
if (param.status == ESP_BT_STATUS_DONE) {
this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
} else {
this->scan_set_param_failed_ = param.status;
}
}
void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param &param) {
+15 -3
View File
@@ -107,7 +107,11 @@ void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) {
GPOC = 1 << num;
}
} else {
GP16O = value != pin->is_inverted() ? GP16O | 1 : GP16O & ~1;
if (value != pin->is_inverted()) {
GP16O = GP16O | 1;
} else {
GP16O = GP16O & ~1;
}
}
}
@@ -137,8 +141,16 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
isr_gpf(num) = isr_gpf(num) & ~(1 << GPFPU);
}
} else {
GP16E = flags & gpio::FLAG_OUTPUT ? GP16E | 1 : GP16E & ~1;
GPF16 = flags & gpio::FLAG_PULLDOWN ? GPF16 | (1 << GP16FPD) : GPF16 & ~(1 << GP16FPD);
if (flags & gpio::FLAG_OUTPUT) {
GP16E = GP16E | 1;
} else {
GP16E = GP16E & ~1;
}
if (flags & gpio::FLAG_PULLDOWN) {
GPF16 = GPF16 | (1 << GP16FPD);
} else {
GPF16 = GPF16 & ~(1 << GP16FPD);
}
}
}
+5 -1
View File
@@ -182,7 +182,11 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in
continue;
}
min_x = !has_char ? glyph->offset_x : std::min(min_x, x + glyph->offset_x);
if (!has_char) {
min_x = glyph->offset_x;
} else {
min_x = std::min(min_x, x + glyph->offset_x);
}
x += glyph->advance;
has_char = true;
+10 -2
View File
@@ -294,7 +294,11 @@ void GraphLegend::init(Graph *g) {
} else if (valpos == VALUE_POSITION_TYPE_BESIDE) {
this->xs_ = txtw + valw;
}
this->width_ = dir == DIRECTION_TYPE_VERTICAL ? this->xs_ : this->xs_ * n;
if (dir == DIRECTION_TYPE_VERTICAL) {
this->width_ = this->xs_;
} else {
this->width_ = this->xs_ * n;
}
} else {
this->xs_ = w / n;
this->x0_ = this->xs_ / 2;
@@ -315,7 +319,11 @@ void GraphLegend::init(Graph *g) {
}
this->height_ = this->ys_ * n;
}
this->height_ = dir == DIRECTION_TYPE_HORIZONTAL ? this->ys_ : this->ys_ * n;
if (dir == DIRECTION_TYPE_HORIZONTAL) {
this->height_ = this->ys_;
} else {
this->height_ = this->ys_ * n;
}
} else {
this->ys_ = h / n;
}
@@ -60,17 +60,21 @@ void GroveMotorDriveTB6612FNG::set_i2c_addr(uint8_t addr) {
void GroveMotorDriveTB6612FNG::dc_motor_run(uint8_t channel, int16_t speed) {
speed = clamp<int16_t>(speed, -255, 255);
this->buffer_[0] = channel;
this->buffer_[1] = speed >= 0 ? speed : (uint8_t) (-speed);
buffer_[0] = channel;
if (speed >= 0) {
buffer_[1] = speed;
} else {
buffer_[1] = (uint8_t) (-speed);
}
if (speed >= 0) {
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CW, this->buffer_, 2) != i2c::ERROR_OK) {
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CW, buffer_, 2) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Run motor failed!");
this->status_set_warning();
return;
}
} else {
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CCW, this->buffer_, 2) != i2c::ERROR_OK) {
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CCW, buffer_, 2) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Run motor failed!");
this->status_set_warning();
return;
@@ -294,7 +294,11 @@ bool HitachiClimate::parse_swing_(const uint8_t remote_state[]) {
HITACHI_AC424_SWINGH_SIZE);
ESP_LOGV(TAG, "SwingH: %02X %02X", remote_state[HITACHI_AC424_SWINGH_BYTE], swing_modeh);
this->swing_mode = (swing_modeh & 0x3) == 0x3 ? climate::CLIMATE_SWING_OFF : climate::CLIMATE_SWING_HORIZONTAL;
if ((swing_modeh & 0x3) == 0x3) {
this->swing_mode = climate::CLIMATE_SWING_OFF;
} else {
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
}
return true;
}
+12 -3
View File
@@ -73,9 +73,18 @@ void HTU21DComponent::update() {
this->status_clear_warning();
// HTU21D has a heater but no heater level, so report 1 while the heater is on
const uint8_t heater_level =
this->sensor_model_ == HTU21D_SENSOR_MODEL_HTU21D ? this->is_heater_enabled() : this->get_heater_level();
// HTU21D does have a heater module but does not have heater level
// Setting heater level to 1 in case the heater is ON
uint8_t heater_level = 0;
if (this->sensor_model_ == HTU21D_SENSOR_MODEL_HTU21D) {
if (this->is_heater_enabled()) {
heater_level = 1;
} else {
heater_level = 0;
}
} else {
heater_level = this->get_heater_level();
}
ESP_LOGD(TAG, "Heater Level=%d", heater_level);
-4
View File
@@ -325,10 +325,6 @@ async def to_code(config: ConfigType) -> None:
# Re-enable the I2C driver (excluded by default to save compile time)
include_builtin_idf_component("esp_driver_i2c")
if get_esp32_variant() == VARIANT_ESP32:
# ACK_ERR interrupt storm after a NACK; see i2c_bus_esp_idf.cpp
cg.add_define("USE_I2C_ISR_GUARD")
cg.add_build_flag("-Wl,--wrap=esp_intr_alloc_intrstatus")
if CORE.is_host:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
+5 -1
View File
@@ -24,7 +24,11 @@ void ArduinoI2CBus::setup() {
// I2C controller = (gpio / 2) % 2: even pairs (0-1,4-5,...) → I2C0, odd pairs (2-3,6-7,...) → I2C1
// RP2040 datasheet Table 2 (section 1.4.3): https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf
// RP2350 datasheet Table 7 (section 9.4): https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf
this->wire_ = (this->sda_pin_ / 2) % 2 == 0 ? &Wire : &Wire1;
if ((this->sda_pin_ / 2) % 2 == 0) {
this->wire_ = &Wire;
} else {
this->wire_ = &Wire1;
}
#endif
this->set_pins_and_clock_();
@@ -10,70 +10,6 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifdef USE_I2C_ISR_GUARD
#include <esp_intr_alloc.h>
#include <hal/i2c_ll.h> // declares i2c_periph_signal (IDF 5: soc/, IDF 6: hal/)
#if SOC_I2C_SUPPORT_HW_FSM_RST || I2C_LL_SUPPORT_HW_FSM_RST
#error "This chip has an I2C FSM reset; the ISR guard is no longer needed"
#endif
namespace esphome::i2c {
// Without an FSM reset ACK_ERR can stay raised and the IDF ISR never masks it (esp-idf#17690). Mask the master
// events until the next transaction when an error is raised both before and after the handler clears it.
static constexpr uint32_t I2C_ERROR_INTR = I2C_LL_INTR_NACK | I2C_LL_INTR_TIMEOUT | I2C_LL_INTR_ARBITRATION;
static_assert((I2C_LL_MASTER_EVENT_INTR & I2C_ERROR_INTR) == I2C_ERROR_INTR,
"The master event mask must cover the error interrupts");
struct I2CIsrGuard {
intr_handler_t handler;
void *arg;
i2c_dev_t *dev;
};
static I2CIsrGuard i2c_isr_guards[SOC_HP_I2C_NUM]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static void IRAM_ATTR i2c_isr_guard(void *arg) {
auto *guard = static_cast<I2CIsrGuard *>(arg);
uint32_t before;
i2c_ll_get_intr_mask(guard->dev, &before);
guard->handler(guard->arg);
uint32_t after;
i2c_ll_get_intr_mask(guard->dev, &after);
if (before & after & I2C_ERROR_INTR)
i2c_ll_disable_intr_mask(guard->dev, I2C_LL_MASTER_EVENT_INTR);
}
static int i2c_port_for_source(int source) {
for (int port = 0; port < SOC_HP_I2C_NUM; port++) {
if (source == i2c_periph_signal[port].irq)
return port;
}
return -1;
}
} // namespace esphome::i2c
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" {
esp_err_t __real_esp_intr_alloc_intrstatus(int source, int flags, uint32_t reg, uint32_t mask, intr_handler_t handler,
void *arg, intr_handle_t *ret_handle);
esp_err_t __wrap_esp_intr_alloc_intrstatus(int source, int flags, uint32_t reg, uint32_t mask, intr_handler_t handler,
void *arg, intr_handle_t *ret_handle) {
int port = esphome::i2c::i2c_port_for_source(source);
if (port < 0)
return __real_esp_intr_alloc_intrstatus(source, flags, reg, mask, handler, arg, ret_handle);
auto &guard = esphome::i2c::i2c_isr_guards[port];
auto previous = guard;
guard = {handler, arg, I2C_LL_GET_HW(port)};
esp_err_t err =
__real_esp_intr_alloc_intrstatus(source, flags, reg, mask, esphome::i2c::i2c_isr_guard, &guard, ret_handle);
if (err != ESP_OK)
guard = previous; // a failed allocation leaves the live guard alone
return err;
}
} // extern "C"
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif // USE_I2C_ISR_GUARD
namespace esphome::i2c {
ESPHOME_LOG_TAG(TAG, "i2c");
@@ -180,12 +116,6 @@ void IDFI2CBus::dump_config() {
if (this->timeout_ > 0) {
ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 "us", this->timeout_);
}
#ifdef USE_I2C_ISR_GUARD
// Null when the IDF driver no longer allocates its interrupt through the wrapped call
if (i2c_isr_guards[this->port_].handler == nullptr) {
ESP_LOGW(TAG, " Interrupt guard inactive");
}
#endif
switch (this->recovery_result_) {
case RECOVERY_COMPLETED:
ESP_LOGCONFIG(TAG, " Recovery: bus successfully recovered");
+5 -1
View File
@@ -1043,7 +1043,11 @@ void IT8951Display::set_mono_pixel_(uint16_t x, uint16_t y, bool value) const {
void IT8951Display::set_gray_pixel_(uint16_t x, uint16_t y, uint8_t nibble) const {
const uint32_t index = static_cast<uint32_t>(y) * this->row_width_ + (static_cast<uint32_t>(x) >> 1);
uint8_t buf = this->buffer_[index];
buf = x & 0x1 ? (buf & 0xF0) | nibble : (buf & 0x0F) | static_cast<uint8_t>(nibble << 4);
if (x & 0x1) {
buf = (buf & 0xF0) | nibble;
} else {
buf = (buf & 0x0F) | static_cast<uint8_t>(nibble << 4);
}
this->buffer_[index] = buf;
}
+18
View File
@@ -23,6 +23,8 @@ CONF_MAX_MOVE_DISTANCE = "max_move_distance"
CONF_MAX_STILL_DISTANCE = "max_still_distance"
CONF_BACKGROUND_CORRECTION = "background_correction"
CONF_BACKGROUND_CORRECTION_DURATION = "background_correction_duration"
CONF_MOVE_THRESHOLDS = [f"g{x}_move_threshold" for x in range(9)]
CONF_STILL_THRESHOLDS = [f"g{x}_still_threshold" for x in range(9)]
CONFIG_SCHEMA = cv.Schema(
{
@@ -52,6 +54,22 @@ CONFIG_SCHEMA = cv.Schema(
}
)
for i in range(9):
CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
cv.Schema(
{
cv.Optional(CONF_MOVE_THRESHOLDS[i]): cv.invalid(
f"The '{CONF_MOVE_THRESHOLDS[i]}' option has been moved to the '{CONF_MOVE_THRESHOLDS[i]}'"
f" number component"
),
cv.Optional(CONF_STILL_THRESHOLDS[i]): cv.invalid(
f"The '{CONF_STILL_THRESHOLDS[i]}' option has been moved to the '{CONF_STILL_THRESHOLDS[i]}'"
f" number component"
),
}
)
)
CONFIG_SCHEMA = cv.All(
CONFIG_SCHEMA.extend(uart.UART_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)
)
+2
View File
@@ -17,6 +17,8 @@ CONF_LD2412_ID = "ld2412_id"
CONF_MAX_MOVE_DISTANCE = "max_move_distance"
CONF_MAX_STILL_DISTANCE = "max_still_distance"
CONF_MOVE_THRESHOLDS = [f"g{x}_move_threshold" for x in range(9)]
CONF_STILL_THRESHOLDS = [f"g{x}_still_threshold" for x in range(9)]
CONFIG_SCHEMA = (
cv.Schema(
+2 -2
View File
@@ -7,7 +7,7 @@ from esphome.types import ConfigType
from .. import CONF_LD2420_ID, LD2420Component
CONF_OPERATING_MODE = "operating_mode"
SELECT_OPTIONS = [
CONF_SELECTS = [
"Normal",
"Calibrate",
"Simple",
@@ -29,6 +29,6 @@ async def to_code(config: ConfigType) -> None:
await selects(
CONF_OPERATING_MODE,
hub.set_operating_mode_select,
options=SELECT_OPTIONS,
options=CONF_SELECTS,
apply="change_operating_mode(static_cast<ld2420::OpMode>(index + 1))",
)
-15
View File
@@ -61,8 +61,6 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
# Fault handlers the crash handler wraps (BK7238, BK7231N)
BK_TRAP_HANDLERS = ("bk_trap_udef", "bk_trap_pabt", "bk_trap_dabt")
# Timing functions RTL8710B builds replace (rtl8710b_micros.c)
RTL8710B_TIME_WRAPS = ("millis", "micros", "delay", "delayMicroseconds")
# Families with a crash handler; the Cortex-M ones install it at run time (no wraps).
CRASH_HANDLER_FAMILIES = (
FAMILY_BK7231N,
@@ -160,16 +158,6 @@ def _update_core_data(config):
return config
def FILTER_SOURCE_FILES() -> list[str]:
"""Leave the RTL8710B timing wrappers out of every other family's build."""
if (data := CORE.data.get(KEY_LIBRETINY)) is None or data.get(KEY_FAMILY) in (
None,
FAMILY_RTL8710B,
):
return []
return ["rtl8710b_micros.c"]
def get_libretiny_component(core_obj=None):
return (core_obj or CORE).data[KEY_LIBRETINY][KEY_COMPONENT]
@@ -541,9 +529,6 @@ async def component_to_code(config):
# RTL8720C (ambz2) requires FreeRTOS 10.x so this only applies to RTL8710B
if config[CONF_FAMILY] == FAMILY_RTL8710B:
cg.add_platformio_option("custom_versions.freertos", "8.2.3")
# millis(), micros() and delays on the system timer and CPU cycles (see rtl8710b_micros.c)
for func in RTL8710B_TIME_WRAPS:
cg.add_build_flag(f"-Wl,--wrap={func}")
# LibreTiny bundles BDK 3.0.33 for these two but already builds BK7238 with 3.0.78,
# where STA+AP works (esphome#19430), the web server OTA does not trip the watchdog
@@ -34,33 +34,6 @@ static int IRAM_ATTR flags_to_mode(gpio::Flags flags) {
// The ISR copy is the pin itself, so both share the stored mode and level.
ISRInternalGPIOPin ArduinoInternalGPIOPin::to_isr() const { return ISRInternalGPIOPin(this); }
#if defined(USE_LIBRETINY_VARIANT_RTL8710B) && !defined(CLANG_TIDY)
// RTL8710B GPIO interrupts on one edge only and LibreTiny maps CHANGE to rising, so flip the edge on every interrupt.
// Mirrors LibreTiny 1.14.0 wiring_irq.c (PinData is the SDK handler id); drop once libretiny-eu/libretiny#155 is fixed.
// Re-reads the level after each write; a pin still bouncing after four tries is corrected at its next edge.
static void IRAM_ATTR rtl8710b_arm_next_edge(gpio_irq_t *irq) {
uint32_t level = GPIO_ReadDataBit(irq->pin);
for (int i = 0; i < 4; i++) {
// gpio_irq_set() without its flash wrapper, so the handler runs from RAM and mask ROM only
GPIO_INTMode(irq->pin, ENABLE, GPIO_INT_Trigger_EDGE,
level ? GPIO_INT_POLARITY_ACTIVE_LOW : GPIO_INT_POLARITY_ACTIVE_HIGH, GPIO_INT_DEBOUNCE_ENABLE);
const uint32_t now = GPIO_ReadDataBit(irq->pin);
if (now == level)
return;
level = now;
}
}
static void IRAM_ATTR rtl8710b_change_handler(uint32_t id, gpio_irq_event event) {
auto *data = reinterpret_cast<PinData *>(id); // NOLINT(performance-no-int-to-ptr)
// A later attach with another mode keeps this handler
if (data->irqMode == CHANGE)
rtl8710b_arm_next_edge(data->irq);
if (data->irqHandler != nullptr)
reinterpret_cast<void (*)(void *)>(data->irqHandler)(data->irqParam);
}
#endif
void ArduinoInternalGPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const {
PinStatus arduino_mode = (PinStatus) 255;
switch (type) {
@@ -82,20 +55,6 @@ void ArduinoInternalGPIOPin::attach_interrupt_(void (*func)(void *), void *arg,
}
attachInterruptParam(pin_, func, arduino_mode, arg);
#if defined(USE_LIBRETINY_VARIANT_RTL8710B) && !defined(CLANG_TIDY)
if (arduino_mode == CHANGE) {
PinInfo *info = pinInfo(this->pin_);
PinData *data = info != nullptr ? info->data : nullptr;
if (data != nullptr && data->irq != nullptr) {
// The interrupt is already live: keep it from arming an edge between the level read and the write below
const uint32_t primask = __get_PRIMASK();
__disable_irq();
GPIO_UserRegIrq(data->irq->pin, reinterpret_cast<void *>(rtl8710b_change_handler), data);
rtl8710b_arm_next_edge(data->irq);
__set_PRIMASK(primask);
}
}
#endif
}
#ifdef USE_BK72XX
+1 -4
View File
@@ -94,10 +94,7 @@ __attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); }
__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(::micros()); }
// Per-variant millis() fast path — matches MillisInternal::get().
#if defined(USE_LIBRETINY_VARIANT_RTL8710B)
// The RTL8710B tick is 976.6 us; millis() comes from the system timer instead (rtl8710b_micros.c)
__attribute__((always_inline)) inline uint32_t millis() { return static_cast<uint32_t>(::millis()); }
#elif defined(USE_RTL87XX) || defined(USE_LN882X)
#if defined(USE_RTL87XX) || defined(USE_LN882X)
static_assert(configTICK_RATE_HZ == 1000, "millis() fast path requires 1 kHz FreeRTOS tick");
__attribute__((always_inline)) inline uint32_t millis() {
// xTaskGetTickCountFromISR is mandatory in interrupt context per the FreeRTOS API contract.
@@ -1,71 +0,0 @@
// RTL8710B: the FreeRTOS tick is 1/1024 s, so millis(), micros() and delay() use the 32.768 kHz system timer instead
// and delayMicroseconds() the DWT cycle counter. Linked with -Wl,--wrap (RTL8710B_TIME_WRAPS in __init__.py).
#ifdef USE_LIBRETINY_VARIANT_RTL8710B
#include <stdint.h>
#include <FreeRTOS.h>
#include <task.h>
extern uint32_t SystemCoreClock;
__attribute__((long_call)) uint32_t SYSTIMER_TickGet(void); // mask ROM, rtl8710b_delay.h
#define DEMCR (*(volatile uint32_t *) 0xE000EDFC)
#define DEMCR_TRCENA (1UL << 24)
#define DWT_CTRL (*(volatile uint32_t *) 0xE0001000)
#define DWT_CYCCNT (*(volatile uint32_t *) 0xE0001004)
// Interrupt handlers call these, so they live in RAM like IRAM_ATTR (hal.h)
#define RTL8710B_RAM_TEXT __attribute__((noinline, section(".sram.text")))
// Extends the 32-bit timer, which wraps every 36.4 hours; the main loop calls millis() far more often
static inline __attribute__((always_inline)) uint64_t timer_ticks(void) {
static uint64_t ticks;
uint32_t primask;
__asm volatile("mrs %0, primask\n cpsid i" : "=r"(primask)::"memory");
ticks += (uint32_t) (SYSTIMER_TickGet() - (uint32_t) ticks);
const uint64_t now = ticks;
__asm volatile("msr primask, %0" ::"r"(primask) : "memory");
return now;
}
RTL8710B_RAM_TEXT unsigned long __wrap_micros(void) { // NOLINT(readability-identifier-naming)
// 1000000 / 32768 = 15625 / 512
return (uint32_t) ((timer_ticks() * 15625) >> 9);
}
RTL8710B_RAM_TEXT unsigned long __wrap_millis(void) { // NOLINT(readability-identifier-naming)
// 1000 / 32768 = 125 / 4096
return (uint32_t) ((timer_ticks() * 125) >> 12);
}
void __wrap_delay(uint32_t ms) { // NOLINT(readability-identifier-naming)
if (ms == 0) {
vTaskDelay(0);
return;
}
// vTaskDelay() counts the partial tick, so sleep to a deadline; +1 covers the elapsed part of the current count
const uint64_t deadline = timer_ticks() + ((uint64_t) ms * 32768 + 999) / 1000 + 1;
// Sleeps are capped at 1024 s so a delay longer than the 36.4 hour timer wrap still sees every wrap
for (uint64_t now = timer_ticks(); now < deadline; now = timer_ticks()) {
const uint64_t ticks = (deadline - now + 31) / 32;
vTaskDelay((TickType_t) (ticks < 0x100000 ? ticks : 0x100000));
}
}
RTL8710B_RAM_TEXT void __wrap_delayMicroseconds(unsigned int us) { // NOLINT(readability-identifier-naming)
DEMCR |= DEMCR_TRCENA;
DWT_CTRL |= 1;
uint32_t start = DWT_CYCCNT;
// Rounded up: the BW12 runs at 62.5 MHz
uint64_t cycles = (uint64_t) us * ((SystemCoreClock + 999999) / 1000000);
// Steps of at most 2^31 cycles keep the 32-bit difference from wrapping
while (cycles > 0) {
const uint32_t step = cycles > 0x80000000UL ? 0x80000000UL : (uint32_t) cycles;
while (DWT_CYCCNT - start < step) {
}
start += step;
cycles -= step;
}
}
#endif // USE_LIBRETINY_VARIANT_RTL8710B
+10 -2
View File
@@ -115,12 +115,20 @@ bool MAX44009Sensor::set_low_power_mode() {
uint8_t MAX44009Sensor::read_(uint8_t reg) {
uint8_t data = 0;
this->error_ = this->read_byte(reg, &data) ? MAX44009_OK : MAX44009_ERROR_WIRE_REQUEST;
if (!this->read_byte(reg, &data)) {
this->error_ = MAX44009_ERROR_WIRE_REQUEST;
} else {
this->error_ = MAX44009_OK;
}
return data;
}
void MAX44009Sensor::write_(uint8_t reg, uint8_t value) {
this->error_ = this->write_byte(reg, value) ? MAX44009_OK : MAX44009_ERROR_WIRE_REQUEST;
if (!this->write_byte(reg, value)) {
this->error_ = MAX44009_ERROR_WIRE_REQUEST;
} else {
this->error_ = MAX44009_OK;
}
}
} // namespace esphome::max44009
@@ -273,7 +273,11 @@ void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) {
}
}
} else if (this->orientation_ == 1) {
b = this->flip_x_ ? pixels[7 - col] : pixels[col];
if (this->flip_x_) {
b = pixels[7 - col];
} else {
b = pixels[col];
}
} else if (this->orientation_ == 2) {
for (uint8_t i = 0; i < 8; i++) {
if (this->flip_x_) {
+6 -2
View File
@@ -304,8 +304,12 @@ canbus::Error MCP251XFD::send_message(canbus::CanFrame *frame) {
// The object follows the two write command bytes, so it goes out without a copy.
std::array<uint8_t, 2 + MAX_MESSAGE_SIZE> transfer;
const uint32_t id =
frame->use_extended_id ? (frame->can_id >> 18) | ((frame->can_id & EXTENDED_ID_LOW_MASK) << 11) : frame->can_id;
uint32_t id;
if (frame->use_extended_id) {
id = (frame->can_id >> 18) | ((frame->can_id & EXTENDED_ID_LOW_MASK) << 11);
} else {
id = frame->can_id;
}
uint32_t flags = frame->can_data_length_code;
assign_bits(flags, MESSAGE_IDE, frame->use_extended_id);
assign_bits(flags, MESSAGE_RTR, frame->remote_transmission_request);
+1 -1
View File
@@ -30,7 +30,7 @@ def _get_data() -> MicronovaData:
return CORE.data[DOMAIN]
CONF_MICRONOVA_ID = "micronova_id"
CONF_MICRONOVA_ID = f"{DOMAIN}_id"
CONF_ENABLE_RX_PIN = "enable_rx_pin"
CONF_MEMORY_LOCATION = "memory_location"
CONF_MEMORY_ADDRESS = "memory_address"
+30 -9
View File
@@ -79,31 +79,52 @@ void MICS4514Component::update() {
}
if (this->nitrogen_dioxide_sensor_ != nullptr) {
const float nitrogendioxide = ox_f < 0.3f ? 0.0f : 0.164f * powf(ox_f, 0.975f);
float nitrogendioxide = 0.0f;
if (ox_f < 0.3f) {
nitrogendioxide = 0.0;
} else {
nitrogendioxide = 0.164f * powf(ox_f, 0.975f);
}
this->nitrogen_dioxide_sensor_->publish_state(nitrogendioxide);
}
if (this->methane_sensor_ != nullptr) {
// 0 outside the sensor's range
const float methane = (red_f > 0.9f || red_f < 0.5f) ? 0.0f : 630 / powf(red_f, 4.4f);
float methane = 0.0f;
if (red_f > 0.9f || red_f < 0.5f) { // outside the range->unlikely
methane = 0.0;
} else {
methane = 630 / powf(red_f, 4.4f);
}
this->methane_sensor_->publish_state(methane);
}
if (this->ethanol_sensor_ != nullptr) {
// 0 outside the sensor's range
const float ethanol = (red_f > 1.0f || red_f < 0.02f) ? 0.0f : 1.52f / powf(red_f, 1.55f);
float ethanol = 0.0f;
if (red_f > 1.0f || red_f < 0.02f) { // outside the range->unlikely
ethanol = 0.0;
} else {
ethanol = 1.52f / powf(red_f, 1.55f);
}
this->ethanol_sensor_->publish_state(ethanol);
}
if (this->hydrogen_sensor_ != nullptr) {
// 0 outside the sensor's range
const float hydrogen = (red_f > 0.9f || red_f < 0.02f) ? 0.0f : 0.85f / powf(red_f, 1.75f);
float hydrogen = 0.0f;
if (red_f > 0.9f || red_f < 0.02f) { // outside the range->unlikely
hydrogen = 0.0;
} else {
hydrogen = 0.85f / powf(red_f, 1.75f);
}
this->hydrogen_sensor_->publish_state(hydrogen);
}
if (this->ammonia_sensor_ != nullptr) {
// 0 outside the sensor's range
const float ammonia = (red_f > 0.98f || red_f < 0.2532f) ? 0.0f : 0.9f / powf(red_f, 4.6f);
float ammonia = 0.0f;
if (red_f > 0.98f || red_f < 0.2532f) { // outside the ammonia range->unlikely
ammonia = 0.0;
} else {
ammonia = 0.9f / powf(red_f, 4.6f);
}
this->ammonia_sensor_->publish_state(ammonia);
}
}
+15 -3
View File
@@ -147,9 +147,21 @@ void MipiRgb::common_setup_() {
}
config.data_width = data_pin_count;
config.disp_gpio_num = GPIO_NUM_NC;
config.hsync_gpio_num = this->hsync_pin_ ? static_cast<gpio_num_t>(this->hsync_pin_->get_pin()) : GPIO_NUM_NC;
config.vsync_gpio_num = this->vsync_pin_ ? static_cast<gpio_num_t>(this->vsync_pin_->get_pin()) : GPIO_NUM_NC;
config.de_gpio_num = this->de_pin_ ? static_cast<gpio_num_t>(this->de_pin_->get_pin()) : GPIO_NUM_NC;
if (this->hsync_pin_) {
config.hsync_gpio_num = static_cast<gpio_num_t>(this->hsync_pin_->get_pin());
} else {
config.hsync_gpio_num = GPIO_NUM_NC;
}
if (this->vsync_pin_) {
config.vsync_gpio_num = static_cast<gpio_num_t>(this->vsync_pin_->get_pin());
} else {
config.vsync_gpio_num = GPIO_NUM_NC;
}
if (this->de_pin_) {
config.de_gpio_num = static_cast<gpio_num_t>(this->de_pin_->get_pin());
} else {
config.de_gpio_num = GPIO_NUM_NC;
}
config.pclk_gpio_num = static_cast<gpio_num_t>(this->pclk_pin_->get_pin());
esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_);
if (err == ESP_OK)
+10 -2
View File
@@ -174,10 +174,18 @@ void MitsubishiClimate::transmit_state() {
remote_state[9] = 1;
break;
case climate::CLIMATE_FAN_MEDIUM:
remote_state[9] = this->fan_mode_ == MITSUBISHI_FAN_3L ? 2 : 3;
if (this->fan_mode_ == MITSUBISHI_FAN_3L) {
remote_state[9] = 2;
} else {
remote_state[9] = 3;
}
break;
case climate::CLIMATE_FAN_HIGH:
remote_state[9] = this->fan_mode_ == MITSUBISHI_FAN_3L ? 3 : 4;
if (this->fan_mode_ == MITSUBISHI_FAN_3L) {
remote_state[9] = 3;
} else {
remote_state[9] = 4;
}
break;
case climate::CLIMATE_FAN_MIDDLE:
remote_state[9] = 2;
@@ -17,7 +17,7 @@ CODEOWNERS = ["@crnjan"]
DEPENDENCIES = ["uart"]
DOMAIN = "mitsubishi_cn105"
CONF_MITSUBISHI_CN105_ID = "mitsubishi_cn105_id"
CONF_MITSUBISHI_CN105_ID = f"{DOMAIN}_id"
CONF_TELEMETRY_REQUEST_MIN_INTERVAL = "telemetry_request_min_interval"
CONF_HORIZONTAL = "horizontal"
CONF_VANE = "vane"
+7 -1
View File
@@ -680,7 +680,13 @@ PduBuffer create_write_coils_pdu(uint16_t start_address, PackedBits bits);
* @param value_type defines if 16/32/64 bits or FP32 is used
*/
template<typename Container> void float_to_payload(Container &data, float value, SensorValueType value_type) {
const int64_t val = value_type_is_float(value_type) ? bit_cast<uint32_t>(value) : llroundf(value);
int64_t val;
if (value_type_is_float(value_type)) {
val = bit_cast<uint32_t>(value);
} else {
val = llroundf(value);
}
number_to_payload(data, val, value_type);
}
@@ -47,7 +47,7 @@ def small_distance(value: Any) -> float:
# empty and full distance values.
# Format is - tank name: (empty distance in mm, full distance in mm)
#
SUPPORTED_TANKS_MAP = {
CONF_SUPPORTED_TANKS_MAP = {
TANK_TYPE_CUSTOM: (0, 100),
"20LB_V": (38, 254), # empty/full readings for 20lb US tank
"30LB_V": (38, 381),
@@ -81,7 +81,7 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_CUSTOM_DISTANCE_FULL): small_distance,
cv.Optional(CONF_CUSTOM_DISTANCE_EMPTY): small_distance,
cv.Required(CONF_TANK_TYPE): cv.enum(SUPPORTED_TANKS_MAP, upper=True),
cv.Required(CONF_TANK_TYPE): cv.enum(CONF_SUPPORTED_TANKS_MAP, upper=True),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
@@ -140,16 +140,16 @@ async def to_code(config: ConfigType) -> None:
if CONF_CUSTOM_DISTANCE_EMPTY in config:
cg.add(var.set_tank_empty(config[CONF_CUSTOM_DISTANCE_EMPTY]))
else:
cg.add(var.set_tank_empty(SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][0]))
cg.add(var.set_tank_empty(CONF_SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][0]))
if CONF_CUSTOM_DISTANCE_FULL in config:
cg.add(var.set_tank_full(config[CONF_CUSTOM_DISTANCE_FULL]))
else:
cg.add(var.set_tank_full(SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][1]))
cg.add(var.set_tank_full(CONF_SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][1]))
else:
# Set the Tank empty and full based on map - User is requesting standard tank
t = config[CONF_TANK_TYPE]
cg.add(var.set_tank_empty(SUPPORTED_TANKS_MAP[t][0]))
cg.add(var.set_tank_full(SUPPORTED_TANKS_MAP[t][1]))
cg.add(var.set_tank_empty(CONF_SUPPORTED_TANKS_MAP[t][0]))
cg.add(var.set_tank_full(CONF_SUPPORTED_TANKS_MAP[t][1]))
if (
minimum_signal_quality := config.get(CONF_MINIMUM_SIGNAL_QUALITY, None)
@@ -41,7 +41,7 @@ def small_distance(value: Any) -> float:
# empty and full distance values.
# Format is - tank name: (empty distance in mm, full distance in mm)
#
SUPPORTED_TANKS_MAP = {
CONF_SUPPORTED_TANKS_MAP = {
TANK_TYPE_CUSTOM: (38, 100),
"NORTH_AMERICA_20LB_VERTICAL": (38, 254), # empty/full readings for 20lb US tank
"NORTH_AMERICA_30LB_VERTICAL": (38, 381),
@@ -68,7 +68,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_CUSTOM_DISTANCE_FULL): small_distance,
cv.Optional(CONF_CUSTOM_DISTANCE_EMPTY): small_distance,
cv.Optional(CONF_PROPANE_BUTANE_MIX, default="100%"): cv.percentage,
cv.Required(CONF_TANK_TYPE): cv.enum(SUPPORTED_TANKS_MAP, upper=True),
cv.Required(CONF_TANK_TYPE): cv.enum(CONF_SUPPORTED_TANKS_MAP, upper=True),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
@@ -111,16 +111,16 @@ async def to_code(config: ConfigType) -> None:
if CONF_CUSTOM_DISTANCE_EMPTY in config:
cg.add(var.set_tank_empty(config[CONF_CUSTOM_DISTANCE_EMPTY]))
else:
cg.add(var.set_tank_empty(SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][0]))
cg.add(var.set_tank_empty(CONF_SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][0]))
if CONF_CUSTOM_DISTANCE_FULL in config:
cg.add(var.set_tank_full(config[CONF_CUSTOM_DISTANCE_FULL]))
else:
cg.add(var.set_tank_full(SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][1]))
cg.add(var.set_tank_full(CONF_SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][1]))
else:
# Set the Tank empty and full based on map - User is requesting standard tank
t = config[CONF_TANK_TYPE]
cg.add(var.set_tank_empty(SUPPORTED_TANKS_MAP[t][0]))
cg.add(var.set_tank_full(SUPPORTED_TANKS_MAP[t][1]))
cg.add(var.set_tank_empty(CONF_SUPPORTED_TANKS_MAP[t][0]))
cg.add(var.set_tank_full(CONF_SUPPORTED_TANKS_MAP[t][1]))
cg.add_optional(var.set_propane_butane_mix, config.get(CONF_PROPANE_BUTANE_MIX))
+5 -1
View File
@@ -203,7 +203,11 @@ static uint8_t crc4(uint16_t *buffer, size_t length) {
auto apply_crc = [&crc_remainder](uint8_t next) {
crc_remainder ^= next;
for (uint8_t bit = 8; bit > 0; --bit) {
crc_remainder = crc_remainder & 0x8000 ? (crc_remainder << 1) ^ 0x3000 : (crc_remainder << 1);
if (crc_remainder & 0x8000) {
crc_remainder = (crc_remainder << 1) ^ 0x3000;
} else {
crc_remainder = (crc_remainder << 1);
}
}
};
+10 -2
View File
@@ -292,7 +292,11 @@ void MSA3xxComponent::set_transform(bool mirror_x, bool mirror_y, bool mirror_z,
void MSA3xxComponent::setup_odr_(DataRate rate) {
RegOutputDataRate reg_odr;
auto reg = this->read_byte(static_cast<uint8_t>(RegisterMap::ODR));
reg_odr.raw = reg.value_or(0x0F); // default from datasheet
if (reg.has_value()) {
reg_odr.raw = reg.value();
} else {
reg_odr.raw = 0x0F; // defaut from datasheet
}
reg_odr.x_axis_disable = false;
reg_odr.y_axis_disable = false;
@@ -307,7 +311,11 @@ void MSA3xxComponent::setup_power_mode_bandwidth_(PowerMode power_mode, Bandwidt
auto reg = this->read_byte(static_cast<uint8_t>(RegisterMap::POWER_MODE_BANDWIDTH));
RegPowerModeBandwidth power_mode_bandwidth;
power_mode_bandwidth.raw = reg.value_or(0xde); // default from datasheet
if (reg.has_value()) {
power_mode_bandwidth.raw = reg.value();
} else {
power_mode_bandwidth.raw = 0xde; // defaut from datasheet
}
power_mode_bandwidth.power_mode = power_mode;
power_mode_bandwidth.low_power_bandwidth = bandwidth;
@@ -18,7 +18,11 @@ void OpenthermNumber::setup() {
} else {
this->pref_ = this->make_entity_preference<float>();
if (!this->pref_.load(&value)) {
value = !std::isnan(this->initial_value_) ? this->initial_value_ : this->traits.get_min_value();
if (!std::isnan(this->initial_value_)) {
value = this->initial_value_;
} else {
value = this->traits.get_min_value();
}
}
}
this->publish_state(value);
+2 -2
View File
@@ -103,7 +103,7 @@ IDF_TO_OT_LOG_LEVEL = {
"VERBOSE": "DEBG",
}
DEVICE_TYPES = [
CONF_DEVICE_TYPES = [
"FTD",
"MTD",
]
@@ -430,7 +430,7 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(CONF_MDNS_ID): cv.use_id(MDNSComponent),
cv.Optional(CONF_BORDER_ROUTER): _validate_border_router,
cv.Optional(CONF_DEVICE_TYPE, default="FTD"): cv.one_of(
*DEVICE_TYPES, upper=True
*CONF_DEVICE_TYPES, upper=True
),
cv.Optional(CONF_FORCE_DATASET): cv.boolean,
cv.Optional(CONF_TLV): cv.sensitive(
@@ -5,7 +5,6 @@ import esphome.codegen as cg
from esphome.components import esp32, esp32_rmt, remote_base
from esphome.components.libretiny import get_libretiny_family
from esphome.components.libretiny.const import FAMILY_BK7238, FAMILY_RTL8720C
from esphome.components.remote_base import CONF_TRANSMITTER_ID
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
@@ -33,6 +32,7 @@ CONF_EOT_LEVEL = "eot_level"
CONF_NON_BLOCKING = "non_blocking"
CONF_ON_TRANSMIT = "on_transmit"
CONF_ON_COMPLETE = "on_complete"
CONF_TRANSMITTER_ID = remote_base.CONF_TRANSMITTER_ID
remote_transmitter_ns = cg.esphome_ns.namespace("remote_transmitter")
RemoteTransmitterComponent = remote_transmitter_ns.class_(
@@ -22,10 +22,18 @@ void ResistanceSensor::process_(float value) {
float res = 0;
switch (this->configuration_) {
case UPSTREAM:
res = value == 0.0f ? NAN : (this->reference_voltage_ - value) / value;
if (value == 0.0f) {
res = NAN;
} else {
res = (this->reference_voltage_ - value) / value;
}
break;
case DOWNSTREAM:
res = value == this->reference_voltage_ ? NAN : value / (this->reference_voltage_ - value);
if (value == this->reference_voltage_) {
res = NAN;
} else {
res = value / (this->reference_voltage_ - value);
}
break;
}
+6 -1
View File
@@ -39,7 +39,12 @@ void SCD30Component::setup() {
ESP_LOGD(TAG, "SCD30 Firmware v%0d.%02d", (uint16_t(raw_firmware_version[0]) >> 8),
uint16_t(raw_firmware_version[0] & 0xFF));
const uint16_t temp_offset = this->temperature_offset_ > 0 ? (this->temperature_offset_ * 100) : 0;
uint16_t temp_offset;
if (this->temperature_offset_ > 0) {
temp_offset = (this->temperature_offset_ * 100);
} else {
temp_offset = 0;
}
if (!this->setup_check(this->write_command(SCD30_CMD_TEMPERATURE_OFFSET, temp_offset),
LOG_STR("Error setting temperature offset")))
+15 -3
View File
@@ -47,11 +47,23 @@ void SHT4XComponent::setup() {
ESP_LOGD(TAG, "Heater interval: %" PRIu32, this->heater_interval_);
if (this->heater_power_ == SHT4X_HEATERPOWER_HIGH) {
this->heater_command_ = this->heater_time_ == SHT4X_HEATERTIME_LONG ? 0x39 : 0x32;
if (this->heater_time_ == SHT4X_HEATERTIME_LONG) {
this->heater_command_ = 0x39;
} else {
this->heater_command_ = 0x32;
}
} else if (this->heater_power_ == SHT4X_HEATERPOWER_MED) {
this->heater_command_ = this->heater_time_ == SHT4X_HEATERTIME_LONG ? 0x2F : 0x24;
if (this->heater_time_ == SHT4X_HEATERTIME_LONG) {
this->heater_command_ = 0x2F;
} else {
this->heater_command_ = 0x24;
}
} else {
this->heater_command_ = this->heater_time_ == SHT4X_HEATERTIME_LONG ? 0x1E : 0x15;
if (this->heater_time_ == SHT4X_HEATERTIME_LONG) {
this->heater_command_ = 0x1E;
} else {
this->heater_command_ = 0x15;
}
}
ESP_LOGD(TAG, "Heater command: %x", this->heater_command_);
}
+5 -1
View File
@@ -36,7 +36,11 @@ void SHTCXComponent::setup() {
this->sensor_id_ = device_id_register;
if ((device_id_register & 0x3F) == 0x07) {
this->type_ = device_id_register & 0x800 ? SHTCX_TYPE_SHTC3 : SHTCX_TYPE_SHTC1;
if (device_id_register & 0x800) {
this->type_ = SHTCX_TYPE_SHTC3;
} else {
this->type_ = SHTCX_TYPE_SHTC1;
}
} else {
this->type_ = SHTCX_TYPE_UNKNOWN;
}
@@ -12,7 +12,12 @@ SmlTextSensor::SmlTextSensor(std::string server_id, std::string obis_code, SmlTy
: SmlListener(std::move(server_id), std::move(obis_code)), format_(format) {}
void SmlTextSensor::publish_val(const ObisInfo &obis_info) {
const uint8_t value_type = this->format_ == SML_UNDEFINED ? obis_info.value_type : this->format_;
uint8_t value_type;
if (this->format_ == SML_UNDEFINED) {
value_type = obis_info.value_type;
} else {
value_type = this->format_;
}
switch (value_type) {
case SML_HEX: {
@@ -335,7 +335,11 @@ void AudioPipeline::read_task(void *params) {
this_pipeline->raw_file_ring_buffer_ = temp_ring_buffer;
}
err = temp_ring_buffer == nullptr ? ESP_ERR_NO_MEM : reader->add_sink(temp_ring_buffer);
if (temp_ring_buffer == nullptr) {
err = ESP_ERR_NO_MEM;
} else {
err = reader->add_sink(temp_ring_buffer);
}
}
if (err != ESP_OK) {
@@ -524,7 +524,11 @@ void SpeakerMediaPlayer::play_file(const audio::AudioFile *media_file, bool anno
media_command.file = media_file;
media_command.is_announcement = announcement;
media_command.announce = this->single_pipeline_() || announcement;
if (this->single_pipeline_() || announcement) {
media_command.announce = true;
} else {
media_command.announce = false;
}
media_command.enqueue = enqueue;
xQueueSend(this->media_control_command_queue_, &media_command, portMAX_DELAY);
}
@@ -539,7 +543,11 @@ void SpeakerMediaPlayer::control(const media_player::MediaPlayerCall &call) {
auto ann = call.get_announcement();
media_command.is_announcement = ann.has_value() && *ann;
media_command.announce = this->single_pipeline_() || media_command.is_announcement;
if (this->single_pipeline_() || media_command.is_announcement) {
media_command.announce = true;
} else {
media_command.announce = false;
}
const auto &media_url = call.get_media_url();
if (media_url.has_value()) {
@@ -748,7 +748,11 @@ void SpeakerSourceMediaPlayer::control(const media_player::MediaPlayerCall &call
auto command = call.get_command();
bool enqueue = command.has_value() && command.value() == media_player::MEDIA_PLAYER_COMMAND_ENQUEUE;
control_command.type = enqueue ? MediaPlayerControlCommand::ENQUEUE_URI : MediaPlayerControlCommand::PLAY_URI;
if (enqueue) {
control_command.type = MediaPlayerControlCommand::ENQUEUE_URI;
} else {
control_command.type = MediaPlayerControlCommand::PLAY_URI;
}
// Heap allocation is unavoidable: URIs from Home Assistant are arbitrary-length (media URLs with tokens
// can easily exceed 500 bytes). Deleted in process_control_queue_() after the command is consumed. FreeRTOS queues
// require items to be copyable, so we store a pointer to the string in the queue rather than the string itself.
+6 -1
View File
@@ -95,7 +95,12 @@ uint16_t SPIDelegateBitBash::transfer_(uint16_t data, size_t num_bits) {
uint16_t out_data = 0;
for (uint8_t i = 0; i != num_bits; i++) {
const uint8_t shift = this->bit_order_ == BIT_ORDER_MSB_FIRST ? num_bits - 1 - i : i;
uint8_t shift;
if (bit_order_ == BIT_ORDER_MSB_FIRST) {
shift = num_bits - 1 - i;
} else {
shift = i;
}
if (clock_phase_ == CLOCK_PHASE_LEADING) {
// sampling on leading edge
+5 -1
View File
@@ -19,7 +19,11 @@ void SprinklerControllerNumber::setup() {
} else {
this->pref_ = this->make_entity_preference<float>();
if (!this->pref_.load(&value)) {
value = !std::isnan(this->initial_value_) ? this->initial_value_ : this->traits.get_min_value();
if (!std::isnan(this->initial_value_)) {
value = this->initial_value_;
} else {
value = this->traits.get_min_value();
}
}
}
this->publish_state(value);
@@ -135,7 +135,11 @@ void SSD1331::fill(Color color) {
const uint32_t color565 = display::ColorUtil::color_to_565(color);
for (uint32_t i = 0; i < this->get_buffer_length_(); i++) {
this->buffer_[i] = i & 1 ? color565 & 0xff : (color565 >> 8) & 0xff;
if (i & 1) {
this->buffer_[i] = color565 & 0xff;
} else {
this->buffer_[i] = (color565 >> 8) & 0xff;
}
}
}
void SSD1331::init_reset_() { pulse_reset(this->reset_pin_, {.high_before_ms = 1, .low_ms = 10}); }
@@ -167,7 +167,11 @@ void SSD1351::fill(Color color) {
const uint32_t color565 = display::ColorUtil::color_to_565(color);
for (uint32_t i = 0; i < this->get_buffer_length_(); i++) {
this->buffer_[i] = i & 1 ? color565 & 0xff : (color565 >> 8) & 0xff;
if (i & 1) {
this->buffer_[i] = color565 & 0xff;
} else {
this->buffer_[i] = (color565 >> 8) & 0xff;
}
}
}
void SSD1351::init_reset_() { pulse_reset(this->reset_pin_, {.high_before_ms = 1, .low_ms = 10}); }
@@ -18,8 +18,12 @@ class SunTextSensor final : public text_sensor::TextSensor, public PollingCompon
void set_format(const std::string &format) = delete;
void update() override {
optional<ESPTime> res =
this->sunrise_ ? this->parent_->sunrise(this->elevation_) : this->parent_->sunset(this->elevation_);
optional<ESPTime> res;
if (this->sunrise_) {
res = this->parent_->sunrise(this->elevation_);
} else {
res = this->parent_->sunset(this->elevation_);
}
if (!res) {
this->publish_state("");
return;
@@ -79,9 +79,21 @@ void TemplateDate::control(const datetime::DateCall &call) {
if (this->restore_value_) {
datetime::DateEntityRestoreState temp = {};
temp.year = opt_year.value_or(this->year_);
temp.month = opt_month.value_or(this->month_);
temp.day = opt_day.value_or(this->day_);
if (has_year) {
temp.year = *opt_year;
} else {
temp.year = this->year_;
}
if (has_month) {
temp.month = *opt_month;
} else {
temp.month = this->month_;
}
if (has_day) {
temp.day = *opt_day;
} else {
temp.day = this->day_;
}
this->pref_.save(&temp);
}
@@ -106,12 +106,36 @@ void TemplateDateTime::control(const datetime::DateTimeCall &call) {
if (this->restore_value_) {
datetime::DateTimeEntityRestoreState temp = {};
temp.year = opt_year.value_or(this->year_);
temp.month = opt_month.value_or(this->month_);
temp.day = opt_day.value_or(this->day_);
temp.hour = opt_hour.value_or(this->hour_);
temp.minute = opt_minute.value_or(this->minute_);
temp.second = opt_second.value_or(this->second_);
if (has_year) {
temp.year = *opt_year;
} else {
temp.year = this->year_;
}
if (has_month) {
temp.month = *opt_month;
} else {
temp.month = this->month_;
}
if (has_day) {
temp.day = *opt_day;
} else {
temp.day = this->day_;
}
if (has_hour) {
temp.hour = *opt_hour;
} else {
temp.hour = this->hour_;
}
if (has_minute) {
temp.minute = *opt_minute;
} else {
temp.minute = this->minute_;
}
if (has_second) {
temp.second = *opt_second;
} else {
temp.second = this->second_;
}
this->pref_.save(&temp);
}
@@ -79,9 +79,21 @@ void TemplateTime::control(const datetime::TimeCall &call) {
if (this->restore_value_) {
datetime::TimeEntityRestoreState temp = {};
temp.hour = opt_hour.value_or(this->hour_);
temp.minute = opt_minute.value_or(this->minute_);
temp.second = opt_second.value_or(this->second_);
if (has_hour) {
temp.hour = *opt_hour;
} else {
temp.hour = this->hour_;
}
if (has_minute) {
temp.minute = *opt_minute;
} else {
temp.minute = this->minute_;
}
if (has_second) {
temp.second = *opt_second;
} else {
temp.second = this->second_;
}
this->pref_.save(&temp);
}
@@ -15,7 +15,11 @@ void TemplateNumber::setup() {
} else {
this->pref_ = this->make_entity_preference<float>();
if (!this->pref_.load(&value)) {
value = !std::isnan(this->initial_value_) ? this->initial_value_ : this->traits.get_min_value();
if (!std::isnan(this->initial_value_)) {
value = this->initial_value_;
} else {
value = this->traits.get_min_value();
}
}
}
this->publish_state(value);
@@ -210,7 +210,11 @@ void ESP8266UartComponent::write_array(const uint8_t *data, size_t len) {
bool ESP8266UartComponent::peek_byte(uint8_t *data) {
if (!this->check_read_timeout_())
return false;
*data = this->hw_serial_ != nullptr ? this->hw_serial_->peek() : this->sw_serial_->peek_byte();
if (this->hw_serial_ != nullptr) {
*data = this->hw_serial_->peek();
} else {
*data = this->sw_serial_->peek_byte();
}
return true;
}
bool ESP8266UartComponent::read_array(uint8_t *data, size_t len) {
@@ -145,7 +145,12 @@ void RP2UartComponent::setup() {
this->serial_ = serial;
} else {
ESP_LOGV(TAG, "Using Hardware Serial");
SerialUART *serial = hw_uart == 0 ? &Serial1 : &Serial2;
SerialUART *serial;
if (hw_uart == 0) {
serial = &Serial1;
} else {
serial = &Serial2;
}
if (this->tx_pin_ != nullptr)
serial->setTX(this->tx_pin_->get_pin());
if (this->rx_pin_ != nullptr)
+15 -3
View File
@@ -70,7 +70,11 @@ static int ftdi_to_clkbits_am(int baudrate, uint32_t *encoded_divisor) {
}
}
baud_estimate = (24000000 + (try_divisor / 2)) / try_divisor;
baud_diff = baud_estimate < baudrate ? baudrate - baud_estimate : baud_estimate - baudrate;
if (baud_estimate < baudrate) {
baud_diff = baudrate - baud_estimate;
} else {
baud_diff = baud_estimate - baudrate;
}
if (i == 0 || baud_diff < best_baud_diff) {
best_divisor = try_divisor;
best_baud = baud_estimate;
@@ -104,11 +108,19 @@ static int ftdi_to_clkbits(int baudrate, unsigned int clk, int clk_div, uint32_t
best_baud = clk / (2 * clk_div);
} else {
divisor = clk * 16 / clk_div / baudrate;
best_divisor = divisor & 1 ? divisor / 2 + 1 : divisor / 2;
if (divisor & 1) {
best_divisor = divisor / 2 + 1;
} else {
best_divisor = divisor / 2;
}
if (best_divisor > 0x20000)
best_divisor = 0x1ffff;
best_baud = clk * 16 / clk_div / best_divisor;
best_baud = best_baud & 1 ? best_baud / 2 + 1 : best_baud / 2;
if (best_baud & 1) {
best_baud = best_baud / 2 + 1;
} else {
best_baud = best_baud / 2;
}
*encoded_divisor = (best_divisor >> 3) | (FRAC_CODE[best_divisor & 0x7] << 14);
}
return best_baud;
@@ -27,7 +27,7 @@ def validate_pwm_limits(config: ConfigType) -> ConfigType:
return config
_SAFE_PWM_LEVELS_SCHEMA = cv.Schema(
CONF_SAFE_PWM_LEVELS_SCHEMA = cv.Schema(
{
cv.Optional(CONF_MIN_VALUE, default=DUTY_DEFAULT_MIN): cv.uint8_t,
cv.Optional(CONF_MAX_VALUE, default=DUTY_DEFAULT_MAX): cv.uint8_t,
@@ -41,7 +41,7 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(CONF_WAVESHARE_IO_CH32V003_ID): cv.use_id(
WaveshareIOCH32V003Component
),
cv.Optional(CONF_SAFE_PWM_LEVELS): _SAFE_PWM_LEVELS_SCHEMA,
cv.Optional(CONF_SAFE_PWM_LEVELS): CONF_SAFE_PWM_LEVELS_SCHEMA,
}
),
validate_pwm_limits,
+5 -1
View File
@@ -96,7 +96,11 @@ uint64_t Wl134Component::hex_lsb_ascii_to_uint64_(const uint8_t *text, uint8_t t
i--;
uint8_t digit = text[i];
digit = digit >= 'A' ? digit - 'A' + 10 : digit - '0';
if (digit >= 'A') {
digit = digit - 'A' + 10;
} else {
digit = digit - '0';
}
value = (value << 4) + digit;
} while (i != 0);
+5 -1
View File
@@ -23,7 +23,11 @@ void ZHLT01Climate::transmit_state() {
// Byte 7 : Power | Swing | Fan
// -- Power
ir_message[7] = this->mode == climate::CLIMATE_MODE_OFF ? AC1_POWER_OFF : AC1_POWER_ON;
if (this->mode == climate::CLIMATE_MODE_OFF) {
ir_message[7] = AC1_POWER_OFF;
} else {
ir_message[7] = AC1_POWER_ON;
}
// -- Swing
switch (this->swing_mode) {
+3 -3
View File
@@ -53,9 +53,9 @@ from .const import ( # noqa: F401
)
from .const_zephyr import (
CONF_IEEE802154_VENDOR_OUI,
CONF_MAX_EP_NUMBER_ZEPHYR,
CONF_ZIGBEE_ID,
KEY_EP_NUMBER,
MAX_EP_NUMBER_ZEPHYR,
)
from .zigbee_esp32 import (
final_validate_esp32,
@@ -182,8 +182,8 @@ def validate_number_of_ep(config: ConfigType) -> ConfigType:
_LOGGER.warning(
"Single endpoint requires ZHA or at leatst Zigbee2MQTT 2.8.0. For older versions of Zigbee2MQTT use multiple endpoints"
)
if count > MAX_EP_NUMBER_ZEPHYR and not CORE.testing_mode:
raise cv.Invalid(f"Maximum number of end points is {MAX_EP_NUMBER_ZEPHYR}")
if count > CONF_MAX_EP_NUMBER_ZEPHYR and not CORE.testing_mode:
raise cv.Invalid(f"Maximum number of end points is {CONF_MAX_EP_NUMBER_ZEPHYR}")
return config
+1 -1
View File
@@ -8,7 +8,7 @@ from esphome.const import ( # noqa: F401 # pylint: disable=unused-import
CONF_ZIGBEE_SWITCH,
)
MAX_EP_NUMBER_ZEPHYR = 8
CONF_MAX_EP_NUMBER_ZEPHYR = 8
CONF_IEEE802154_VENDOR_OUI = "ieee802154_vendor_oui"
# Keys for CORE.data storage
+2
View File
@@ -291,6 +291,7 @@ bool Application::any_component_has_status_flag_(uint8_t flag) const {
void Application::reboot() {
ESP_LOGI(TAG, "Forcing a reboot");
this->app_state_ |= APP_STATE_SHUTTING_DOWN;
for (auto &component : std::ranges::reverse_view(this->components_)) {
component->on_shutdown();
}
@@ -309,6 +310,7 @@ void Application::safe_shutdown(uint32_t timeout_ms) {
}
void Application::run_safe_shutdown_hooks() {
this->app_state_ |= APP_STATE_SHUTTING_DOWN;
for (auto &component : std::ranges::reverse_view(this->components_)) {
component->on_safe_shutdown();
}
+5 -1
View File
@@ -318,7 +318,7 @@ class Application {
/// Return the public app state status bits (STATUS_LED_* only).
/// Internal bookkeeping bits like APP_STATE_SETUP_COMPLETE are masked
/// out so external readers (status_led components, etc.) never see them.
uint8_t get_app_state() const { return this->app_state_ & ~APP_STATE_SETUP_COMPLETE; }
uint8_t get_app_state() const { return this->app_state_ & ~(APP_STATE_SETUP_COMPLETE | APP_STATE_SHUTTING_DOWN); }
/// True once Application::setup() has finished walking all components
/// and finalized the initial status flags. Before this point, the
@@ -328,6 +328,10 @@ class Application {
/// (bit 6) to avoid costing additional RAM.
bool is_setup_complete() const { return (this->app_state_ & APP_STATE_SETUP_COMPLETE) != 0; }
/// True once shutdown started (reboot or safe shutdown); timeouts set from then on never run.
/// Stored as a free bit on app_state_ (bit 7) to avoid costing additional RAM.
bool is_shutting_down() const { return (this->app_state_ & APP_STATE_SHUTTING_DOWN) != 0; }
// Helper macro for entity getter method declarations
#ifdef USE_DEVICES
#define GET_ENTITY_METHOD(entity_type, entity_name, entities_member) \
+3
View File
@@ -595,6 +595,7 @@ template<typename... Ts> class Trigger {
};
template<typename... Ts> class ActionList;
class ActionTimer;
template<typename... Ts> class Action {
public:
@@ -624,6 +625,8 @@ template<typename... Ts> class Action {
protected:
friend ActionList<Ts...>;
// Resumes a while that gave the main loop a turn
friend ActionTimer;
/// For a branch owner (if, while), play() runs when the branch finishes.
virtual void play(const Ts &...x) = 0;
+119 -47
View File
@@ -11,6 +11,7 @@
#include <array>
#include <list>
#include <memory>
#include <tuple>
#include <type_traits>
#include <vector>
@@ -145,6 +146,31 @@ template<typename... Ts> class ForCondition : public Condition<Ts...>, public Co
uint32_t last_inactive_{0};
};
/// A while with no trigger arguments gives the main loop a turn after this long when its body never waits,
/// so entity states can change. With arguments it never yields: they may only be valid while the trigger runs.
inline constexpr uint32_t WHILE_YIELD_AFTER_MS = 20;
/// Timeouts named by an action; the one scheduler friend, for delay and while.
class ActionTimer {
template<typename... Ts> friend class DelayAction;
template<typename... Ts> friend class WhileAction;
friend class ApplyWhileActionBase;
/// See Scheduler::set_action_timeout_.
static void set(const void *self, uint32_t delay, std::function<void()> &&func, bool parallel) {
App.scheduler.set_action_timeout_(self, delay, std::move(func), parallel);
}
/// For a while without arguments: once its loop started at `start` ran too long, resumes it through play() on
/// the next main loop pass and returns true. Parallel, as a yielding run has no timeout of its own pending.
static bool yield_if_due(Action<> *self, uint32_t start) {
// No later pass runs the timeout once shutdown started, so a loop then runs to its end
if (App.is_shutting_down() || millis() - start <= WHILE_YIELD_AFTER_MS)
return false;
set(self, 0, [self]() { self->play(); }, /* parallel= */ true);
return true;
}
};
class StartupTrigger : public Trigger<>, public Component {
public:
explicit StartupTrigger(float setup_priority) : setup_priority_(setup_priority) {}
@@ -205,37 +231,17 @@ template<typename... Ts> class DelayAction : public Action<Ts...> {
// If num_running_ > 1, we have multiple instances running in parallel
// In single/restart/queued modes, only one instance runs at a time
// Parallel mode uses skip_cancel=true to allow multiple delays to coexist
// Parallel mode passes parallel=true to allow multiple delays to coexist
// WARNING: This can accumulate delays if scripts are triggered faster than they complete!
// Users should set max_runs on parallel scripts to limit concurrent executions.
// Issue #10264: This is a workaround for parallel script delays interfering with each other.
// Optimization: For no-argument delays (most common case), use direct lambda
// to avoid overhead from capturing arguments by value
if constexpr (sizeof...(Ts) == 0) {
auto f = [this]() { this->play_next_(); };
App.scheduler.set_timer_common_(
/* component= */ nullptr, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::SELF_POINTER,
/* static_name= */ reinterpret_cast<const char *>(this), /* hash_or_id= */ 0, this->delay_.value(),
std::move(f),
/* skip_cancel= */ this->num_running_ > 1,
// Record the owning script (if any) so the blocking warning can name it; propagates across
// chained delays via the scheduler.
/* source= */ App.get_current_source());
} else {
// For delays with arguments, capture by value to preserve argument values
// Arguments must be copied because original references may be invalid after delay
// `mutable` is required so captured copies of non-const reference args (e.g. std::string&)
// are passed as non-const lvalues to play_next_(const Ts&...) where Ts may be `T&`
auto f = [this, x...]() mutable { this->play_next_(x...); };
App.scheduler.set_timer_common_(
/* component= */ nullptr, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::SELF_POINTER,
/* static_name= */ reinterpret_cast<const char *>(this), /* hash_or_id= */ 0, this->delay_.value(x...),
std::move(f),
/* skip_cancel= */ this->num_running_ > 1,
// See the no-argument branch above: record the owning script for log attribution.
/* source= */ App.get_current_source());
}
// Arguments must be copied because original references may be invalid after delay
// `mutable` is required so captured copies of non-const reference args (e.g. std::string&)
// are passed as non-const lvalues to play_next_(const Ts&...) where Ts may be `T&`
ActionTimer::set(
this, this->delay_.value(x...), [this, x...]() mutable { this->play_next_(x...); },
/* parallel= */ this->num_running_ > 1);
}
void play(const Ts &...x) override { /* ignore - see play_complex */ }
@@ -360,6 +366,38 @@ template<bool HasElse, typename... Ts> class IfAction : public Action<Ts...> {
[[no_unique_address]] std::conditional_t<HasElse, ActionList<Ts...>, NoElse> else_;
};
/// Shared by every ApplyWhileAction, so each step function adds only its play() and vtable.
class ApplyWhileActionBase : public Action<> {
public:
void play_complex() override {
this->num_running_++;
this->play();
}
void stop() override { App.scheduler.cancel_timeout(this); }
protected:
using StepFn = bool (*)(const int &running);
void run_(StepFn step) {
// Any other type would bind the step's const int & to a temporary that never sees a stop
static_assert(std::is_same_v<decltype(this->num_running_), int>);
const uint32_t start = millis();
while (step(this->num_running_)) {
if (ActionTimer::yield_if_due(this, start))
return;
}
this->play_next_();
}
};
/// A while with no trigger arguments whose condition and body codegen merged into one step function.
/// Step runs the body once and returns true, or returns false once the condition is false or the run stopped.
template<auto Step> class ApplyWhileAction final : public ApplyWhileActionBase {
public:
void play() override { this->run_(Step); }
};
template<typename... Ts> class WhileAction : public Action<Ts...> {
public:
WhileAction(Condition<Ts...> *condition) : condition_(condition) {}
@@ -375,40 +413,74 @@ template<typename... Ts> class WhileAction : public Action<Ts...> {
void play_complex(const Ts &...x) final {
this->num_running_++;
// Initial condition check
if (!this->condition_->check(x...)) {
// If new condition check failed, stop loop if running
this->then_.stop();
this->play_next_(x...);
return;
}
// The condition may have stopped this automation
if (this->num_running_ == 0)
return;
// An empty body cannot resume the loop
// An empty body cannot resume the loop; the condition still runs once, as it may stop the automation
if (this->then_.empty()) {
this->condition_->check(x...);
this->play_next_(x...);
return;
}
this->then_.play(x...);
this->run_loop_(x...);
}
// Branch-end resume hook: loop again or continue
// Branch-end resume hook
void play(const Ts &...x) override {
// The condition may stop this automation
if (this->num_running_ > 0 && this->condition_->check(x...) && this->num_running_ > 0) {
this->then_.play(x...);
} else {
this->play_next_(x...);
// A body that ended without waiting is looped by run_loop_(); mark it ended. Another run's end passes other
// argument storage, and a second end in one pass loops on its own.
Body *body = this->body_;
if (body != nullptr && !body->ended && body->args == Keys{std::addressof(x)...}) {
body->ended = true;
return;
}
this->run_loop_(x...);
}
void stop() override { this->then_.stop(); }
void stop() override {
this->then_.stop();
if constexpr (sizeof...(Ts) == 0)
App.scheduler.cancel_timeout(this);
}
protected:
// The argument storage, which tells a run from another run of this action
using Keys = std::array<const void *, sizeof...(Ts)>;
// The body running synchronously, on the stack of run_loop_(); play() marks it ended. Per action, not
// shared, so loops of different actions never see each other's.
struct Body {
Keys args;
bool ended;
};
// Runs the body in a loop, not by recursion, while it ends without waiting
void run_loop_(const Ts &...x) {
[[maybe_unused]] uint32_t start{};
if constexpr (sizeof...(Ts) == 0)
start = millis();
const Keys keys{std::addressof(x)...};
while (true) {
// The condition may stop this automation
if (this->num_running_ == 0 || !this->condition_->check(x...) || this->num_running_ == 0) {
this->play_next_(x...);
return;
}
// On the stack, as the body can start this action again
Body body{keys, false};
Body *outer = this->body_;
this->body_ = &body;
this->then_.play(x...);
this->body_ = outer;
// A body that waits resumes through play() later
if (!body.ended)
return;
if constexpr (sizeof...(Ts) == 0) {
if (this->num_running_ > 0 && ActionTimer::yield_if_due(this, start))
return;
}
}
}
Condition<Ts...> *condition_;
ActionList<Ts...> then_;
Body *body_{nullptr};
};
// Forward declaration for RepeatLoopContinuation
+40 -10
View File
@@ -1,7 +1,5 @@
#pragma once
#include <algorithm>
#include "defines.h"
#include "component.h"
#include "helpers.h"
@@ -118,10 +116,26 @@ struct Color {
}
inline Color operator+(const Color &add) const ESPHOME_ALWAYS_INLINE {
Color ret;
ret.r = std::min(this->r + add.r, 255);
ret.g = std::min(this->g + add.g, 255);
ret.b = std::min(this->b + add.b, 255);
ret.w = std::min(this->w + add.w, 255);
if (uint8_t(add.r + this->r) < this->r) {
ret.r = 255;
} else {
ret.r = this->r + add.r;
}
if (uint8_t(add.g + this->g) < this->g) {
ret.g = 255;
} else {
ret.g = this->g + add.g;
}
if (uint8_t(add.b + this->b) < this->b) {
ret.b = 255;
} else {
ret.b = this->b + add.b;
}
if (uint8_t(add.w + this->w) < this->w) {
ret.w = 255;
} else {
ret.w = this->w + add.w;
}
return ret;
}
inline Color &operator+=(const Color &add) ESPHOME_ALWAYS_INLINE { return *this = (*this) + add; }
@@ -129,10 +143,26 @@ struct Color {
inline Color &operator+=(uint8_t add) ESPHOME_ALWAYS_INLINE { return *this = (*this) + add; }
inline Color operator-(const Color &subtract) const ESPHOME_ALWAYS_INLINE {
Color ret;
ret.r = std::max(this->r - subtract.r, 0);
ret.g = std::max(this->g - subtract.g, 0);
ret.b = std::max(this->b - subtract.b, 0);
ret.w = std::max(this->w - subtract.w, 0);
if (subtract.r > this->r) {
ret.r = 0;
} else {
ret.r = this->r - subtract.r;
}
if (subtract.g > this->g) {
ret.g = 0;
} else {
ret.g = this->g - subtract.g;
}
if (subtract.b > this->b) {
ret.b = 0;
} else {
ret.b = this->b - subtract.b;
}
if (subtract.w > this->w) {
ret.w = 0;
} else {
ret.w = this->w - subtract.w;
}
return ret;
}
inline Color &operator-=(const Color &subtract) ESPHOME_ALWAYS_INLINE { return *this = (*this) - subtract; }
+3
View File
@@ -98,6 +98,9 @@ inline constexpr uint8_t COMPONENT_HAS_LOOP = 0x20;
// decide whether to propagate clears to App.app_state_. Never set on a
// Component's component_state_.
inline constexpr uint8_t APP_STATE_SETUP_COMPLETE = 0x40;
// Bit 7 on Application::app_state_ (ONLY) — set once shutdown starts, as the
// scheduler then no longer runs.
inline constexpr uint8_t APP_STATE_SHUTTING_DOWN = 0x80;
inline constexpr uint8_t WARN_IF_BLOCKING_OVER_CS = 5U; // 50ms in centiseconds (1cs = 10ms)
-3
View File
@@ -723,9 +723,6 @@
#define USE_UDP_OMR_SOURCE
#define USE_ZIGBEE
#endif
#if defined(USE_ESP32_VARIANT_ESP32)
#define USE_I2C_ISR_GUARD
#endif
#if defined(USE_ESP32_VARIANT_ESP32S3)
// Border router with a UART RCP; a native-radio border router (C5/C6) adds only the coexistence call
#define USE_OPENTHREAD
+5 -1
View File
@@ -803,7 +803,11 @@ void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation,
hue = int(fmodf((60.0f * ((red - green) / delta)) + 240.0f, 360.0f));
}
saturation = max_color_value == 0 ? 0 : delta / max_color_value;
if (max_color_value == 0) {
saturation = 0;
} else {
saturation = delta / max_color_value;
}
value = max_color_value;
}
-3
View File
@@ -35,9 +35,6 @@ class MillisInternal {
static ESPHOME_ALWAYS_INLINE uint32_t get() {
#if defined(USE_ESP32) && CONFIG_FREERTOS_HZ == 1000
return xTaskGetTickCount();
#elif defined(USE_LIBRETINY_VARIANT_RTL8710B)
// The tick is 976.6 us here, so millis() reads the system timer (rtl8710b_micros.c)
return millis();
#elif defined(USE_LIBRETINY) && (defined(USE_RTL87XX) || defined(USE_LN882X))
// 1 kHz: xTaskGetTickCount() is already ms.
static_assert(configTICK_RATE_HZ == 1000, "MillisInternal fast path requires 1 kHz FreeRTOS tick");
+5
View File
@@ -110,6 +110,11 @@ uint32_t Scheduler::calculate_interval_offset_(uint32_t delay) {
return static_cast<uint32_t>((static_cast<uint64_t>(random_uint32()) * max_offset) >> 32);
}
void Scheduler::set_action_timeout_(const void *self, uint32_t delay, std::function<void()> &&func, bool parallel) {
this->set_timer_common_(nullptr, SchedulerItem::TIMEOUT, NameType::SELF_POINTER, static_cast<const char *>(self), 0,
delay, std::move(func), parallel, App.get_current_source());
}
// Common implementation for both timeout and interval
// name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id
void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type,
+6 -2
View File
@@ -18,11 +18,11 @@ namespace esphome {
class Component;
class Scheduler {
// Allow DelayAction to call set_timer_common_ with skip_cancel=true for parallel script delays.
// Allow ActionTimer (delay and while) to call set_action_timeout_, which skips cancel for parallel runs.
// This is needed to fix issue #10264 where parallel scripts with delays interfere with each other.
// We use friend instead of a public API because skip_cancel is dangerous - it can cause delays
// to accumulate and overload the scheduler if misused.
template<typename... Ts> friend class DelayAction;
friend class ActionTimer;
public:
/** Set a timeout with a const char* name.
@@ -266,6 +266,10 @@ class Scheduler {
}
};
// A timeout named by the action `self`; `parallel` keeps the timeouts of other runs of it.
// Records the owning script, if any, so the blocking warning can name it.
void set_action_timeout_(const void *self, uint32_t delay, std::function<void()> &&func, bool parallel);
// Common implementation for both timeout and interval
// name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id
// `source` is stored (in the union slot) only for SELF_POINTER items; ignored otherwise.
+4 -4
View File
@@ -85,10 +85,10 @@ def generate_cmakelists_txt(component: IDFComponent) -> str:
# Extract the values
build_src_dir = component.data.get("build", {}).get("srcDir", None)
if not build_src_dir:
# read_path always exists (checked when the library is downloaded)
build_src_dir = next(
(d for d in ("src", "Src") if (read_path / d).is_dir()), "."
)
for d in ["src", "Src", "."]:
if (read_path / Path(d)).is_dir():
build_src_dir = d
break
build_include_dir = component.data.get("build", {}).get(
"includeDir", DEFAULT_BUILD_INCLUDE_DIR
+1 -84
View File
@@ -4,7 +4,7 @@ import argparse
import ast
import codecs
import collections
from collections.abc import Iterable, Iterator
from collections.abc import Iterator
import fnmatch
import functools
import os.path
@@ -610,89 +610,6 @@ def lint_unit_from_const_py(fname, match):
return err
# Names that start with CONF_ or UNIT_ but are not config key or unit constants.
NON_LITERAL_CONSTANT_ALLOWLIST = {"UNIT_OF_MEASUREMENT_MAX_LENGTH"}
def _constant_names(target: ast.expr) -> Iterator[ast.Name]:
"""Yield the CONF_/UNIT_ names bound by an assignment target."""
if isinstance(target, ast.Name):
if (
target.id.startswith(("CONF_", "UNIT_"))
and target.id not in NON_LITERAL_CONSTANT_ALLOWLIST
):
yield target
elif isinstance(target, ast.Starred):
yield from _constant_names(target.value)
elif isinstance(target, (ast.Tuple, ast.List)):
for elt in target.elts:
yield from _constant_names(elt)
def _module_level_statements(nodes: Iterable[ast.AST]) -> Iterator[ast.AST]:
"""Yield statements at module level, including those inside if/try/with/for blocks.
Function and class bodies are a different scope and are not entered.
"""
for node in nodes:
yield node
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
continue
yield from _module_level_statements(
child
for child in ast.iter_child_nodes(node)
if isinstance(child, (ast.stmt, ast.excepthandler, ast.match_case))
)
@lint_content_check(include=["esphome/*.py"])
def lint_constant_is_plain_string(
fname: Path, content: str
) -> list[tuple[int, int, str]]:
"""Require module-level CONF_ and UNIT_ constants to be plain string literals.
Other scripts and lints (for example the duplicate constant checks above) read these
definitions as text, so a computed value would be missed or moved incorrectly.
"""
try:
tree = ast.parse(content, filename=str(fname))
except SyntaxError:
# Reported by the Python linters; nothing useful to add here.
return []
errs: list[tuple[int, int, str]] = []
for node in _module_level_statements(tree.body):
if isinstance(node, ast.Assign):
targets = node.targets
value = node.value
elif isinstance(node, ast.AnnAssign) and node.value is not None:
targets = [node.target]
value = node.value
elif isinstance(node, ast.AugAssign):
targets = [node.target]
value = None
else:
continue
# Unpacking a string binds single characters or a list, so only a plain name counts.
if (
isinstance(value, ast.Constant)
and type(value.value) is str
and all(isinstance(target, ast.Name) for target in targets)
):
continue
for target in targets:
for name in _constant_names(target):
example = highlight(f'{name.id} = "value"')
msg = (
f"Constant {highlight(name.id)} must be set to a plain string "
f"literal, such as {example}. Do not build it from other "
"constants, f-strings, operators or function calls. If the value "
"is not a config key or a unit, give it a name without the CONF_ "
"or UNIT_ prefix."
)
errs.append((name.lineno, name.col_offset + 1, msg))
return errs
RAW_PIN_ACCESS_RE = (
r"^\s(pinMode|digitalWrite|digitalRead)\((.*)->get_pin\(\),\s*([^)]+).*\)"
)
-2
View File
@@ -212,8 +212,6 @@ def clang_options(idedata, environment):
and not flag.startswith("-mtune=esp")
)
cmd.append("-std=gnu++20")
# clang otherwise reads the `module:` bit-field in IDF's soc/regdma.h as a C++20 module directive
cmd.append("-fno-cxx-modules")
if environment.startswith(("bk72xx", "ln882h", "rtl87xx")):
# LibreTiny leaves function-like macro values unparenthesized
+2 -52
View File
@@ -6,7 +6,6 @@ Windows entry points cannot drift apart. Uses only the standard library: it runs
before any dependency has been installed.
"""
import argparse
import os
from pathlib import Path
import shutil
@@ -19,8 +18,6 @@ MIN_PYTHON = (3, 12)
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_VENV = ROOT / "venv"
POST_CHECKOUT_HOOK = ROOT / "script" / "git-hooks" / "post-checkout"
VSCODE_SETTINGS = ROOT / ".vscode" / "settings.json"
VSCODE_DEFAULT_SETTINGS = ROOT / ".vscode" / "settings.default.json"
# State of the environment the dependencies end up in, used for the closing
# message.
@@ -73,22 +70,6 @@ def git_output(*args: str) -> str:
return result.stdout.strip()
def environment_ready() -> bool:
"""Return whether ESPHome is already installed in the environment setup uses."""
active = os.environ.get("VIRTUAL_ENV")
python = venv_python(Path(active) if active else DEFAULT_VENV)
if not python.is_file():
return False
# -I keeps the checkout itself off sys.path, so only an installed copy counts.
result = subprocess.run(
[str(python), "-I", "-c", "import esphome"],
cwd=ROOT,
capture_output=True,
check=False,
)
return result.returncode == 0
def create_venv(venv: Path) -> None:
"""Create a virtual environment, replacing anything already at the path."""
# --clear replaces a partial environment left behind by an interrupted run.
@@ -178,18 +159,6 @@ def install_git_hooks(env: dict[str, str]) -> None:
installed.chmod(0o755)
def install_vscode_settings() -> None:
"""Copy the default VS Code workspace settings, unless settings already exist.
settings.json is not tracked, so contributors can change it freely; it is
only seeded on the first run.
"""
if VSCODE_SETTINGS.exists():
return
shutil.copyfile(VSCODE_DEFAULT_SETTINGS, VSCODE_SETTINGS)
print(f"Created {VSCODE_SETTINGS.relative_to(ROOT).as_posix()} from the defaults.")
def activate_hint() -> str:
"""Return the command that activates the environment this script creates."""
activate = bin_dir(DEFAULT_VENV).relative_to(ROOT) / "activate"
@@ -221,32 +190,14 @@ def report(state: str, venv: Path) -> None:
)
def parse_args(argv: list[str]) -> argparse.Namespace:
"""Parse the command line."""
parser = argparse.ArgumentParser(description="Set up the ESPHome dev environment.")
parser.add_argument(
"--if-needed",
action="store_true",
help="do nothing if ESPHome is already installed (used when VS Code opens "
"the folder)",
)
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> None:
def main() -> None:
"""Set up the development environment."""
args = parse_args([] if argv is None else argv)
if sys.version_info < MIN_PYTHON:
raise SystemExit(
f"ESPHome needs Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]} or newer, "
f"but this is Python {sys.version.split()[0]}."
)
if args.if_needed and environment_ready():
install_vscode_settings()
print("Development environment already set up. Run script/setup to update it.")
return
# A virtual environment that is already active (for example the
# devcontainer's pre-provisioned esphome-venv) is installed into rather than
# creating a ./venv in the workspace.
@@ -263,10 +214,9 @@ def main(argv: list[str] | None = None) -> None:
env = venv_environment(venv)
install_dependencies(venv, env)
install_git_hooks(env)
install_vscode_settings()
(ROOT / ".temp").mkdir(exist_ok=True)
report(state, venv)
if __name__ == "__main__":
main(sys.argv[1:])
main()
@@ -670,11 +670,11 @@ def test_uc8179_e1001_code_generation(
assert "97, 4, 3, 32, 1, 224" in main_cpp
def test_uc8179_named_models(
def test_gdey0583t81_defaults_and_driver_class(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""The named UC8179 models use the UC8179 driver class with their own dimensions."""
"""The GDEY0583T81 models default to 648x480 and use the UC8179 driver class."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
@@ -682,12 +682,7 @@ def test_uc8179_named_models(
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
for name, dimensions in (
("goodisplay-gdew075t7-7.5", (800, 480)),
("075BN-T7-D2", (800, 480)),
("goodisplay-gdey0583t81-5.83", (648, 480)),
("0583BN-T81-D2", (648, 480)),
):
for name in ("goodisplay-gdey0583t81-5.83", "0583BN-T81-D2"):
model = MODELS[name.upper()]
assert model.class_name == "EPaperUC8179"
config = run_schema_validation(
@@ -698,7 +693,7 @@ def test_uc8179_named_models(
"full_update_every": 5,
}
)
assert model.get_dimensions(config) == dimensions
assert model.get_dimensions(config) == (648, 480)
def test_uc8279_width_must_be_a_multiple_of_8(
Whitespace-only changes.
@@ -1,11 +0,0 @@
esphome:
name: isr-guard-esp32
esp32:
board: esp32dev
framework:
type: esp-idf
i2c:
sda: GPIO21
scl: GPIO22
@@ -1,11 +0,0 @@
esphome:
name: isr-guard-esp32c3
esp32:
board: esp32-c3-devkitm-1
framework:
type: esp-idf
i2c:
sda: GPIO5
scl: GPIO6
@@ -1,31 +0,0 @@
"""The I2C ISR guard define and its linker wrap are emitted together, only for classic ESP32."""
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome.core import CORE
# Spelled out so a typo in the component's flag fails here
_WRAP_FLAG = "-Wl,--wrap=esp_intr_alloc_intrstatus"
@pytest.mark.parametrize(
("config_file", "expected"),
[
pytest.param("isr_guard_esp32.yaml", True, id="esp32"),
pytest.param("isr_guard_esp32c3.yaml", False, id="esp32c3"),
],
)
def test_guard_define_and_wrap_flag_go_together(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
config_file: str,
expected: bool,
) -> None:
generate_main(component_config_path(config_file))
defines = {define.name for define in CORE.defines}
# A define without the flag leaves the guard uninstalled; a flag without the define fails to link
assert ("USE_I2C_ISR_GUARD" in defines) is expected
assert (_WRAP_FLAG in CORE.build_flags) is expected
@@ -1,42 +0,0 @@
"""RTL8710B builds wrap the timing functions that rtl8710b_micros.c replaces and compile that file; other families
get neither, since a flag without the wrappers fails the link."""
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
from unittest.mock import patch
import pytest
from esphome.components.libretiny import FILTER_SOURCE_FILES, RTL8710B_TIME_WRAPS
from esphome.components.libretiny.const import KEY_LIBRETINY
from esphome.core import CORE
@pytest.mark.parametrize(
("config", "wrapped"),
[
("rtl8710b.yaml", True),
("rtl8720c.yaml", False),
("ln882x.yaml", False),
("bk72xx.yaml", False),
("bk7238.yaml", False),
],
)
def test_rtl8710b_time_wraps(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
config: str,
wrapped: bool,
) -> None:
generate_main(component_config_path(config))
for func in RTL8710B_TIME_WRAPS:
assert (f"-Wl,--wrap={func}" in CORE.build_flags) is wrapped
assert ("rtl8710b_micros.c" not in FILTER_SOURCE_FILES()) is wrapped
@pytest.mark.parametrize("core_data", [{}, {KEY_LIBRETINY: {}}])
def test_nothing_excluded_before_the_family_is_known(core_data: dict) -> None:
with patch("esphome.components.libretiny.CORE.data", core_data):
assert FILTER_SOURCE_FILES() == []
@@ -19,6 +19,19 @@ climate:
- logger.log:
format: "Climate mode %d"
args: [x.mode]
# A while with an argument that cannot be copied (Climate &): the inline and the WhileAction form
- while:
condition:
lambda: return false;
then:
- lambda: ESP_LOGD("test", "inline body");
- while:
condition:
lambda: return false;
then:
- wait_until:
condition:
lambda: return true;
on_control:
- lambda: |-
if (x.get_mode().has_value())
@@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <utility>
#include <vector>
#include "esphome/core/automation.h"
@@ -79,10 +80,10 @@ class StopOnCheckCondition : public Condition<> {
};
// Turns its condition off after a number of checks
class CountdownCondition : public Condition<> {
template<typename... Ts> class CountdownCondition : public Condition<Ts...> {
public:
explicit CountdownCondition(int passes) : passes_(passes) {}
bool check() override { return this->passes_-- > 0; }
bool check(const Ts &.../*x*/) override { return this->passes_-- > 0; }
protected:
int passes_;
@@ -177,7 +178,7 @@ TEST(ActionBranchOwner, PerItemIfAndWhileResumeAfterTheirBranch) {
EXPECT_EQ(log, (Log{1, 2, 4, 3, 4}));
log.clear();
CountdownCondition loop_cond(2);
CountdownCondition<> loop_cond(2);
WhileAction<> loop(&loop_cond);
DeferredAction wait(&log, 5);
RecordAction after2(&log, 6);
@@ -341,7 +342,7 @@ TEST(ActionBranchOwner, EmptyBranchesFinishImmediately) {
TEST(ActionBranchOwner, WhileRechecksEachPassAndExits) {
Log log;
CountdownCondition cond(3);
CountdownCondition<> cond(3);
WhileAction<> loop(&cond);
RecordAction body(&log, 1);
RecordAction after(&log, 2);
@@ -379,7 +380,7 @@ TEST(ActionBranchOwner, StopMidBranchClearsRunning) {
TEST(ActionBranchOwner, NestedIfInsideWhileResumesEachOwner) {
Log log;
CountdownCondition loop_cond(2);
CountdownCondition<> loop_cond(2);
FlagCondition if_cond;
WhileAction<> loop(&loop_cond);
IfAction<false> inner(&if_cond);
@@ -400,4 +401,246 @@ TEST(ActionBranchOwner, NestedIfInsideWhileResumesEachOwner) {
EXPECT_EQ(top.num_running(), 0);
}
// Counts the passes of a body that never waits
class CountAction : public Action<int> {
public:
void play(const int &x) override { this->total += x; }
int total{0};
};
// Enough passes to overflow the stack if each one nested another call chain
TEST(ActionBranchOwner, WhileBodyThatNeverWaitsLoopsInPlace) {
static constexpr int PASSES = 200000;
CountdownCondition<int> cond(PASSES);
WhileAction<int> loop(&cond);
CountAction body, after;
loop.add_then({&body});
ActionList<int> list;
list.add_actions({&loop, &after});
list.play(1);
// With an argument the loop never yields, so it ends before play() returns
EXPECT_EQ(body.total, PASSES);
EXPECT_EQ(after.total, 1);
EXPECT_FALSE(list.is_running());
}
// Keeps the first argument by reference, like wait_until does for an entity, and copies the second
class MixedHandoffAction : public Action<const int &, int> {
public:
void play_complex(const int &entity, const int &value) override {
this->num_running_++;
this->finish();
this->entity_ = &entity;
this->value_ = value;
this->waiting_ = true;
}
void play(const int & /*entity*/, const int & /*value*/) override {}
void finish() {
if (this->waiting_) {
this->waiting_ = false;
int released = this->value_;
this->play_next_(*this->entity_, released);
}
}
protected:
const int *entity_{nullptr};
int value_{0};
bool waiting_{false};
};
class OncePerSecondValueCondition : public Condition<const int &, int> {
public:
bool check(const int & /*entity*/, const int &x) override { return !std::exchange(this->seen_[x], true); }
protected:
bool seen_[4]{};
};
class RecordSecondAction : public Action<const int &, int> {
public:
explicit RecordSecondAction(Log *log) : log_(log) {}
void play(const int & /*entity*/, const int &x) override { this->log_->push_back(x); }
protected:
Log *log_;
};
// The end of another run's body inside this run's body must resume that run, not end this one. Runs on the same
// entity share its storage; the copied argument still tells them apart.
TEST(ActionBranchOwner, WhileTellsRunsApartBySharedAndCopiedArguments) {
OncePerSecondValueCondition cond;
WhileAction<const int &, int> loop(&cond);
MixedHandoffAction wait;
Log log;
RecordSecondAction after(&log);
loop.add_then({&wait});
ActionList<const int &, int> list;
list.add_actions({&loop, &after});
const int entity = 0;
list.play(entity, 1);
list.play(entity, 2);
EXPECT_EQ(log, (Log{1}));
wait.finish();
EXPECT_EQ(log, (Log{1, 2}));
EXPECT_FALSE(list.is_running());
}
class CountPassAction : public Action<> {
public:
void play() override { this->passes++; }
int passes{0};
};
// Without arguments a wait_until timeout of one run can end inside another run's pass. Runs without arguments
// are alike: whichever frame takes that end loops once for it, so every pass and every run still continues once.
TEST(ActionBranchOwner, WhileWithoutArgumentsKeepsRunsWhenAWaitTimesOutInside) {
CountdownCondition<> cond(3);
WhileAction<> loop(&cond);
CountPassAction pass;
FlagCondition never;
never.value = false;
WaitUntilAction<> wait(&never);
wait.set_timeout_value([]() -> uint32_t { return 10; });
Log log;
RecordAction after(&log, 1);
loop.add_then({&pass, &wait});
ActionList<> list;
list.add_actions({&loop, &after});
list.play(); // run A waits
delay(15);
list.play(); // run B's pass releases A, whose wait timed out, inside it
delay(15);
wait.loop(); // the remaining waits time out
EXPECT_EQ(pass.passes, 3);
EXPECT_EQ(log, (Log{1, 1}));
EXPECT_FALSE(list.is_running());
}
// Releases the run that waits here, then lets the current run go on at once: two ends in one pass
class ReleaseAndPassAction : public Action<> {
public:
void play_complex() override {
this->num_running_++;
if (this->waiting_) {
this->waiting_ = false;
this->play_next_();
}
if (this->pass) {
this->play_next_();
return;
}
this->waiting_ = true;
}
void play() override {}
bool pass{false};
protected:
bool waiting_{false};
};
// Without arguments both ends of one pass mark the same body; the second must still continue its run
TEST(ActionBranchOwner, WhileWithoutArgumentsContinuesBothEndsOfOnePass) {
CountdownCondition<> cond(2);
WhileAction<> loop(&cond);
ReleaseAndPassAction wait;
Log log;
RecordAction after(&log, 1);
loop.add_then({&wait});
ActionList<> list;
list.add_actions({&loop, &after});
list.play(); // run A waits
wait.pass = true;
list.play(); // run B's pass releases A and ends itself
EXPECT_EQ(log, (Log{1, 1}));
EXPECT_FALSE(list.is_running());
}
static int step_passes = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static ActionList<> *step_list = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static int step_stop_on = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// Like a generated step: false once the condition is false or the run stopped
static bool count_step(const int &running) {
if (!running || step_passes >= 3)
return false;
if (++step_passes == step_stop_on)
step_list->stop();
return true;
}
// Runs an ApplyWhileAction of count_step that stops its run on pass `stop_on` (0 for never)
static Log run_apply_while(int stop_on) {
step_passes = 0;
step_stop_on = stop_on;
Log log;
ApplyWhileAction<count_step> loop;
RecordAction after(&log, 1);
ActionList<> list;
list.add_actions({&loop, &after});
step_list = &list;
list.play();
step_list = nullptr;
EXPECT_FALSE(list.is_running());
return log;
}
TEST(ActionBranchOwner, ApplyWhileRunsStepsThenContinues) {
EXPECT_EQ(run_apply_while(0), (Log{1}));
EXPECT_EQ(step_passes, 3);
}
TEST(ActionBranchOwner, ApplyWhileStoppedInAStepEndsTheRun) {
EXPECT_TRUE(run_apply_while(2).empty());
EXPECT_EQ(step_passes, 2);
}
static int slow_passes = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// Two passes that together take longer than the yield time
static bool slow_step(const int &running) {
if (!running || slow_passes >= 2)
return false;
delay(WHILE_YIELD_AFTER_MS);
slow_passes++;
return true;
}
// Never constructed: names Application's protected state bits through a member pointer
struct AppStateAccess : Application {
static uint8_t Application::*state() { return &AppStateAccess::app_state_; }
};
// Restores App's state bits, so a shutdown started by a test does not reach later tests
class AppStateRestore {
public:
AppStateRestore() : saved_(App.*AppStateAccess::state()) {}
~AppStateRestore() { App.*AppStateAccess::state() = this->saved_; }
protected:
uint8_t saved_;
};
// Nothing runs a timeout once shutdown started, so a loop then must not yield
TEST(ActionBranchOwner, WhileDoesNotYieldOnceShutdownStarted) {
AppStateRestore restore;
App.run_safe_shutdown_hooks();
ASSERT_TRUE(App.is_shutting_down());
Log log;
ApplyWhileAction<slow_step> loop;
RecordAction after(&log, 1);
ActionList<> list;
list.add_actions({&loop, &after});
list.play();
EXPECT_EQ(slow_passes, 2);
EXPECT_EQ(log, (Log{1}));
}
TEST(ActionBranchOwner, ShutdownStateIsRestoredAfterTheTest) { EXPECT_FALSE(App.is_shutting_down()); }
} // namespace esphome::core::testing
@@ -567,43 +567,6 @@ display:
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
it.circle(it.get_width() / 2, it.get_height() / 2, 100, Color::BLACK);
# Good Display GDEW075T7 7.5" mono e-paper (800x480, UC8179)
- platform: epaper_spi
spi_id: spi_bus
model: goodisplay-gdew075t7-7.5
full_update_every: 5
cs_pin:
allow_other_uses: true
number: GPIO5
dc_pin:
allow_other_uses: true
number: GPIO17
reset_pin:
allow_other_uses: true
number: GPIO16
busy_pin:
allow_other_uses: true
number: GPIO4
inverted: true
# Seeed 075BN-T7-D2, the label name of the GDEW075T7 panel
- platform: epaper_spi
spi_id: spi_bus
model: 075BN-T7-D2
cs_pin:
allow_other_uses: true
number: GPIO5
dc_pin:
allow_other_uses: true
number: GPIO17
reset_pin:
allow_other_uses: true
number: GPIO16
busy_pin:
allow_other_uses: true
number: GPIO4
inverted: true
# Good Display GDEY0583T81 5.83" mono e-paper (648x480, UC8179)
- platform: epaper_spi
spi_id: spi_bus
@@ -0,0 +1,134 @@
esphome:
name: test-while-without-wait
host:
logger:
globals:
- id: count
type: int
initial_value: "0"
binary_sensor:
- platform: template
id: held
name: Held
script:
- id: release_later
then:
- delay: 300ms
- binary_sensor.template.publish:
id: held
state: false
# No arguments, inline body: ApplyWhileAction, which yields
- id: inline_noarg
then:
- while:
condition:
binary_sensor.is_on: held
then:
- lambda: id(count) += 1;
- logger.log: "INLINE_NOARG done"
# No arguments, a body with its own action object (wait_until ends at once): WhileAction<>, which yields
- id: object_noarg
then:
- while:
condition:
binary_sensor.is_on: held
then:
- lambda: id(count) += 1;
- wait_until:
condition:
lambda: return true;
- logger.log: "OBJECT_NOARG done"
# Busy bodies longer than the yield time. Without arguments the caller runs first; with arguments
# the loop never yields and ends before the caller goes on.
- id: busy_inline_noarg
then:
- lambda: id(count) = 0;
- while: &busy_inline
condition:
lambda: return id(count) < 2;
then:
- lambda: |-
delay(25);
id(count) += 1;
- logger.log: "BUSY_INLINE_NOARG done"
- id: busy_object_noarg
then:
- lambda: id(count) = 0;
- while: &busy_object
condition:
lambda: return id(count) < 2;
then:
- lambda: |-
delay(25);
id(count) += 1;
- wait_until:
condition:
lambda: return true;
- logger.log: "BUSY_OBJECT_NOARG done"
- id: busy_inline_str
parameters:
tag: string
then:
- lambda: id(count) = 0;
- while: *busy_inline
- logger.log:
format: "BUSY_INLINE_STR done %s"
args: [tag.c_str()]
- id: busy_object_str
parameters:
tag: string
then:
- lambda: id(count) = 0;
- while: *busy_object
- logger.log:
format: "BUSY_OBJECT_STR done %s"
args: [tag.c_str()]
api:
actions:
- action: run_entity_loops
then:
- binary_sensor.template.publish:
id: held
state: true
- script.execute: release_later
- script.execute: inline_noarg
- script.execute: object_noarg
- action: run_busy_loops
then:
- script.execute: busy_inline_noarg
- logger.log: "BUSY_INLINE_NOARG caller"
- script.execute: busy_object_noarg
- logger.log: "BUSY_OBJECT_NOARG caller"
- delay: 200ms
- script.execute:
id: busy_inline_str
tag: a
- logger.log: "BUSY_INLINE_STR caller"
- script.execute:
id: busy_object_str
tag: b
- logger.log: "BUSY_OBJECT_STR caller"
- action: run_stop_while_yielded
then:
- binary_sensor.template.publish:
id: held
state: true
- script.execute: inline_noarg
- script.execute: object_noarg
- delay: 100ms
- script.stop: inline_noarg
- script.stop: object_noarg
- delay: 100ms
- lambda: |-
ESP_LOGI("test", "STOP running %d %d", id(inline_noarg).is_running(), id(object_noarg).is_running());
- binary_sensor.template.publish:
id: held
state: false
Loaded 100 of 106 files, more files were not shown because too many files have changed in this diff. Show more