Compare commits

..
Author SHA1 Message Date
J. Nick Koston 7a36a832e3 Simplify the while check and name its condition in one helper
The condition's name and place in the file come from one helper, also
for a list condition, without the private config location formatter.
The fix text is shared by the error and the warning, and the check
stops at the first action that waits.
2026-11-09 02:10:37 -08:00
J. Nick Koston 098891bef6 [core] Reject a while whose body never waits and cannot change its condition
A while whose body never waits never lets other components run, so a
condition only they can change stays true until the device crashes. When
the condition only reads entity state and the body neither runs code nor
acts on what the condition reads, validation now fails and says to add a
delay. Other bodies that never wait and may not change the condition get
a warning with the loop's file and line.
2026-11-09 02:07:34 -08:00
Jesse Hills 46f532e2bd [espidf] Remove unreachable fallback when choosing a library source directory (#22305) 2026-11-09 16:54:33 +13:00
Jesse Hills f1a1502fb6 [ci] Check stale PRs for merge conflicts every 3 hours (#22446) 2026-11-09 16:26:12 +13:00
Jesse Hills d3738e31f6 [core] Require CONF_ and UNIT_ constants to be plain string literals (#22445) 2026-11-09 02:58:49 +00:00
Jesse Hills 76d0b989e7 [core] Recommend VS Code extensions and set up the dev environment on first open (#22301) 2026-11-09 15:51:41 +13:00
Jesse Hills f7c91fad0f [ld2412] Remove unused threshold key lists (#22443) 2026-11-08 20:59:27 -05:00
Jesse Hills a7b3c34598 [ld2410] Remove obsolete threshold key errors (#22444) 2026-11-08 20:58:11 -05:00
Jesse Hills 918fe15eb8 Merge branch 'beta' into dev 2026-11-09 14:05:11 +13:00
Jesse Hills 26c4f8d40e Merge pull request #22442 from esphome/bump-2026.11.0b3
2026.11.0b3
2026-11-09 14:04:52 +13:00
Jesse Hills c097fe388e Bump version to 2026.11.0b3 2026-11-09 13:36:04 +13:00
J. Nick Koston 21267d7421 [libretiny] Interrupt on both edges for any edge pins on RTL8710B (#22403) 2026-11-09 13:36:03 +13:00
J. Nick Koston 582adc950a [libretiny] Run millis(), micros() and delays on real time on RTL8710B (#22404) 2026-11-09 13:36:03 +13:00
J. Nick Koston 613319f984 [i2c] Mask I2C interrupts after an error on classic ESP32 (#22396) 2026-11-09 13:36:02 +13:00
J. Nick Koston 4ad3cc7c0e [light] Keep power estimation state in codegen static storage (#22370) 2026-11-09 13:36:02 +13:00
J. Nick Koston a033260639 [libretiny] Read pins in interrupts without detaching the interrupt on Realtek (#22400) 2026-11-09 13:36:02 +13:00
24f23ba3db [i2s_audio] Fix APLL clock source on ESP32-P4 (#22402)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-09 13:36:02 +13:00
Roman PriesolandJ. Nick Koston 674d593f75 [wifi] Create the IPv6 link-local through lwIP on connect (#22364)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-09 13:36:02 +13:00
Jonathan Swoboda c1771e002b [ld2410][ld2412][ld2420] Drop device classes from unitless gate and light entities (#22374) 2026-11-09 13:36:02 +13:00
Jonathan Swoboda 5529e87a40 [tsl2591][seeed_mr24hpc1] Drop device classes from unitless sensors (#22375) 2026-11-09 13:36:02 +13:00
J. Nick Koston 54cae78a42 [ht16k33_char] Name the I2C bus in the component test (#22372) 2026-11-09 13:36:02 +13:00
J. Nick Koston e77a3bc54d [core] Add cg.static_buffer for config-sized static buffers (#22326) 2026-11-09 13:36:02 +13:00
guillempages af7f95a01b [ht16k33] Rename display_off method (#22352) 2026-11-09 13:36:02 +13:00
daf48ce4f2 [vl53l0x] Recover from a failed I2C read instead of stalling (#19235)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Tekla Bania <[EMAIL]>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Signed-off-by: Tekla Bania <8668038+teklabania@users.noreply.github.com>
Signed-off-by: Tekla Bania <[EMAIL]>
2026-11-09 13:36:02 +13:00
J. Nick Koston 3de3f72e7c [libretiny] Interrupt on both edges for any edge pins on RTL8710B (#22403) 2026-11-09 13:33:53 +13:00
J. Nick Koston 1878a69187 [libretiny] Run millis(), micros() and delays on real time on RTL8710B (#22404) 2026-11-09 13:31:33 +13:00
J. Nick Koston 032c8d9618 [i2c] Mask I2C interrupts after an error on classic ESP32 (#22396) 2026-11-09 13:18:05 +13:00
Jesse Hills 36c079de78 [epaper_spi] Add Good Display GDEW075T7 7.5" model (075BN-T7-D2) (#22322) 2026-11-09 11:35:45 +13:00
J. Nick Koston bdd0a2b3b6 [core][components] Use standard helpers and ternaries for if/else blocks that pick a value (#22425) 2026-11-08 12:18:46 -06:00
106 changed files with 1323 additions and 1328 deletions

No files matched your search

+2 -4
View File
@@ -210,10 +210,8 @@ 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`, `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.
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.
## Conditions
+7 -1
View File
@@ -3,7 +3,11 @@ 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
@@ -16,12 +20,14 @@ 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@e1432c7b31fec7d24a13dc192953ebac836ed98a # main
uses: esphome/workflows/.github/workflows/stale.yml@2026.11.0
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
+3 -1
View File
@@ -98,8 +98,10 @@ venv-*/
.pioenvs
.piolibdeps
.pio
.vscode/
.vscode/*
!.vscode/tasks.json
!.vscode/extensions.json
!.vscode/settings.default.json
CMakeListsPrivate.txt
CMakeLists.txt
+14
View File
@@ -0,0 +1,14 @@
{
"recommendations": [
// python
"ms-python.python",
"ms-python.pylint",
"charliermarsh.ruff",
// yaml
"redhat.vscode-yaml",
// cpp
"ms-vscode.cpptools",
// editorconfig
"editorconfig.editorconfig"
]
}
+32
View File
@@ -0,0 +1,32 @@
{
// 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,6 +1,36 @@
{
"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",
+81 -82
View File
@@ -18,6 +18,7 @@ from esphome.const import (
CONF_CONDITION,
CONF_COUNT,
CONF_ELSE,
CONF_FOR,
CONF_ID,
CONF_IF,
CONF_LAMBDA,
@@ -325,10 +326,8 @@ class _CollectedAction(MockObj):
_COLLECTED_ACTION = _CollectedAction("esphome__collected_action", "->")
_RUNNING_PARAM = "esphome__running"
# Its own statement, matched exactly: _direct_function drops it and a while step rewrites it to _STOP_STEP
# One statement of its own, so a function that nothing can stop can leave them all out
_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:
@@ -992,7 +991,6 @@ 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)
@@ -1392,7 +1390,6 @@ 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,
@@ -1465,6 +1462,10 @@ 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
@@ -1490,32 +1491,69 @@ def _referenced_ids(value: Any) -> set[str]:
return {id_.id for id_, _ in iter_ids(value) if _is_id_reference(id_)}
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.
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.
"""
body = config[CONF_THEN]
if not body:
# An empty body ends the loop at once
if not body or has_non_synchronous_actions(body):
return config
actions = list(_iter_actions(body))
if any(_is_deferring(key) for key, _ in actions):
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):
return config
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,
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],
)
_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
@@ -1529,7 +1567,7 @@ def _warn_while_without_wait(config: ConfigType) -> ConfigType:
cv.Required(CONF_THEN): validate_action_list,
}
),
_warn_while_without_wait,
_validate_while_waits,
),
synchronous=True,
)
@@ -1544,41 +1582,17 @@ 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):
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(
return _collect_control_flow(
action_id,
ApplyWhileAction,
cg.bool_,
[
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;",
"while (true) {",
f"const bool esphome__condition = {_condition_expression(condition, args)};",
_STOP_CHECK,
"if (!esphome__condition)\n break;",
*_plan_statements(actions),
_STOP_CHECK,
"}",
],
template_arg,
args,
)
var = cg.new_Pvariable(action_id, template_arg, _condition_object(condition))
for action in _emit_action_plan(actions, template_arg, args):
@@ -1833,31 +1847,16 @@ def _emit_inline_run(
"""
if len(run) == 1 and run[0].emit is not None:
return run[0].emit()
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
statements = _run_statements(run)
id_ = run[0].id_.copy()
id_.type = ApplyRunAction
fn = apply_static_function(
id_,
return_type,
[(cg.RawExpression("const int &"), _RUNNING_PARAM), *_apply_params(args)],
cg.void,
[
(cg.RawExpression("const int &"), _RUNNING_PARAM),
*_apply_params(args),
],
statements,
)
return cg.new_Pvariable(id_, cg.TemplateArguments(fn, *template_arg))
+1 -5
View File
@@ -50,11 +50,7 @@ void Animation::set_frame(int frame) {
unsigned abs_frame = abs(frame);
if (abs_frame < this->animation_frame_count_) {
if (frame >= 0) {
this->current_frame_ = frame;
} else {
this->current_frame_ = this->animation_frame_count_ - abs_frame;
}
this->current_frame_ = frame >= 0 ? frame : this->animation_frame_count_ - abs_frame;
}
this->update_data_start_();
+1 -5
View File
@@ -726,11 +726,7 @@ class DumpBuffer {
size_t pos() const { return pos_; }
/// Update position after buf_append_printf call
void set_pos(size_t pos) {
if (pos >= CAPACITY) {
pos_ = CAPACITY - 1;
} else {
pos_ = pos;
}
pos_ = pos >= CAPACITY ? CAPACITY - 1 : pos;
buf_[pos_] = '\0';
}
+1 -6
View File
@@ -253,12 +253,7 @@ template<typename... Ts> class BLEClientPasskeyReplyAction final : public Action
BLEClientPasskeyReplyAction(BLEClient *ble_client) { parent_ = ble_client; }
void play(const Ts &...x) override {
uint32_t passkey;
if (has_simple_value_) {
passkey = this->value_.simple;
} else {
passkey = this->value_.template_func(x...);
}
const uint32_t passkey = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...);
if (passkey > 999999)
return;
esp_bd_addr_t remote_bda;
+1 -5
View File
@@ -91,11 +91,7 @@ void BM8563::get_date_(ESPTime &time) {
uint8_t year_byte = bcd_to_uint8(buf[3] & 0xff);
if (buf[2] & 0x80) {
time.year = 1900 + year_byte;
} else {
time.year = 2000 + year_byte;
}
time.year = buf[2] & 0x80 ? 1900 + year_byte : 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
CONF_TEMPERATURES = [
TEMPERATURE_KEYS = [
"internal_temperature",
"cfetoff_temperature",
"dfetoff_temperature",
@@ -30,7 +30,7 @@ CONF_TEMPERATURES = [
"ddsg_temperature",
]
CONF_CELL_VOLTAGES = [f"cell_{i}_voltage" for i in range(1, 17)]
CELL_VOLTAGE_KEYS = [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 CONF_CELL_VOLTAGES},
**{cv.Optional(key): temp_sensor_schema for key in CONF_TEMPERATURES},
**{cv.Optional(key): voltage_sensor_schema for key in CELL_VOLTAGE_KEYS},
**{cv.Optional(key): temp_sensor_schema for key in TEMPERATURE_KEYS},
}
)
@@ -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(CONF_TEMPERATURES):
for idx, key in enumerate(TEMPERATURE_KEYS):
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(CONF_CELL_VOLTAGES):
for idx, key in enumerate(CELL_VOLTAGE_KEYS):
if (conf := config.get(key)) is not None:
sens = await sensor.new_sensor(conf)
cg.add(hub.set_cell_sensor(sens, idx))
+1 -5
View File
@@ -203,11 +203,7 @@ 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)) {
if (nrf_power_usbregstatus_outrdy_get(NRF_POWER)) {
usb_state = "ready";
} else {
usb_state = "connected (regulator is not ready)";
}
usb_state = nrf_power_usbregstatus_outrdy_get(NRF_POWER) ? "ready" : "connected (regulator is not ready)";
} else {
usb_state = "disconnected";
}
+2 -10
View File
@@ -304,11 +304,7 @@ 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) {
if (float(dymax) < float(-dxmax) * tan_a) {
upd_dxmax = std::ceil(float(dymax) / tan_a);
} else {
upd_dxmax = -dxmax;
}
upd_dxmax = float(dymax) < float(-dxmax) * tan_a ? std::ceil(float(dymax) / tan_a) : -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
@@ -319,11 +315,7 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2,
rhline_width > lhline_width ? lhline_width : rhline_width, color);
}
} else {
if (float(dymin) > float(-dxmin) * tan_a) {
upd_dxmin = std::ceil(float(dymin) / tan_a);
} else {
upd_dxmin = -dxmin;
}
upd_dxmin = float(dymin) > float(-dxmin) * tan_a ? std::ceil(float(dymin) / tan_a) : -dxmin;
lhline_width = -(dxmax + upd_dxmin) + 1;
if (!dymax)
this->horizontal_line(center_x - dxmin, center_y, lhline_width, color); // right horizontal border
+1 -5
View File
@@ -432,11 +432,7 @@ 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];
if (color_to_mono(color) == 0) {
this->buffer_[byte_position] = original & ~pixel_bit;
} else {
this->buffer_[byte_position] = original | pixel_bit;
}
this->buffer_[byte_position] = color_to_mono(color) == 0 ? original & ~pixel_bit : original | pixel_bit;
}
namespace {
@@ -2,6 +2,8 @@
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,
@@ -77,6 +79,13 @@ 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,11 +392,7 @@ 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);
if (param.status == ESP_BT_STATUS_DONE) {
this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
} else {
this->scan_set_param_failed_ = param.status;
}
this->scan_set_param_failed_ = param.status == ESP_BT_STATUS_DONE ? ESP_BT_STATUS_SUCCESS : param.status;
}
void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param &param) {
+3 -15
View File
@@ -107,11 +107,7 @@ void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) {
GPOC = 1 << num;
}
} else {
if (value != pin->is_inverted()) {
GP16O = GP16O | 1;
} else {
GP16O = GP16O & ~1;
}
GP16O = value != pin->is_inverted() ? GP16O | 1 : GP16O & ~1;
}
}
@@ -141,16 +137,8 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
isr_gpf(num) = isr_gpf(num) & ~(1 << GPFPU);
}
} else {
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);
}
GP16E = flags & gpio::FLAG_OUTPUT ? GP16E | 1 : GP16E & ~1;
GPF16 = flags & gpio::FLAG_PULLDOWN ? GPF16 | (1 << GP16FPD) : GPF16 & ~(1 << GP16FPD);
}
}
+1 -5
View File
@@ -182,11 +182,7 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in
continue;
}
if (!has_char) {
min_x = glyph->offset_x;
} else {
min_x = std::min(min_x, x + glyph->offset_x);
}
min_x = !has_char ? glyph->offset_x : std::min(min_x, x + glyph->offset_x);
x += glyph->advance;
has_char = true;
+2 -10
View File
@@ -294,11 +294,7 @@ void GraphLegend::init(Graph *g) {
} else if (valpos == VALUE_POSITION_TYPE_BESIDE) {
this->xs_ = txtw + valw;
}
if (dir == DIRECTION_TYPE_VERTICAL) {
this->width_ = this->xs_;
} else {
this->width_ = this->xs_ * n;
}
this->width_ = dir == DIRECTION_TYPE_VERTICAL ? this->xs_ : this->xs_ * n;
} else {
this->xs_ = w / n;
this->x0_ = this->xs_ / 2;
@@ -319,11 +315,7 @@ void GraphLegend::init(Graph *g) {
}
this->height_ = this->ys_ * n;
}
if (dir == DIRECTION_TYPE_HORIZONTAL) {
this->height_ = this->ys_;
} else {
this->height_ = this->ys_ * n;
}
this->height_ = dir == DIRECTION_TYPE_HORIZONTAL ? this->ys_ : this->ys_ * n;
} else {
this->ys_ = h / n;
}
@@ -60,21 +60,17 @@ 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);
buffer_[0] = channel;
if (speed >= 0) {
buffer_[1] = speed;
} else {
buffer_[1] = (uint8_t) (-speed);
}
this->buffer_[0] = channel;
this->buffer_[1] = speed >= 0 ? speed : (uint8_t) (-speed);
if (speed >= 0) {
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CW, buffer_, 2) != i2c::ERROR_OK) {
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CW, this->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, buffer_, 2) != i2c::ERROR_OK) {
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CCW, this->buffer_, 2) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Run motor failed!");
this->status_set_warning();
return;
@@ -294,11 +294,7 @@ 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);
if ((swing_modeh & 0x3) == 0x3) {
this->swing_mode = climate::CLIMATE_SWING_OFF;
} else {
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
}
this->swing_mode = (swing_modeh & 0x3) == 0x3 ? climate::CLIMATE_SWING_OFF : climate::CLIMATE_SWING_HORIZONTAL;
return true;
}
+3 -12
View File
@@ -73,18 +73,9 @@ void HTU21DComponent::update() {
this->status_clear_warning();
// 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();
}
// 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();
ESP_LOGD(TAG, "Heater Level=%d", heater_level);
+4
View File
@@ -325,6 +325,10 @@ 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)
+1 -5
View File
@@ -24,11 +24,7 @@ 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
if ((this->sda_pin_ / 2) % 2 == 0) {
this->wire_ = &Wire;
} else {
this->wire_ = &Wire1;
}
this->wire_ = (this->sda_pin_ / 2) % 2 == 0 ? &Wire : &Wire1;
#endif
this->set_pins_and_clock_();
@@ -10,6 +10,70 @@
#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");
@@ -116,6 +180,12 @@ 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");
+1 -5
View File
@@ -1043,11 +1043,7 @@ 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];
if (x & 0x1) {
buf = (buf & 0xF0) | nibble;
} else {
buf = (buf & 0x0F) | static_cast<uint8_t>(nibble << 4);
}
buf = x & 0x1 ? (buf & 0xF0) | nibble : (buf & 0x0F) | static_cast<uint8_t>(nibble << 4);
this->buffer_[index] = buf;
}
-18
View File
@@ -23,8 +23,6 @@ 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(
{
@@ -54,22 +52,6 @@ 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,8 +17,6 @@ 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"
CONF_SELECTS = [
SELECT_OPTIONS = [
"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=CONF_SELECTS,
options=SELECT_OPTIONS,
apply="change_operating_mode(static_cast<ld2420::OpMode>(index + 1))",
)
+15
View File
@@ -61,6 +61,8 @@ 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,
@@ -158,6 +160,16 @@ 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]
@@ -529,6 +541,9 @@ 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,6 +34,33 @@ 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) {
@@ -55,6 +82,20 @@ 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
+4 -1
View File
@@ -94,7 +94,10 @@ __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_RTL87XX) || defined(USE_LN882X)
#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)
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.
@@ -0,0 +1,71 @@
// 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
+2 -10
View File
@@ -115,20 +115,12 @@ bool MAX44009Sensor::set_low_power_mode() {
uint8_t MAX44009Sensor::read_(uint8_t reg) {
uint8_t data = 0;
if (!this->read_byte(reg, &data)) {
this->error_ = MAX44009_ERROR_WIRE_REQUEST;
} else {
this->error_ = MAX44009_OK;
}
this->error_ = this->read_byte(reg, &data) ? MAX44009_OK : MAX44009_ERROR_WIRE_REQUEST;
return data;
}
void MAX44009Sensor::write_(uint8_t reg, uint8_t value) {
if (!this->write_byte(reg, value)) {
this->error_ = MAX44009_ERROR_WIRE_REQUEST;
} else {
this->error_ = MAX44009_OK;
}
this->error_ = this->write_byte(reg, value) ? MAX44009_OK : MAX44009_ERROR_WIRE_REQUEST;
}
} // namespace esphome::max44009
@@ -273,11 +273,7 @@ void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) {
}
}
} else if (this->orientation_ == 1) {
if (this->flip_x_) {
b = pixels[7 - col];
} else {
b = pixels[col];
}
b = this->flip_x_ ? pixels[7 - col] : pixels[col];
} else if (this->orientation_ == 2) {
for (uint8_t i = 0; i < 8; i++) {
if (this->flip_x_) {
+2 -6
View File
@@ -304,12 +304,8 @@ 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;
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;
}
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 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 = f"{DOMAIN}_id"
CONF_MICRONOVA_ID = "micronova_id"
CONF_ENABLE_RX_PIN = "enable_rx_pin"
CONF_MEMORY_LOCATION = "memory_location"
CONF_MEMORY_ADDRESS = "memory_address"
+9 -30
View File
@@ -79,52 +79,31 @@ void MICS4514Component::update() {
}
if (this->nitrogen_dioxide_sensor_ != nullptr) {
float nitrogendioxide = 0.0f;
if (ox_f < 0.3f) {
nitrogendioxide = 0.0;
} else {
nitrogendioxide = 0.164f * powf(ox_f, 0.975f);
}
const float nitrogendioxide = ox_f < 0.3f ? 0.0f : 0.164f * powf(ox_f, 0.975f);
this->nitrogen_dioxide_sensor_->publish_state(nitrogendioxide);
}
if (this->methane_sensor_ != nullptr) {
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);
}
// 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);
this->methane_sensor_->publish_state(methane);
}
if (this->ethanol_sensor_ != nullptr) {
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);
}
// 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);
this->ethanol_sensor_->publish_state(ethanol);
}
if (this->hydrogen_sensor_ != nullptr) {
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);
}
// 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);
this->hydrogen_sensor_->publish_state(hydrogen);
}
if (this->ammonia_sensor_ != nullptr) {
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);
}
// 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);
this->ammonia_sensor_->publish_state(ammonia);
}
}
+3 -15
View File
@@ -147,21 +147,9 @@ void MipiRgb::common_setup_() {
}
config.data_width = data_pin_count;
config.disp_gpio_num = 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.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;
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)
+2 -10
View File
@@ -174,18 +174,10 @@ void MitsubishiClimate::transmit_state() {
remote_state[9] = 1;
break;
case climate::CLIMATE_FAN_MEDIUM:
if (this->fan_mode_ == MITSUBISHI_FAN_3L) {
remote_state[9] = 2;
} else {
remote_state[9] = 3;
}
remote_state[9] = this->fan_mode_ == MITSUBISHI_FAN_3L ? 2 : 3;
break;
case climate::CLIMATE_FAN_HIGH:
if (this->fan_mode_ == MITSUBISHI_FAN_3L) {
remote_state[9] = 3;
} else {
remote_state[9] = 4;
}
remote_state[9] = this->fan_mode_ == MITSUBISHI_FAN_3L ? 3 : 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 = f"{DOMAIN}_id"
CONF_MITSUBISHI_CN105_ID = "mitsubishi_cn105_id"
CONF_TELEMETRY_REQUEST_MIN_INTERVAL = "telemetry_request_min_interval"
CONF_HORIZONTAL = "horizontal"
CONF_VANE = "vane"
+1 -7
View File
@@ -680,13 +680,7 @@ 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) {
int64_t val;
if (value_type_is_float(value_type)) {
val = bit_cast<uint32_t>(value);
} else {
val = llroundf(value);
}
const int64_t val = value_type_is_float(value_type) ? bit_cast<uint32_t>(value) : 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)
#
CONF_SUPPORTED_TANKS_MAP = {
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(CONF_SUPPORTED_TANKS_MAP, upper=True),
cv.Required(CONF_TANK_TYPE): cv.enum(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(CONF_SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][0]))
cg.add(var.set_tank_empty(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(CONF_SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][1]))
cg.add(var.set_tank_full(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(CONF_SUPPORTED_TANKS_MAP[t][0]))
cg.add(var.set_tank_full(CONF_SUPPORTED_TANKS_MAP[t][1]))
cg.add(var.set_tank_empty(SUPPORTED_TANKS_MAP[t][0]))
cg.add(var.set_tank_full(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)
#
CONF_SUPPORTED_TANKS_MAP = {
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(CONF_SUPPORTED_TANKS_MAP, upper=True),
cv.Required(CONF_TANK_TYPE): cv.enum(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(CONF_SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][0]))
cg.add(var.set_tank_empty(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(CONF_SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][1]))
cg.add(var.set_tank_full(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(CONF_SUPPORTED_TANKS_MAP[t][0]))
cg.add(var.set_tank_full(CONF_SUPPORTED_TANKS_MAP[t][1]))
cg.add(var.set_tank_empty(SUPPORTED_TANKS_MAP[t][0]))
cg.add(var.set_tank_full(SUPPORTED_TANKS_MAP[t][1]))
cg.add_optional(var.set_propane_butane_mix, config.get(CONF_PROPANE_BUTANE_MIX))
+1 -5
View File
@@ -203,11 +203,7 @@ 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) {
if (crc_remainder & 0x8000) {
crc_remainder = (crc_remainder << 1) ^ 0x3000;
} else {
crc_remainder = (crc_remainder << 1);
}
crc_remainder = crc_remainder & 0x8000 ? (crc_remainder << 1) ^ 0x3000 : (crc_remainder << 1);
}
};
+2 -10
View File
@@ -292,11 +292,7 @@ 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));
if (reg.has_value()) {
reg_odr.raw = reg.value();
} else {
reg_odr.raw = 0x0F; // defaut from datasheet
}
reg_odr.raw = reg.value_or(0x0F); // default from datasheet
reg_odr.x_axis_disable = false;
reg_odr.y_axis_disable = false;
@@ -311,11 +307,7 @@ 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;
if (reg.has_value()) {
power_mode_bandwidth.raw = reg.value();
} else {
power_mode_bandwidth.raw = 0xde; // defaut from datasheet
}
power_mode_bandwidth.raw = reg.value_or(0xde); // default from datasheet
power_mode_bandwidth.power_mode = power_mode;
power_mode_bandwidth.low_power_bandwidth = bandwidth;
@@ -18,11 +18,7 @@ void OpenthermNumber::setup() {
} else {
this->pref_ = this->make_entity_preference<float>();
if (!this->pref_.load(&value)) {
if (!std::isnan(this->initial_value_)) {
value = this->initial_value_;
} else {
value = this->traits.get_min_value();
}
value = !std::isnan(this->initial_value_) ? this->initial_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",
}
CONF_DEVICE_TYPES = [
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(
*CONF_DEVICE_TYPES, upper=True
*DEVICE_TYPES, upper=True
),
cv.Optional(CONF_FORCE_DATASET): cv.boolean,
cv.Optional(CONF_TLV): cv.sensitive(
@@ -5,6 +5,7 @@ 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 (
@@ -32,7 +33,6 @@ 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,18 +22,10 @@ void ResistanceSensor::process_(float value) {
float res = 0;
switch (this->configuration_) {
case UPSTREAM:
if (value == 0.0f) {
res = NAN;
} else {
res = (this->reference_voltage_ - value) / value;
}
res = value == 0.0f ? NAN : (this->reference_voltage_ - value) / value;
break;
case DOWNSTREAM:
if (value == this->reference_voltage_) {
res = NAN;
} else {
res = value / (this->reference_voltage_ - value);
}
res = value == this->reference_voltage_ ? NAN : value / (this->reference_voltage_ - value);
break;
}
+1 -6
View File
@@ -39,12 +39,7 @@ 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));
uint16_t temp_offset;
if (this->temperature_offset_ > 0) {
temp_offset = (this->temperature_offset_ * 100);
} else {
temp_offset = 0;
}
const uint16_t temp_offset = this->temperature_offset_ > 0 ? (this->temperature_offset_ * 100) : 0;
if (!this->setup_check(this->write_command(SCD30_CMD_TEMPERATURE_OFFSET, temp_offset),
LOG_STR("Error setting temperature offset")))
+3 -15
View File
@@ -47,23 +47,11 @@ void SHT4XComponent::setup() {
ESP_LOGD(TAG, "Heater interval: %" PRIu32, this->heater_interval_);
if (this->heater_power_ == SHT4X_HEATERPOWER_HIGH) {
if (this->heater_time_ == SHT4X_HEATERTIME_LONG) {
this->heater_command_ = 0x39;
} else {
this->heater_command_ = 0x32;
}
this->heater_command_ = this->heater_time_ == SHT4X_HEATERTIME_LONG ? 0x39 : 0x32;
} else if (this->heater_power_ == SHT4X_HEATERPOWER_MED) {
if (this->heater_time_ == SHT4X_HEATERTIME_LONG) {
this->heater_command_ = 0x2F;
} else {
this->heater_command_ = 0x24;
}
this->heater_command_ = this->heater_time_ == SHT4X_HEATERTIME_LONG ? 0x2F : 0x24;
} else {
if (this->heater_time_ == SHT4X_HEATERTIME_LONG) {
this->heater_command_ = 0x1E;
} else {
this->heater_command_ = 0x15;
}
this->heater_command_ = this->heater_time_ == SHT4X_HEATERTIME_LONG ? 0x1E : 0x15;
}
ESP_LOGD(TAG, "Heater command: %x", this->heater_command_);
}
+1 -5
View File
@@ -36,11 +36,7 @@ void SHTCXComponent::setup() {
this->sensor_id_ = device_id_register;
if ((device_id_register & 0x3F) == 0x07) {
if (device_id_register & 0x800) {
this->type_ = SHTCX_TYPE_SHTC3;
} else {
this->type_ = SHTCX_TYPE_SHTC1;
}
this->type_ = device_id_register & 0x800 ? SHTCX_TYPE_SHTC3 : SHTCX_TYPE_SHTC1;
} else {
this->type_ = SHTCX_TYPE_UNKNOWN;
}
@@ -12,12 +12,7 @@ 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) {
uint8_t value_type;
if (this->format_ == SML_UNDEFINED) {
value_type = obis_info.value_type;
} else {
value_type = this->format_;
}
const uint8_t value_type = this->format_ == SML_UNDEFINED ? obis_info.value_type : this->format_;
switch (value_type) {
case SML_HEX: {
@@ -335,11 +335,7 @@ void AudioPipeline::read_task(void *params) {
this_pipeline->raw_file_ring_buffer_ = temp_ring_buffer;
}
if (temp_ring_buffer == nullptr) {
err = ESP_ERR_NO_MEM;
} else {
err = reader->add_sink(temp_ring_buffer);
}
err = temp_ring_buffer == nullptr ? ESP_ERR_NO_MEM : reader->add_sink(temp_ring_buffer);
}
if (err != ESP_OK) {
@@ -524,11 +524,7 @@ void SpeakerMediaPlayer::play_file(const audio::AudioFile *media_file, bool anno
media_command.file = media_file;
media_command.is_announcement = announcement;
if (this->single_pipeline_() || announcement) {
media_command.announce = true;
} else {
media_command.announce = false;
}
media_command.announce = this->single_pipeline_() || announcement;
media_command.enqueue = enqueue;
xQueueSend(this->media_control_command_queue_, &media_command, portMAX_DELAY);
}
@@ -543,11 +539,7 @@ void SpeakerMediaPlayer::control(const media_player::MediaPlayerCall &call) {
auto ann = call.get_announcement();
media_command.is_announcement = ann.has_value() && *ann;
if (this->single_pipeline_() || media_command.is_announcement) {
media_command.announce = true;
} else {
media_command.announce = false;
}
media_command.announce = this->single_pipeline_() || media_command.is_announcement;
const auto &media_url = call.get_media_url();
if (media_url.has_value()) {
@@ -748,11 +748,7 @@ 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;
if (enqueue) {
control_command.type = MediaPlayerControlCommand::ENQUEUE_URI;
} else {
control_command.type = MediaPlayerControlCommand::PLAY_URI;
}
control_command.type = enqueue ? MediaPlayerControlCommand::ENQUEUE_URI : 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.
+1 -6
View File
@@ -95,12 +95,7 @@ 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++) {
uint8_t shift;
if (bit_order_ == BIT_ORDER_MSB_FIRST) {
shift = num_bits - 1 - i;
} else {
shift = i;
}
const uint8_t shift = this->bit_order_ == BIT_ORDER_MSB_FIRST ? num_bits - 1 - i : i;
if (clock_phase_ == CLOCK_PHASE_LEADING) {
// sampling on leading edge
+1 -5
View File
@@ -19,11 +19,7 @@ void SprinklerControllerNumber::setup() {
} else {
this->pref_ = this->make_entity_preference<float>();
if (!this->pref_.load(&value)) {
if (!std::isnan(this->initial_value_)) {
value = this->initial_value_;
} else {
value = this->traits.get_min_value();
}
value = !std::isnan(this->initial_value_) ? this->initial_value_ : this->traits.get_min_value();
}
}
this->publish_state(value);
@@ -135,11 +135,7 @@ 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++) {
if (i & 1) {
this->buffer_[i] = color565 & 0xff;
} else {
this->buffer_[i] = (color565 >> 8) & 0xff;
}
this->buffer_[i] = i & 1 ? color565 & 0xff : (color565 >> 8) & 0xff;
}
}
void SSD1331::init_reset_() { pulse_reset(this->reset_pin_, {.high_before_ms = 1, .low_ms = 10}); }
@@ -167,11 +167,7 @@ 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++) {
if (i & 1) {
this->buffer_[i] = color565 & 0xff;
} else {
this->buffer_[i] = (color565 >> 8) & 0xff;
}
this->buffer_[i] = i & 1 ? color565 & 0xff : (color565 >> 8) & 0xff;
}
}
void SSD1351::init_reset_() { pulse_reset(this->reset_pin_, {.high_before_ms = 1, .low_ms = 10}); }
@@ -18,12 +18,8 @@ class SunTextSensor final : public text_sensor::TextSensor, public PollingCompon
void set_format(const std::string &format) = delete;
void update() override {
optional<ESPTime> res;
if (this->sunrise_) {
res = this->parent_->sunrise(this->elevation_);
} else {
res = this->parent_->sunset(this->elevation_);
}
optional<ESPTime> res =
this->sunrise_ ? this->parent_->sunrise(this->elevation_) : this->parent_->sunset(this->elevation_);
if (!res) {
this->publish_state("");
return;
@@ -79,21 +79,9 @@ void TemplateDate::control(const datetime::DateCall &call) {
if (this->restore_value_) {
datetime::DateEntityRestoreState temp = {};
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_;
}
temp.year = opt_year.value_or(this->year_);
temp.month = opt_month.value_or(this->month_);
temp.day = opt_day.value_or(this->day_);
this->pref_.save(&temp);
}
@@ -106,36 +106,12 @@ void TemplateDateTime::control(const datetime::DateTimeCall &call) {
if (this->restore_value_) {
datetime::DateTimeEntityRestoreState temp = {};
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_;
}
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_);
this->pref_.save(&temp);
}
@@ -79,21 +79,9 @@ void TemplateTime::control(const datetime::TimeCall &call) {
if (this->restore_value_) {
datetime::TimeEntityRestoreState temp = {};
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_;
}
temp.hour = opt_hour.value_or(this->hour_);
temp.minute = opt_minute.value_or(this->minute_);
temp.second = opt_second.value_or(this->second_);
this->pref_.save(&temp);
}
@@ -15,11 +15,7 @@ void TemplateNumber::setup() {
} else {
this->pref_ = this->make_entity_preference<float>();
if (!this->pref_.load(&value)) {
if (!std::isnan(this->initial_value_)) {
value = this->initial_value_;
} else {
value = this->traits.get_min_value();
}
value = !std::isnan(this->initial_value_) ? this->initial_value_ : this->traits.get_min_value();
}
}
this->publish_state(value);
@@ -210,11 +210,7 @@ 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;
if (this->hw_serial_ != nullptr) {
*data = this->hw_serial_->peek();
} else {
*data = this->sw_serial_->peek_byte();
}
*data = this->hw_serial_ != nullptr ? this->hw_serial_->peek() : this->sw_serial_->peek_byte();
return true;
}
bool ESP8266UartComponent::read_array(uint8_t *data, size_t len) {
@@ -145,12 +145,7 @@ void RP2UartComponent::setup() {
this->serial_ = serial;
} else {
ESP_LOGV(TAG, "Using Hardware Serial");
SerialUART *serial;
if (hw_uart == 0) {
serial = &Serial1;
} else {
serial = &Serial2;
}
SerialUART *serial = hw_uart == 0 ? &Serial1 : &Serial2;
if (this->tx_pin_ != nullptr)
serial->setTX(this->tx_pin_->get_pin());
if (this->rx_pin_ != nullptr)
+3 -15
View File
@@ -70,11 +70,7 @@ static int ftdi_to_clkbits_am(int baudrate, uint32_t *encoded_divisor) {
}
}
baud_estimate = (24000000 + (try_divisor / 2)) / try_divisor;
if (baud_estimate < baudrate) {
baud_diff = baudrate - baud_estimate;
} else {
baud_diff = baud_estimate - baudrate;
}
baud_diff = baud_estimate < baudrate ? baudrate - baud_estimate : baud_estimate - baudrate;
if (i == 0 || baud_diff < best_baud_diff) {
best_divisor = try_divisor;
best_baud = baud_estimate;
@@ -108,19 +104,11 @@ 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;
if (divisor & 1) {
best_divisor = divisor / 2 + 1;
} else {
best_divisor = divisor / 2;
}
best_divisor = divisor & 1 ? divisor / 2 + 1 : divisor / 2;
if (best_divisor > 0x20000)
best_divisor = 0x1ffff;
best_baud = clk * 16 / clk_div / best_divisor;
if (best_baud & 1) {
best_baud = best_baud / 2 + 1;
} else {
best_baud = best_baud / 2;
}
best_baud = best_baud & 1 ? best_baud / 2 + 1 : 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
CONF_SAFE_PWM_LEVELS_SCHEMA = cv.Schema(
_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): CONF_SAFE_PWM_LEVELS_SCHEMA,
cv.Optional(CONF_SAFE_PWM_LEVELS): _SAFE_PWM_LEVELS_SCHEMA,
}
),
validate_pwm_limits,
+1 -5
View File
@@ -96,11 +96,7 @@ uint64_t Wl134Component::hex_lsb_ascii_to_uint64_(const uint8_t *text, uint8_t t
i--;
uint8_t digit = text[i];
if (digit >= 'A') {
digit = digit - 'A' + 10;
} else {
digit = digit - '0';
}
digit = digit >= 'A' ? digit - 'A' + 10 : digit - '0';
value = (value << 4) + digit;
} while (i != 0);
+1 -5
View File
@@ -23,11 +23,7 @@ void ZHLT01Climate::transmit_state() {
// Byte 7 : Power | Swing | Fan
// -- Power
if (this->mode == climate::CLIMATE_MODE_OFF) {
ir_message[7] = AC1_POWER_OFF;
} else {
ir_message[7] = AC1_POWER_ON;
}
ir_message[7] = this->mode == climate::CLIMATE_MODE_OFF ? AC1_POWER_OFF : 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 > 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}")
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}")
return config
+1 -1
View File
@@ -8,7 +8,7 @@ from esphome.const import ( # noqa: F401 # pylint: disable=unused-import
CONF_ZIGBEE_SWITCH,
)
CONF_MAX_EP_NUMBER_ZEPHYR = 8
MAX_EP_NUMBER_ZEPHYR = 8
CONF_IEEE802154_VENDOR_OUI = "ieee802154_vendor_oui"
# Keys for CORE.data storage
-2
View File
@@ -291,7 +291,6 @@ 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();
}
@@ -310,7 +309,6 @@ 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();
}
+1 -5
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 | APP_STATE_SHUTTING_DOWN); }
uint8_t get_app_state() const { return this->app_state_ & ~APP_STATE_SETUP_COMPLETE; }
/// True once Application::setup() has finished walking all components
/// and finalized the initial status flags. Before this point, the
@@ -328,10 +328,6 @@ 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,7 +595,6 @@ template<typename... Ts> class Trigger {
};
template<typename... Ts> class ActionList;
class ActionTimer;
template<typename... Ts> class Action {
public:
@@ -625,8 +624,6 @@ 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;
+47 -119
View File
@@ -11,7 +11,6 @@
#include <array>
#include <list>
#include <memory>
#include <tuple>
#include <type_traits>
#include <vector>
@@ -146,31 +145,6 @@ 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) {}
@@ -231,17 +205,37 @@ 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 passes parallel=true to allow multiple delays to coexist
// Parallel mode uses skip_cancel=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.
// 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);
// 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());
}
}
void play(const Ts &...x) override { /* ignore - see play_complex */ }
@@ -366,38 +360,6 @@ 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) {}
@@ -413,74 +375,40 @@ template<typename... Ts> class WhileAction : public Action<Ts...> {
void play_complex(const Ts &...x) final {
this->num_running_++;
// 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...);
// 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;
}
this->run_loop_(x...);
}
// Branch-end resume hook
void play(const Ts &...x) override {
// 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;
// The condition may have stopped this automation
if (this->num_running_ == 0)
return;
// An empty body cannot resume the loop
if (this->then_.empty()) {
this->play_next_(x...);
return;
}
this->run_loop_(x...);
this->then_.play(x...);
}
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;
// Branch-end resume hook: loop again or continue
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...);
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;
}
} else {
this->play_next_(x...);
}
}
void stop() override { this->then_.stop(); }
protected:
Condition<Ts...> *condition_;
ActionList<Ts...> then_;
Body *body_{nullptr};
};
// Forward declaration for RepeatLoopContinuation
+10 -40
View File
@@ -1,5 +1,7 @@
#pragma once
#include <algorithm>
#include "defines.h"
#include "component.h"
#include "helpers.h"
@@ -116,26 +118,10 @@ struct Color {
}
inline Color operator+(const Color &add) const ESPHOME_ALWAYS_INLINE {
Color ret;
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;
}
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);
return ret;
}
inline Color &operator+=(const Color &add) ESPHOME_ALWAYS_INLINE { return *this = (*this) + add; }
@@ -143,26 +129,10 @@ 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;
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;
}
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);
return ret;
}
inline Color &operator-=(const Color &subtract) ESPHOME_ALWAYS_INLINE { return *this = (*this) - subtract; }
-3
View File
@@ -98,9 +98,6 @@ 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,6 +723,9 @@
#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
+1 -5
View File
@@ -803,11 +803,7 @@ 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));
}
if (max_color_value == 0) {
saturation = 0;
} else {
saturation = delta / max_color_value;
}
saturation = max_color_value == 0 ? 0 : delta / max_color_value;
value = max_color_value;
}
+3
View File
@@ -35,6 +35,9 @@ 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,11 +110,6 @@ 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,
+2 -6
View File
@@ -18,11 +18,11 @@ namespace esphome {
class Component;
class Scheduler {
// Allow ActionTimer (delay and while) to call set_action_timeout_, which skips cancel for parallel runs.
// Allow DelayAction to call set_timer_common_ with skip_cancel=true for parallel script delays.
// 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.
friend class ActionTimer;
template<typename... Ts> friend class DelayAction;
public:
/** Set a timeout with a const char* name.
@@ -266,10 +266,6 @@ 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:
for d in ["src", "Src", "."]:
if (read_path / Path(d)).is_dir():
build_src_dir = d
break
# 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()), "."
)
build_include_dir = component.data.get("build", {}).get(
"includeDir", DEFAULT_BUILD_INCLUDE_DIR
+84 -1
View File
@@ -4,7 +4,7 @@ import argparse
import ast
import codecs
import collections
from collections.abc import Iterator
from collections.abc import Iterable, Iterator
import fnmatch
import functools
import os.path
@@ -610,6 +610,89 @@ 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,6 +212,8 @@ 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
+52 -2
View File
@@ -6,6 +6,7 @@ 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
@@ -18,6 +19,8 @@ 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.
@@ -70,6 +73,22 @@ 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.
@@ -159,6 +178,18 @@ 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"
@@ -190,14 +221,32 @@ def report(state: str, venv: Path) -> None:
)
def main() -> 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:
"""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.
@@ -214,9 +263,10 @@ def main() -> 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()
main(sys.argv[1:])
@@ -670,11 +670,11 @@ def test_uc8179_e1001_code_generation(
assert "97, 4, 3, 32, 1, 224" in main_cpp
def test_gdey0583t81_defaults_and_driver_class(
def test_uc8179_named_models(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""The GDEY0583T81 models default to 648x480 and use the UC8179 driver class."""
"""The named UC8179 models use the UC8179 driver class with their own dimensions."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
@@ -682,7 +682,12 @@ def test_gdey0583t81_defaults_and_driver_class(
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
for name in ("goodisplay-gdey0583t81-5.83", "0583BN-T81-D2"):
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)),
):
model = MODELS[name.upper()]
assert model.class_name == "EPaperUC8179"
config = run_schema_validation(
@@ -693,7 +698,7 @@ def test_gdey0583t81_defaults_and_driver_class(
"full_update_every": 5,
}
)
assert model.get_dimensions(config) == (648, 480)
assert model.get_dimensions(config) == dimensions
def test_uc8279_width_must_be_a_multiple_of_8(
Whitespace-only changes.
@@ -0,0 +1,11 @@
esphome:
name: isr-guard-esp32
esp32:
board: esp32dev
framework:
type: esp-idf
i2c:
sda: GPIO21
scl: GPIO22
@@ -0,0 +1,11 @@
esphome:
name: isr-guard-esp32c3
esp32:
board: esp32-c3-devkitm-1
framework:
type: esp-idf
i2c:
sda: GPIO5
scl: GPIO6
@@ -0,0 +1,31 @@
"""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
@@ -0,0 +1,42 @@
"""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,19 +19,6 @@ 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,5 +1,4 @@
#include <gtest/gtest.h>
#include <utility>
#include <vector>
#include "esphome/core/automation.h"
@@ -80,10 +79,10 @@ class StopOnCheckCondition : public Condition<> {
};
// Turns its condition off after a number of checks
template<typename... Ts> class CountdownCondition : public Condition<Ts...> {
class CountdownCondition : public Condition<> {
public:
explicit CountdownCondition(int passes) : passes_(passes) {}
bool check(const Ts &.../*x*/) override { return this->passes_-- > 0; }
bool check() override { return this->passes_-- > 0; }
protected:
int passes_;
@@ -178,7 +177,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);
@@ -342,7 +341,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);
@@ -380,7 +379,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);
@@ -401,246 +400,4 @@ 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,6 +567,43 @@ 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
@@ -1,134 +0,0 @@
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