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