mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 01:25:57 +00:00
Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a36a832e3 | ||
|
|
098891bef6 | ||
|
|
46f532e2bd | ||
|
|
f1a1502fb6 | ||
|
|
d3738e31f6 | ||
|
|
76d0b989e7 | ||
|
|
f7c91fad0f | ||
|
|
a7b3c34598 | ||
|
|
918fe15eb8 | ||
|
|
26c4f8d40e | ||
|
|
c097fe388e | ||
|
|
21267d7421 | ||
|
|
582adc950a | ||
|
|
613319f984 | ||
|
|
4ad3cc7c0e | ||
|
|
a033260639 | ||
|
|
24f23ba3db | ||
|
|
674d593f75 | ||
|
|
c1771e002b | ||
|
|
5529e87a40 | ||
|
|
54cae78a42 | ||
|
|
e77a3bc54d | ||
|
|
af7f95a01b | ||
|
|
daf48ce4f2 | ||
|
|
3de3f72e7c | ||
|
|
1878a69187 | ||
|
|
032c8d9618 | ||
|
|
36c079de78 | ||
|
|
bdd0a2b3b6 |
No files matched your search
@@ -3,7 +3,11 @@ name: Stale
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Full pass once a day
|
||||
- cron: "30 0 * * *"
|
||||
# Conflicts only, every 3 hours in between, so authors hear about new
|
||||
# merge conflicts the same day
|
||||
- cron: "30 3-21/3 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
# The reusable workflow authenticates as the ESPHome GitHub App, so GITHUB_TOKEN
|
||||
@@ -16,12 +20,14 @@ jobs:
|
||||
# No GITHUB_TOKEN permissions: the reusable workflow mints an ESPHome
|
||||
# GitHub App token so the labels, comments and closures come from
|
||||
# esphome[bot] instead of github-actions[bot].
|
||||
uses: esphome/workflows/.github/workflows/stale.yml@e1432c7b31fec7d24a13dc192953ebac836ed98a # main
|
||||
uses: esphome/workflows/.github/workflows/stale.yml@2026.11.0
|
||||
secrets:
|
||||
ESPHOME_GITHUB_APP_PRIVATE_KEY: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
with:
|
||||
# Live only on dev: a workflow_dispatch from any other branch is a dry run
|
||||
dry-run: ${{ github.ref != 'refs/heads/dev' }}
|
||||
# Must match the conflicts-only cron above; workflow_dispatch runs a full pass
|
||||
conflicts-only: ${{ github.event.schedule == '30 3-21/3 * * *' }}
|
||||
days-before-stale: 90
|
||||
days-before-stale-conflicting: 30
|
||||
days-before-close: 7
|
||||
|
||||
+3
-1
@@ -98,8 +98,10 @@ venv-*/
|
||||
.pioenvs
|
||||
.piolibdeps
|
||||
.pio
|
||||
.vscode/
|
||||
.vscode/*
|
||||
!.vscode/tasks.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/settings.default.json
|
||||
CMakeListsPrivate.txt
|
||||
CMakeLists.txt
|
||||
|
||||
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"recommendations": [
|
||||
// python
|
||||
"ms-python.python",
|
||||
"ms-python.pylint",
|
||||
"charliermarsh.ruff",
|
||||
// yaml
|
||||
"redhat.vscode-yaml",
|
||||
// cpp
|
||||
"ms-vscode.cpptools",
|
||||
// editorconfig
|
||||
"editorconfig.editorconfig"
|
||||
]
|
||||
}
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
// Default workspace settings. script/setup copies this file to settings.json
|
||||
// when that file does not exist yet; settings.json is not tracked, so change it freely.
|
||||
"pylint.args": [
|
||||
"--rcfile=${workspaceFolder}/pyproject.toml"
|
||||
],
|
||||
"ruff.configuration": "${workspaceFolder}/pyproject.toml",
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"yaml.customTags": [
|
||||
"!secret scalar",
|
||||
"!env_var scalar",
|
||||
"!include scalar",
|
||||
"!include mapping",
|
||||
"!lambda scalar",
|
||||
"!literal scalar",
|
||||
"!literal sequence",
|
||||
"!literal mapping",
|
||||
"!extend scalar",
|
||||
"!remove scalar",
|
||||
"!include_dir_named scalar",
|
||||
"!include_dir_list scalar",
|
||||
"!include_dir_merge_list scalar",
|
||||
"!include_dir_merge_named scalar"
|
||||
],
|
||||
"files.associations": {
|
||||
"**/.vscode/*.json": "jsonc"
|
||||
}
|
||||
}
|
||||
Vendored
+30
@@ -1,6 +1,36 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Set up dev environment",
|
||||
"type": "shell",
|
||||
"command": "script/setup",
|
||||
"windows": {
|
||||
"command": "script\\setup.bat"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Check dev environment",
|
||||
"detail": "Runs script/setup when the folder is opened, if it has not been run yet",
|
||||
"type": "shell",
|
||||
"command": "script/setup",
|
||||
"args": [
|
||||
"--if-needed"
|
||||
],
|
||||
"windows": {
|
||||
"command": "script\\setup.bat"
|
||||
},
|
||||
"runOptions": {
|
||||
"runOn": "folderOpen"
|
||||
},
|
||||
"presentation": {
|
||||
"panel": "dedicated",
|
||||
"focus": false,
|
||||
"showReuseMessage": false
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Run Dashboard",
|
||||
"type": "shell",
|
||||
|
||||
+145
-21
@@ -1,4 +1,4 @@
|
||||
from collections.abc import Awaitable, Callable, Sequence
|
||||
from collections.abc import Awaitable, Callable, Iterator, Sequence
|
||||
from contextvars import ContextVar
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
@@ -18,8 +18,10 @@ from esphome.const import (
|
||||
CONF_CONDITION,
|
||||
CONF_COUNT,
|
||||
CONF_ELSE,
|
||||
CONF_FOR,
|
||||
CONF_ID,
|
||||
CONF_IF,
|
||||
CONF_LAMBDA,
|
||||
CONF_THEN,
|
||||
CONF_TIME,
|
||||
CONF_TIMEOUT,
|
||||
@@ -1460,14 +1462,112 @@ 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
|
||||
|
||||
|
||||
def _action_ids(actions: list[tuple[str, Any]]) -> set[str]:
|
||||
"""The ids the actions act on: their ``id`` option or shorthand id, and every id a lambda action names."""
|
||||
ids: set[str] = set()
|
||||
for key, options in actions:
|
||||
if key == CONF_LAMBDA:
|
||||
ids |= _referenced_ids(options)
|
||||
elif _is_id_reference(
|
||||
target := options.get(CONF_ID) if isinstance(options, dict) else options
|
||||
):
|
||||
ids.add(target.id)
|
||||
return ids
|
||||
|
||||
|
||||
def _referenced_ids(value: Any) -> set[str]:
|
||||
# Lazy: importing esphome.config here would load it for every entity component
|
||||
from esphome.config import iter_ids
|
||||
|
||||
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.
|
||||
"""
|
||||
body = config[CONF_THEN]
|
||||
# An empty body ends the loop at once
|
||||
if not body or has_non_synchronous_actions(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):
|
||||
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],
|
||||
)
|
||||
_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
|
||||
|
||||
|
||||
@register_action(
|
||||
"while",
|
||||
WhileAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_CONDITION): validate_potentially_and_condition,
|
||||
cv.Required(CONF_THEN): validate_action_list,
|
||||
}
|
||||
cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_CONDITION): validate_potentially_and_condition,
|
||||
cv.Required(CONF_THEN): validate_action_list,
|
||||
}
|
||||
),
|
||||
_validate_while_waits,
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
@@ -1694,10 +1794,14 @@ def _check_collected(result: MockObj, collector: _InlineCollector) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _registry_key(full_config: ConfigType) -> str:
|
||||
"""The action or condition name of a validated entry."""
|
||||
return next(key for key in full_config if key != CONF_TYPE_ID)
|
||||
|
||||
|
||||
def _builder_name(full_config: ConfigType) -> str:
|
||||
"""For an error: the id is usually generated, so the registry key tells the author where to look."""
|
||||
name = next(key for key in full_config if key != CONF_TYPE_ID)
|
||||
return f"'{name}' ({full_config[CONF_TYPE_ID]})"
|
||||
return f"'{_registry_key(full_config)}' ({full_config[CONF_TYPE_ID]})"
|
||||
|
||||
|
||||
async def _build_condition_or_inline(
|
||||
@@ -1830,24 +1934,44 @@ def has_non_synchronous_actions(actions: ConfigType) -> bool:
|
||||
option that shares a name with an action, such as the ``delay:`` option of
|
||||
``speaker_source.set_playlist_delay``, is not mistaken for that action.
|
||||
"""
|
||||
return any(_is_deferring(key) for key, _ in _iter_actions(actions))
|
||||
|
||||
|
||||
def _is_deferring(key: str) -> bool:
|
||||
return key in ACTION_REGISTRY and not ACTION_REGISTRY[key].synchronous
|
||||
|
||||
|
||||
def _iter_actions(actions: Any) -> Iterator[tuple[str, Any]]:
|
||||
"""Yield ``(key, options)`` of every action in an action list, a single action or an automation.
|
||||
|
||||
Nested action lists (``then:``, ``else:``, ...) are searched; other options, including conditions, are not
|
||||
taken for actions.
|
||||
"""
|
||||
if isinstance(actions, list):
|
||||
return any(has_non_synchronous_actions(item) for item in actions)
|
||||
if isinstance(actions, dict):
|
||||
for item in actions:
|
||||
yield from _iter_actions(item)
|
||||
elif isinstance(actions, dict):
|
||||
for key, options in actions.items():
|
||||
if key in ACTION_REGISTRY and not ACTION_REGISTRY[key].synchronous:
|
||||
return True
|
||||
if _options_have_non_synchronous_actions(options):
|
||||
return True
|
||||
return False
|
||||
yield key, options
|
||||
if key == CONF_CHOOSE:
|
||||
# A list of choices, not of actions
|
||||
for choice in options:
|
||||
yield from _iter_option_actions(choice)
|
||||
else:
|
||||
yield from _iter_option_actions(options)
|
||||
|
||||
|
||||
def _options_have_non_synchronous_actions(options: Any) -> bool:
|
||||
"""Search an action's options for nested action lists, without treating option keys as actions."""
|
||||
# Options that hold conditions, not actions, though and:/or: are lists
|
||||
_CONDITION_OPTIONS = frozenset((CONF_CONDITION, CONF_ANY, CONF_ALL, CONF_IF))
|
||||
|
||||
|
||||
def _iter_option_actions(options: Any) -> Iterator[tuple[str, Any]]:
|
||||
if isinstance(options, list):
|
||||
return has_non_synchronous_actions(options)
|
||||
if isinstance(options, dict):
|
||||
return any(_options_have_non_synchronous_actions(v) for v in options.values())
|
||||
return False
|
||||
yield from _iter_actions(options)
|
||||
elif isinstance(options, dict):
|
||||
for key, value in options.items():
|
||||
if key not in _CONDITION_OPTIONS:
|
||||
yield from _iter_option_actions(value)
|
||||
|
||||
|
||||
def owning_args(args: TemplateArgsType, conf: ConfigType) -> TemplateArgsType:
|
||||
|
||||
@@ -50,11 +50,7 @@ void Animation::set_frame(int frame) {
|
||||
unsigned abs_frame = abs(frame);
|
||||
|
||||
if (abs_frame < this->animation_frame_count_) {
|
||||
if (frame >= 0) {
|
||||
this->current_frame_ = frame;
|
||||
} else {
|
||||
this->current_frame_ = this->animation_frame_count_ - abs_frame;
|
||||
}
|
||||
this->current_frame_ = frame >= 0 ? frame : this->animation_frame_count_ - abs_frame;
|
||||
}
|
||||
|
||||
this->update_data_start_();
|
||||
|
||||
@@ -726,11 +726,7 @@ class DumpBuffer {
|
||||
size_t pos() const { return pos_; }
|
||||
/// Update position after buf_append_printf call
|
||||
void set_pos(size_t pos) {
|
||||
if (pos >= CAPACITY) {
|
||||
pos_ = CAPACITY - 1;
|
||||
} else {
|
||||
pos_ = pos;
|
||||
}
|
||||
pos_ = pos >= CAPACITY ? CAPACITY - 1 : pos;
|
||||
buf_[pos_] = '\0';
|
||||
}
|
||||
|
||||
|
||||
@@ -253,12 +253,7 @@ template<typename... Ts> class BLEClientPasskeyReplyAction final : public Action
|
||||
BLEClientPasskeyReplyAction(BLEClient *ble_client) { parent_ = ble_client; }
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
uint32_t passkey;
|
||||
if (has_simple_value_) {
|
||||
passkey = this->value_.simple;
|
||||
} else {
|
||||
passkey = this->value_.template_func(x...);
|
||||
}
|
||||
const uint32_t passkey = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...);
|
||||
if (passkey > 999999)
|
||||
return;
|
||||
esp_bd_addr_t remote_bda;
|
||||
|
||||
@@ -91,11 +91,7 @@ void BM8563::get_date_(ESPTime &time) {
|
||||
|
||||
uint8_t year_byte = bcd_to_uint8(buf[3] & 0xff);
|
||||
|
||||
if (buf[2] & 0x80) {
|
||||
time.year = 1900 + year_byte;
|
||||
} else {
|
||||
time.year = 2000 + year_byte;
|
||||
}
|
||||
time.year = buf[2] & 0x80 ? 1900 + year_byte : 2000 + year_byte;
|
||||
}
|
||||
|
||||
void BM8563::set_date_(const ESPTime &time) {
|
||||
|
||||
@@ -17,7 +17,7 @@ DEPENDENCIES = ["bq76972"]
|
||||
CONF_STACK_VOLTAGE = "stack_voltage"
|
||||
|
||||
# Ordered as the chip reports them, starting at command 0x68
|
||||
CONF_TEMPERATURES = [
|
||||
TEMPERATURE_KEYS = [
|
||||
"internal_temperature",
|
||||
"cfetoff_temperature",
|
||||
"dfetoff_temperature",
|
||||
@@ -30,7 +30,7 @@ CONF_TEMPERATURES = [
|
||||
"ddsg_temperature",
|
||||
]
|
||||
|
||||
CONF_CELL_VOLTAGES = [f"cell_{i}_voltage" for i in range(1, 17)]
|
||||
CELL_VOLTAGE_KEYS = [f"cell_{i}_voltage" for i in range(1, 17)]
|
||||
|
||||
voltage_sensor_schema = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
@@ -50,8 +50,8 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_BQ76972_ID): cv.use_id(BQ76972Component),
|
||||
cv.Optional(CONF_STACK_VOLTAGE): voltage_sensor_schema,
|
||||
**{cv.Optional(key): voltage_sensor_schema for key in CONF_CELL_VOLTAGES},
|
||||
**{cv.Optional(key): temp_sensor_schema for key in CONF_TEMPERATURES},
|
||||
**{cv.Optional(key): voltage_sensor_schema for key in CELL_VOLTAGE_KEYS},
|
||||
**{cv.Optional(key): temp_sensor_schema for key in TEMPERATURE_KEYS},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -62,12 +62,12 @@ async def to_code(config: ConfigType) -> None:
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_STACK_VOLTAGE, hub.set_stack_voltage_sensor)
|
||||
|
||||
for idx, key in enumerate(CONF_TEMPERATURES):
|
||||
for idx, key in enumerate(TEMPERATURE_KEYS):
|
||||
if (conf := config.get(key)) is not None:
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(hub.set_temperature_sensor(sens, idx))
|
||||
|
||||
for idx, key in enumerate(CONF_CELL_VOLTAGES):
|
||||
for idx, key in enumerate(CELL_VOLTAGE_KEYS):
|
||||
if (conf := config.get(key)) is not None:
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(hub.set_cell_sensor(sens, idx))
|
||||
@@ -203,11 +203,7 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
// USB power state
|
||||
const char *usb_state;
|
||||
if (nrf_power_usbregstatus_vbusdet_get(NRF_POWER)) {
|
||||
if (nrf_power_usbregstatus_outrdy_get(NRF_POWER)) {
|
||||
usb_state = "ready";
|
||||
} else {
|
||||
usb_state = "connected (regulator is not ready)";
|
||||
}
|
||||
usb_state = nrf_power_usbregstatus_outrdy_get(NRF_POWER) ? "ready" : "connected (regulator is not ready)";
|
||||
} else {
|
||||
usb_state = "disconnected";
|
||||
}
|
||||
|
||||
@@ -304,11 +304,7 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2,
|
||||
// side parts
|
||||
int lhline_width = -(dxmax - dxmin) + 1;
|
||||
if (progress >= 50) {
|
||||
if (float(dymax) < float(-dxmax) * tan_a) {
|
||||
upd_dxmax = std::ceil(float(dymax) / tan_a);
|
||||
} else {
|
||||
upd_dxmax = -dxmax;
|
||||
}
|
||||
upd_dxmax = float(dymax) < float(-dxmax) * tan_a ? std::ceil(float(dymax) / tan_a) : -dxmax;
|
||||
this->horizontal_line(center_x + dxmax, center_y - dymax, lhline_width, color); // left
|
||||
if (!dymax)
|
||||
this->horizontal_line(center_x - dxmin, center_y, lhline_width, color); // right horizontal border
|
||||
@@ -319,11 +315,7 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2,
|
||||
rhline_width > lhline_width ? lhline_width : rhline_width, color);
|
||||
}
|
||||
} else {
|
||||
if (float(dymin) > float(-dxmin) * tan_a) {
|
||||
upd_dxmin = std::ceil(float(dymin) / tan_a);
|
||||
} else {
|
||||
upd_dxmin = -dxmin;
|
||||
}
|
||||
upd_dxmin = float(dymin) > float(-dxmin) * tan_a ? std::ceil(float(dymin) / tan_a) : -dxmin;
|
||||
lhline_width = -(dxmax + upd_dxmin) + 1;
|
||||
if (!dymax)
|
||||
this->horizontal_line(center_x - dxmin, center_y, lhline_width, color); // right horizontal border
|
||||
|
||||
@@ -432,11 +432,7 @@ void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) {
|
||||
const uint8_t bit_position = x % 8;
|
||||
const uint8_t pixel_bit = 0x80u >> bit_position;
|
||||
const auto original = this->buffer_[byte_position];
|
||||
if (color_to_mono(color) == 0) {
|
||||
this->buffer_[byte_position] = original & ~pixel_bit;
|
||||
} else {
|
||||
this->buffer_[byte_position] = original | pixel_bit;
|
||||
}
|
||||
this->buffer_[byte_position] = color_to_mono(color) == 0 ? original & ~pixel_bit : original | pixel_bit;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Supported models:
|
||||
- waveshare-7.5in-v2: 7.5" mono display, 800x480 pixels (EPD_7in5_V2)
|
||||
- goodisplay-gdew075t7-7.5: Good Display GDEW075T7, the same 7.5" 800x480
|
||||
panel, also sold by Seeed as 075BN-T7-D2
|
||||
- seeed-reterminal-e1001: Seeed reTerminal E1001, which uses the same
|
||||
7.5" 800x480 panel on an integrated ESP32-S3 board
|
||||
- goodisplay-gdey0583t81-5.83: Good Display GDEY0583T81 5.83" mono display,
|
||||
@@ -77,6 +79,13 @@ waveshare_7_5_v2 = uc8179.extend(
|
||||
height=480,
|
||||
)
|
||||
|
||||
# Good Display GDEW075T7, 7.5" mono 800x480, UC8179; the panel used in the Waveshare 7.5" V2.
|
||||
# Replaces the waveshare_epaper models 7.50inv2 and 7.50inv2p (use full_update_every for partial updates).
|
||||
goodisplay_gdew075t7 = waveshare_7_5_v2.extend("goodisplay-gdew075t7-7.5")
|
||||
|
||||
# Name printed on the panel's label as sold by Seeed
|
||||
goodisplay_gdew075t7.extend("075bn-t7-d2")
|
||||
|
||||
# Good Display GDEY0583T81, 5.83" mono 648x480, UC8179; also sold by Seeed as 0583BN-T81-D2.
|
||||
# Replaces the waveshare_epaper model gdey0583t81. Busy is low while busy, so set inverted: true.
|
||||
goodisplay_gdey0583t81 = uc8179.extend(
|
||||
|
||||
@@ -392,11 +392,7 @@ void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
|
||||
void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶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);
|
||||
if (param.status == ESP_BT_STATUS_DONE) {
|
||||
this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
|
||||
} else {
|
||||
this->scan_set_param_failed_ = param.status;
|
||||
}
|
||||
this->scan_set_param_failed_ = param.status == ESP_BT_STATUS_DONE ? ESP_BT_STATUS_SUCCESS : param.status;
|
||||
}
|
||||
|
||||
void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m) {
|
||||
|
||||
@@ -107,11 +107,7 @@ void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) {
|
||||
GPOC = 1 << num;
|
||||
}
|
||||
} else {
|
||||
if (value != pin->is_inverted()) {
|
||||
GP16O = GP16O | 1;
|
||||
} else {
|
||||
GP16O = GP16O & ~1;
|
||||
}
|
||||
GP16O = value != pin->is_inverted() ? GP16O | 1 : GP16O & ~1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,16 +137,8 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
|
||||
isr_gpf(num) = isr_gpf(num) & ~(1 << GPFPU);
|
||||
}
|
||||
} else {
|
||||
if (flags & gpio::FLAG_OUTPUT) {
|
||||
GP16E = GP16E | 1;
|
||||
} else {
|
||||
GP16E = GP16E & ~1;
|
||||
}
|
||||
if (flags & gpio::FLAG_PULLDOWN) {
|
||||
GPF16 = GPF16 | (1 << GP16FPD);
|
||||
} else {
|
||||
GPF16 = GPF16 & ~(1 << GP16FPD);
|
||||
}
|
||||
GP16E = flags & gpio::FLAG_OUTPUT ? GP16E | 1 : GP16E & ~1;
|
||||
GPF16 = flags & gpio::FLAG_PULLDOWN ? GPF16 | (1 << GP16FPD) : GPF16 & ~(1 << GP16FPD);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -182,11 +182,7 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!has_char) {
|
||||
min_x = glyph->offset_x;
|
||||
} else {
|
||||
min_x = std::min(min_x, x + glyph->offset_x);
|
||||
}
|
||||
min_x = !has_char ? glyph->offset_x : std::min(min_x, x + glyph->offset_x);
|
||||
x += glyph->advance;
|
||||
|
||||
has_char = true;
|
||||
|
||||
@@ -294,11 +294,7 @@ void GraphLegend::init(Graph *g) {
|
||||
} else if (valpos == VALUE_POSITION_TYPE_BESIDE) {
|
||||
this->xs_ = txtw + valw;
|
||||
}
|
||||
if (dir == DIRECTION_TYPE_VERTICAL) {
|
||||
this->width_ = this->xs_;
|
||||
} else {
|
||||
this->width_ = this->xs_ * n;
|
||||
}
|
||||
this->width_ = dir == DIRECTION_TYPE_VERTICAL ? this->xs_ : this->xs_ * n;
|
||||
} else {
|
||||
this->xs_ = w / n;
|
||||
this->x0_ = this->xs_ / 2;
|
||||
@@ -319,11 +315,7 @@ void GraphLegend::init(Graph *g) {
|
||||
}
|
||||
this->height_ = this->ys_ * n;
|
||||
}
|
||||
if (dir == DIRECTION_TYPE_HORIZONTAL) {
|
||||
this->height_ = this->ys_;
|
||||
} else {
|
||||
this->height_ = this->ys_ * n;
|
||||
}
|
||||
this->height_ = dir == DIRECTION_TYPE_HORIZONTAL ? this->ys_ : this->ys_ * n;
|
||||
} else {
|
||||
this->ys_ = h / n;
|
||||
}
|
||||
|
||||
@@ -60,21 +60,17 @@ void GroveMotorDriveTB6612FNG::set_i2c_addr(uint8_t addr) {
|
||||
void GroveMotorDriveTB6612FNG::dc_motor_run(uint8_t channel, int16_t speed) {
|
||||
speed = clamp<int16_t>(speed, -255, 255);
|
||||
|
||||
buffer_[0] = channel;
|
||||
if (speed >= 0) {
|
||||
buffer_[1] = speed;
|
||||
} else {
|
||||
buffer_[1] = (uint8_t) (-speed);
|
||||
}
|
||||
this->buffer_[0] = channel;
|
||||
this->buffer_[1] = speed >= 0 ? speed : (uint8_t) (-speed);
|
||||
|
||||
if (speed >= 0) {
|
||||
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CW, buffer_, 2) != i2c::ERROR_OK) {
|
||||
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CW, this->buffer_, 2) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Run motor failed!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CCW, buffer_, 2) != i2c::ERROR_OK) {
|
||||
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CCW, this->buffer_, 2) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Run motor failed!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
|
||||
@@ -294,11 +294,7 @@ bool HitachiClimate::parse_swing_(const uint8_t remote_state[]) {
|
||||
HITACHI_AC424_SWINGH_SIZE);
|
||||
ESP_LOGV(TAG, "SwingH: %02X %02X", remote_state[HITACHI_AC424_SWINGH_BYTE], swing_modeh);
|
||||
|
||||
if ((swing_modeh & 0x3) == 0x3) {
|
||||
this->swing_mode = climate::CLIMATE_SWING_OFF;
|
||||
} else {
|
||||
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
|
||||
}
|
||||
this->swing_mode = (swing_modeh & 0x3) == 0x3 ? climate::CLIMATE_SWING_OFF : climate::CLIMATE_SWING_HORIZONTAL;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -73,18 +73,9 @@ void HTU21DComponent::update() {
|
||||
|
||||
this->status_clear_warning();
|
||||
|
||||
// HTU21D does have a heater module but does not have heater level
|
||||
// Setting heater level to 1 in case the heater is ON
|
||||
uint8_t heater_level = 0;
|
||||
if (this->sensor_model_ == HTU21D_SENSOR_MODEL_HTU21D) {
|
||||
if (this->is_heater_enabled()) {
|
||||
heater_level = 1;
|
||||
} else {
|
||||
heater_level = 0;
|
||||
}
|
||||
} else {
|
||||
heater_level = this->get_heater_level();
|
||||
}
|
||||
// HTU21D has a heater but no heater level, so report 1 while the heater is on
|
||||
const uint8_t heater_level =
|
||||
this->sensor_model_ == HTU21D_SENSOR_MODEL_HTU21D ? this->is_heater_enabled() : this->get_heater_level();
|
||||
|
||||
ESP_LOGD(TAG, "Heater Level=%d", heater_level);
|
||||
|
||||
|
||||
@@ -325,6 +325,10 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
# Re-enable the I2C driver (excluded by default to save compile time)
|
||||
include_builtin_idf_component("esp_driver_i2c")
|
||||
if get_esp32_variant() == VARIANT_ESP32:
|
||||
# ACK_ERR interrupt storm after a NACK; see i2c_bus_esp_idf.cpp
|
||||
cg.add_define("USE_I2C_ISR_GUARD")
|
||||
cg.add_build_flag("-Wl,--wrap=esp_intr_alloc_intrstatus")
|
||||
if CORE.is_host:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -24,11 +24,7 @@ void ArduinoI2CBus::setup() {
|
||||
// I2C controller = (gpio / 2) % 2: even pairs (0-1,4-5,...) → I2C0, odd pairs (2-3,6-7,...) → I2C1
|
||||
// RP2040 datasheet Table 2 (section 1.4.3): https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf
|
||||
// RP2350 datasheet Table 7 (section 9.4): https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf
|
||||
if ((this->sda_pin_ / 2) % 2 == 0) {
|
||||
this->wire_ = &Wire;
|
||||
} else {
|
||||
this->wire_ = &Wire1;
|
||||
}
|
||||
this->wire_ = (this->sda_pin_ / 2) % 2 == 0 ? &Wire : &Wire1;
|
||||
#endif
|
||||
|
||||
this->set_pins_and_clock_();
|
||||
|
||||
@@ -10,6 +10,70 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_I2C_ISR_GUARD
|
||||
#include <esp_intr_alloc.h>
|
||||
#include <hal/i2c_ll.h> // declares i2c_periph_signal (IDF 5: soc/, IDF 6: hal/)
|
||||
#if SOC_I2C_SUPPORT_HW_FSM_RST || I2C_LL_SUPPORT_HW_FSM_RST
|
||||
#error "This chip has an I2C FSM reset; the ISR guard is no longer needed"
|
||||
#endif
|
||||
|
||||
namespace esphome::i2c {
|
||||
// Without an FSM reset ACK_ERR can stay raised and the IDF ISR never masks it (esp-idf#17690). Mask the master
|
||||
// events until the next transaction when an error is raised both before and after the handler clears it.
|
||||
static constexpr uint32_t I2C_ERROR_INTR = I2C_LL_INTR_NACK | I2C_LL_INTR_TIMEOUT | I2C_LL_INTR_ARBITRATION;
|
||||
static_assert((I2C_LL_MASTER_EVENT_INTR & I2C_ERROR_INTR) == I2C_ERROR_INTR,
|
||||
"The master event mask must cover the error interrupts");
|
||||
|
||||
struct I2CIsrGuard {
|
||||
intr_handler_t handler;
|
||||
void *arg;
|
||||
i2c_dev_t *dev;
|
||||
};
|
||||
static I2CIsrGuard i2c_isr_guards[SOC_HP_I2C_NUM]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
static void IRAM_ATTR i2c_isr_guard(void *arg) {
|
||||
auto *guard = static_cast<I2CIsrGuard *>(arg);
|
||||
uint32_t before;
|
||||
i2c_ll_get_intr_mask(guard->dev, &before);
|
||||
guard->handler(guard->arg);
|
||||
uint32_t after;
|
||||
i2c_ll_get_intr_mask(guard->dev, &after);
|
||||
if (before & after & I2C_ERROR_INTR)
|
||||
i2c_ll_disable_intr_mask(guard->dev, I2C_LL_MASTER_EVENT_INTR);
|
||||
}
|
||||
|
||||
static int i2c_port_for_source(int source) {
|
||||
for (int port = 0; port < SOC_HP_I2C_NUM; port++) {
|
||||
if (source == i2c_periph_signal[port].irq)
|
||||
return port;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
} // namespace esphome::i2c
|
||||
|
||||
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
extern "C" {
|
||||
esp_err_t __real_esp_intr_alloc_intrstatus(int source, int flags, uint32_t reg, uint32_t mask, intr_handler_t handler,
|
||||
void *arg, intr_handle_t *ret_handle);
|
||||
|
||||
esp_err_t __wrap_esp_intr_alloc_intrstatus(int source, int flags, uint32_t reg, uint32_t mask, intr_handler_t handler,
|
||||
void *arg, intr_handle_t *ret_handle) {
|
||||
int port = esphome::i2c::i2c_port_for_source(source);
|
||||
if (port < 0)
|
||||
return __real_esp_intr_alloc_intrstatus(source, flags, reg, mask, handler, arg, ret_handle);
|
||||
auto &guard = esphome::i2c::i2c_isr_guards[port];
|
||||
auto previous = guard;
|
||||
guard = {handler, arg, I2C_LL_GET_HW(port)};
|
||||
esp_err_t err =
|
||||
__real_esp_intr_alloc_intrstatus(source, flags, reg, mask, esphome::i2c::i2c_isr_guard, &guard, ret_handle);
|
||||
if (err != ESP_OK)
|
||||
guard = previous; // a failed allocation leaves the live guard alone
|
||||
return err;
|
||||
}
|
||||
} // extern "C"
|
||||
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
#endif // USE_I2C_ISR_GUARD
|
||||
|
||||
namespace esphome::i2c {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "i2c");
|
||||
@@ -116,6 +180,12 @@ void IDFI2CBus::dump_config() {
|
||||
if (this->timeout_ > 0) {
|
||||
ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 "us", this->timeout_);
|
||||
}
|
||||
#ifdef USE_I2C_ISR_GUARD
|
||||
// Null when the IDF driver no longer allocates its interrupt through the wrapped call
|
||||
if (i2c_isr_guards[this->port_].handler == nullptr) {
|
||||
ESP_LOGW(TAG, " Interrupt guard inactive");
|
||||
}
|
||||
#endif
|
||||
switch (this->recovery_result_) {
|
||||
case RECOVERY_COMPLETED:
|
||||
ESP_LOGCONFIG(TAG, " Recovery: bus successfully recovered");
|
||||
|
||||
@@ -1043,11 +1043,7 @@ void IT8951Display::set_mono_pixel_(uint16_t x, uint16_t y, bool value) const {
|
||||
void IT8951Display::set_gray_pixel_(uint16_t x, uint16_t y, uint8_t nibble) const {
|
||||
const uint32_t index = static_cast<uint32_t>(y) * this->row_width_ + (static_cast<uint32_t>(x) >> 1);
|
||||
uint8_t buf = this->buffer_[index];
|
||||
if (x & 0x1) {
|
||||
buf = (buf & 0xF0) | nibble;
|
||||
} else {
|
||||
buf = (buf & 0x0F) | static_cast<uint8_t>(nibble << 4);
|
||||
}
|
||||
buf = x & 0x1 ? (buf & 0xF0) | nibble : (buf & 0x0F) | static_cast<uint8_t>(nibble << 4);
|
||||
this->buffer_[index] = buf;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,6 @@ CONF_MAX_MOVE_DISTANCE = "max_move_distance"
|
||||
CONF_MAX_STILL_DISTANCE = "max_still_distance"
|
||||
CONF_BACKGROUND_CORRECTION = "background_correction"
|
||||
CONF_BACKGROUND_CORRECTION_DURATION = "background_correction_duration"
|
||||
CONF_MOVE_THRESHOLDS = [f"g{x}_move_threshold" for x in range(9)]
|
||||
CONF_STILL_THRESHOLDS = [f"g{x}_still_threshold" for x in range(9)]
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
@@ -54,22 +52,6 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
for i in range(9):
|
||||
CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_MOVE_THRESHOLDS[i]): cv.invalid(
|
||||
f"The '{CONF_MOVE_THRESHOLDS[i]}' option has been moved to the '{CONF_MOVE_THRESHOLDS[i]}'"
|
||||
f" number component"
|
||||
),
|
||||
cv.Optional(CONF_STILL_THRESHOLDS[i]): cv.invalid(
|
||||
f"The '{CONF_STILL_THRESHOLDS[i]}' option has been moved to the '{CONF_STILL_THRESHOLDS[i]}'"
|
||||
f" number component"
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
CONFIG_SCHEMA.extend(uart.UART_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
@@ -17,8 +17,6 @@ CONF_LD2412_ID = "ld2412_id"
|
||||
|
||||
CONF_MAX_MOVE_DISTANCE = "max_move_distance"
|
||||
CONF_MAX_STILL_DISTANCE = "max_still_distance"
|
||||
CONF_MOVE_THRESHOLDS = [f"g{x}_move_threshold" for x in range(9)]
|
||||
CONF_STILL_THRESHOLDS = [f"g{x}_still_threshold" for x in range(9)]
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
|
||||
@@ -7,7 +7,7 @@ from esphome.types import ConfigType
|
||||
from .. import CONF_LD2420_ID, LD2420Component
|
||||
|
||||
CONF_OPERATING_MODE = "operating_mode"
|
||||
CONF_SELECTS = [
|
||||
SELECT_OPTIONS = [
|
||||
"Normal",
|
||||
"Calibrate",
|
||||
"Simple",
|
||||
@@ -29,6 +29,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
await selects(
|
||||
CONF_OPERATING_MODE,
|
||||
hub.set_operating_mode_select,
|
||||
options=CONF_SELECTS,
|
||||
options=SELECT_OPTIONS,
|
||||
apply="change_operating_mode(static_cast<ld2420::OpMode>(index + 1))",
|
||||
)
|
||||
@@ -61,6 +61,8 @@ from .const import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
# Fault handlers the crash handler wraps (BK7238, BK7231N)
|
||||
BK_TRAP_HANDLERS = ("bk_trap_udef", "bk_trap_pabt", "bk_trap_dabt")
|
||||
# Timing functions RTL8710B builds replace (rtl8710b_micros.c)
|
||||
RTL8710B_TIME_WRAPS = ("millis", "micros", "delay", "delayMicroseconds")
|
||||
# Families with a crash handler; the Cortex-M ones install it at run time (no wraps).
|
||||
CRASH_HANDLER_FAMILIES = (
|
||||
FAMILY_BK7231N,
|
||||
@@ -158,6 +160,16 @@ def _update_core_data(config):
|
||||
return config
|
||||
|
||||
|
||||
def FILTER_SOURCE_FILES() -> list[str]:
|
||||
"""Leave the RTL8710B timing wrappers out of every other family's build."""
|
||||
if (data := CORE.data.get(KEY_LIBRETINY)) is None or data.get(KEY_FAMILY) in (
|
||||
None,
|
||||
FAMILY_RTL8710B,
|
||||
):
|
||||
return []
|
||||
return ["rtl8710b_micros.c"]
|
||||
|
||||
|
||||
def get_libretiny_component(core_obj=None):
|
||||
return (core_obj or CORE).data[KEY_LIBRETINY][KEY_COMPONENT]
|
||||
|
||||
@@ -529,6 +541,9 @@ async def component_to_code(config):
|
||||
# RTL8720C (ambz2) requires FreeRTOS 10.x so this only applies to RTL8710B
|
||||
if config[CONF_FAMILY] == FAMILY_RTL8710B:
|
||||
cg.add_platformio_option("custom_versions.freertos", "8.2.3")
|
||||
# millis(), micros() and delays on the system timer and CPU cycles (see rtl8710b_micros.c)
|
||||
for func in RTL8710B_TIME_WRAPS:
|
||||
cg.add_build_flag(f"-Wl,--wrap={func}")
|
||||
|
||||
# LibreTiny bundles BDK 3.0.33 for these two but already builds BK7238 with 3.0.78,
|
||||
# where STA+AP works (esphome#19430), the web server OTA does not trip the watchdog
|
||||
|
||||
@@ -34,6 +34,33 @@ static int IRAM_ATTR flags_to_mode(gpio::Flags flags) {
|
||||
// The ISR copy is the pin itself, so both share the stored mode and level.
|
||||
ISRInternalGPIOPin ArduinoInternalGPIOPin::to_isr() const { return ISRInternalGPIOPin(this); }
|
||||
|
||||
#if defined(USE_LIBRETINY_VARIANT_RTL8710B) && !defined(CLANG_TIDY)
|
||||
// RTL8710B GPIO interrupts on one edge only and LibreTiny maps CHANGE to rising, so flip the edge on every interrupt.
|
||||
// Mirrors LibreTiny 1.14.0 wiring_irq.c (PinData is the SDK handler id); drop once libretiny-eu/libretiny#155 is fixed.
|
||||
// Re-reads the level after each write; a pin still bouncing after four tries is corrected at its next edge.
|
||||
static void IRAM_ATTR rtl8710b_arm_next_edge(gpio_irq_t *irq) {
|
||||
uint32_t level = GPIO_ReadDataBit(irq->pin);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
// gpio_irq_set() without its flash wrapper, so the handler runs from RAM and mask ROM only
|
||||
GPIO_INTMode(irq->pin, ENABLE, GPIO_INT_Trigger_EDGE,
|
||||
level ? GPIO_INT_POLARITY_ACTIVE_LOW : GPIO_INT_POLARITY_ACTIVE_HIGH, GPIO_INT_DEBOUNCE_ENABLE);
|
||||
const uint32_t now = GPIO_ReadDataBit(irq->pin);
|
||||
if (now == level)
|
||||
return;
|
||||
level = now;
|
||||
}
|
||||
}
|
||||
|
||||
static void IRAM_ATTR rtl8710b_change_handler(uint32_t id, gpio_irq_event event) {
|
||||
auto *data = reinterpret_cast<PinData *>(id); // NOLINT(performance-no-int-to-ptr)
|
||||
// A later attach with another mode keeps this handler
|
||||
if (data->irqMode == CHANGE)
|
||||
rtl8710b_arm_next_edge(data->irq);
|
||||
if (data->irqHandler != nullptr)
|
||||
reinterpret_cast<void (*)(void *)>(data->irqHandler)(data->irqParam);
|
||||
}
|
||||
#endif
|
||||
|
||||
void ArduinoInternalGPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||
PinStatus arduino_mode = (PinStatus) 255;
|
||||
switch (type) {
|
||||
@@ -55,6 +82,20 @@ void ArduinoInternalGPIOPin::attach_interrupt_(void (*func)(void *), void *arg,
|
||||
}
|
||||
|
||||
attachInterruptParam(pin_, func, arduino_mode, arg);
|
||||
#if defined(USE_LIBRETINY_VARIANT_RTL8710B) && !defined(CLANG_TIDY)
|
||||
if (arduino_mode == CHANGE) {
|
||||
PinInfo *info = pinInfo(this->pin_);
|
||||
PinData *data = info != nullptr ? info->data : nullptr;
|
||||
if (data != nullptr && data->irq != nullptr) {
|
||||
// The interrupt is already live: keep it from arming an edge between the level read and the write below
|
||||
const uint32_t primask = __get_PRIMASK();
|
||||
__disable_irq();
|
||||
GPIO_UserRegIrq(data->irq->pin, reinterpret_cast<void *>(rtl8710b_change_handler), data);
|
||||
rtl8710b_arm_next_edge(data->irq);
|
||||
__set_PRIMASK(primask);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_BK72XX
|
||||
|
||||
@@ -94,7 +94,10 @@ __attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); }
|
||||
__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(::micros()); }
|
||||
|
||||
// Per-variant millis() fast path — matches MillisInternal::get().
|
||||
#if defined(USE_RTL87XX) || defined(USE_LN882X)
|
||||
#if defined(USE_LIBRETINY_VARIANT_RTL8710B)
|
||||
// The RTL8710B tick is 976.6 us; millis() comes from the system timer instead (rtl8710b_micros.c)
|
||||
__attribute__((always_inline)) inline uint32_t millis() { return static_cast<uint32_t>(::millis()); }
|
||||
#elif defined(USE_RTL87XX) || defined(USE_LN882X)
|
||||
static_assert(configTICK_RATE_HZ == 1000, "millis() fast path requires 1 kHz FreeRTOS tick");
|
||||
__attribute__((always_inline)) inline uint32_t millis() {
|
||||
// xTaskGetTickCountFromISR is mandatory in interrupt context per the FreeRTOS API contract.
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// RTL8710B: the FreeRTOS tick is 1/1024 s, so millis(), micros() and delay() use the 32.768 kHz system timer instead
|
||||
// and delayMicroseconds() the DWT cycle counter. Linked with -Wl,--wrap (RTL8710B_TIME_WRAPS in __init__.py).
|
||||
|
||||
#ifdef USE_LIBRETINY_VARIANT_RTL8710B
|
||||
|
||||
#include <stdint.h>
|
||||
#include <FreeRTOS.h>
|
||||
#include <task.h>
|
||||
|
||||
extern uint32_t SystemCoreClock;
|
||||
__attribute__((long_call)) uint32_t SYSTIMER_TickGet(void); // mask ROM, rtl8710b_delay.h
|
||||
|
||||
#define DEMCR (*(volatile uint32_t *) 0xE000EDFC)
|
||||
#define DEMCR_TRCENA (1UL << 24)
|
||||
#define DWT_CTRL (*(volatile uint32_t *) 0xE0001000)
|
||||
#define DWT_CYCCNT (*(volatile uint32_t *) 0xE0001004)
|
||||
// Interrupt handlers call these, so they live in RAM like IRAM_ATTR (hal.h)
|
||||
#define RTL8710B_RAM_TEXT __attribute__((noinline, section(".sram.text")))
|
||||
|
||||
// Extends the 32-bit timer, which wraps every 36.4 hours; the main loop calls millis() far more often
|
||||
static inline __attribute__((always_inline)) uint64_t timer_ticks(void) {
|
||||
static uint64_t ticks;
|
||||
uint32_t primask;
|
||||
__asm volatile("mrs %0, primask\n cpsid i" : "=r"(primask)::"memory");
|
||||
ticks += (uint32_t) (SYSTIMER_TickGet() - (uint32_t) ticks);
|
||||
const uint64_t now = ticks;
|
||||
__asm volatile("msr primask, %0" ::"r"(primask) : "memory");
|
||||
return now;
|
||||
}
|
||||
|
||||
RTL8710B_RAM_TEXT unsigned long __wrap_micros(void) { // NOLINT(readability-identifier-naming)
|
||||
// 1000000 / 32768 = 15625 / 512
|
||||
return (uint32_t) ((timer_ticks() * 15625) >> 9);
|
||||
}
|
||||
|
||||
RTL8710B_RAM_TEXT unsigned long __wrap_millis(void) { // NOLINT(readability-identifier-naming)
|
||||
// 1000 / 32768 = 125 / 4096
|
||||
return (uint32_t) ((timer_ticks() * 125) >> 12);
|
||||
}
|
||||
|
||||
void __wrap_delay(uint32_t ms) { // NOLINT(readability-identifier-naming)
|
||||
if (ms == 0) {
|
||||
vTaskDelay(0);
|
||||
return;
|
||||
}
|
||||
// vTaskDelay() counts the partial tick, so sleep to a deadline; +1 covers the elapsed part of the current count
|
||||
const uint64_t deadline = timer_ticks() + ((uint64_t) ms * 32768 + 999) / 1000 + 1;
|
||||
// Sleeps are capped at 1024 s so a delay longer than the 36.4 hour timer wrap still sees every wrap
|
||||
for (uint64_t now = timer_ticks(); now < deadline; now = timer_ticks()) {
|
||||
const uint64_t ticks = (deadline - now + 31) / 32;
|
||||
vTaskDelay((TickType_t) (ticks < 0x100000 ? ticks : 0x100000));
|
||||
}
|
||||
}
|
||||
|
||||
RTL8710B_RAM_TEXT void __wrap_delayMicroseconds(unsigned int us) { // NOLINT(readability-identifier-naming)
|
||||
DEMCR |= DEMCR_TRCENA;
|
||||
DWT_CTRL |= 1;
|
||||
uint32_t start = DWT_CYCCNT;
|
||||
// Rounded up: the BW12 runs at 62.5 MHz
|
||||
uint64_t cycles = (uint64_t) us * ((SystemCoreClock + 999999) / 1000000);
|
||||
// Steps of at most 2^31 cycles keep the 32-bit difference from wrapping
|
||||
while (cycles > 0) {
|
||||
const uint32_t step = cycles > 0x80000000UL ? 0x80000000UL : (uint32_t) cycles;
|
||||
while (DWT_CYCCNT - start < step) {
|
||||
}
|
||||
start += step;
|
||||
cycles -= step;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // USE_LIBRETINY_VARIANT_RTL8710B
|
||||
@@ -115,20 +115,12 @@ bool MAX44009Sensor::set_low_power_mode() {
|
||||
|
||||
uint8_t MAX44009Sensor::read_(uint8_t reg) {
|
||||
uint8_t data = 0;
|
||||
if (!this->read_byte(reg, &data)) {
|
||||
this->error_ = MAX44009_ERROR_WIRE_REQUEST;
|
||||
} else {
|
||||
this->error_ = MAX44009_OK;
|
||||
}
|
||||
this->error_ = this->read_byte(reg, &data) ? MAX44009_OK : MAX44009_ERROR_WIRE_REQUEST;
|
||||
return data;
|
||||
}
|
||||
|
||||
void MAX44009Sensor::write_(uint8_t reg, uint8_t value) {
|
||||
if (!this->write_byte(reg, value)) {
|
||||
this->error_ = MAX44009_ERROR_WIRE_REQUEST;
|
||||
} else {
|
||||
this->error_ = MAX44009_OK;
|
||||
}
|
||||
this->error_ = this->write_byte(reg, value) ? MAX44009_OK : MAX44009_ERROR_WIRE_REQUEST;
|
||||
}
|
||||
|
||||
} // namespace esphome::max44009
|
||||
@@ -273,11 +273,7 @@ void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) {
|
||||
}
|
||||
}
|
||||
} else if (this->orientation_ == 1) {
|
||||
if (this->flip_x_) {
|
||||
b = pixels[7 - col];
|
||||
} else {
|
||||
b = pixels[col];
|
||||
}
|
||||
b = this->flip_x_ ? pixels[7 - col] : pixels[col];
|
||||
} else if (this->orientation_ == 2) {
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
if (this->flip_x_) {
|
||||
|
||||
@@ -304,12 +304,8 @@ canbus::Error MCP251XFD::send_message(canbus::CanFrame *frame) {
|
||||
|
||||
// The object follows the two write command bytes, so it goes out without a copy.
|
||||
std::array<uint8_t, 2 + MAX_MESSAGE_SIZE> transfer;
|
||||
uint32_t id;
|
||||
if (frame->use_extended_id) {
|
||||
id = (frame->can_id >> 18) | ((frame->can_id & EXTENDED_ID_LOW_MASK) << 11);
|
||||
} else {
|
||||
id = frame->can_id;
|
||||
}
|
||||
const uint32_t id =
|
||||
frame->use_extended_id ? (frame->can_id >> 18) | ((frame->can_id & EXTENDED_ID_LOW_MASK) << 11) : frame->can_id;
|
||||
uint32_t flags = frame->can_data_length_code;
|
||||
assign_bits(flags, MESSAGE_IDE, frame->use_extended_id);
|
||||
assign_bits(flags, MESSAGE_RTR, frame->remote_transmission_request);
|
||||
|
||||
@@ -30,7 +30,7 @@ def _get_data() -> MicronovaData:
|
||||
return CORE.data[DOMAIN]
|
||||
|
||||
|
||||
CONF_MICRONOVA_ID = f"{DOMAIN}_id"
|
||||
CONF_MICRONOVA_ID = "micronova_id"
|
||||
CONF_ENABLE_RX_PIN = "enable_rx_pin"
|
||||
CONF_MEMORY_LOCATION = "memory_location"
|
||||
CONF_MEMORY_ADDRESS = "memory_address"
|
||||
|
||||
@@ -79,52 +79,31 @@ void MICS4514Component::update() {
|
||||
}
|
||||
|
||||
if (this->nitrogen_dioxide_sensor_ != nullptr) {
|
||||
float nitrogendioxide = 0.0f;
|
||||
if (ox_f < 0.3f) {
|
||||
nitrogendioxide = 0.0;
|
||||
} else {
|
||||
nitrogendioxide = 0.164f * powf(ox_f, 0.975f);
|
||||
}
|
||||
const float nitrogendioxide = ox_f < 0.3f ? 0.0f : 0.164f * powf(ox_f, 0.975f);
|
||||
this->nitrogen_dioxide_sensor_->publish_state(nitrogendioxide);
|
||||
}
|
||||
|
||||
if (this->methane_sensor_ != nullptr) {
|
||||
float methane = 0.0f;
|
||||
if (red_f > 0.9f || red_f < 0.5f) { // outside the range->unlikely
|
||||
methane = 0.0;
|
||||
} else {
|
||||
methane = 630 / powf(red_f, 4.4f);
|
||||
}
|
||||
// 0 outside the sensor's range
|
||||
const float methane = (red_f > 0.9f || red_f < 0.5f) ? 0.0f : 630 / powf(red_f, 4.4f);
|
||||
this->methane_sensor_->publish_state(methane);
|
||||
}
|
||||
|
||||
if (this->ethanol_sensor_ != nullptr) {
|
||||
float ethanol = 0.0f;
|
||||
if (red_f > 1.0f || red_f < 0.02f) { // outside the range->unlikely
|
||||
ethanol = 0.0;
|
||||
} else {
|
||||
ethanol = 1.52f / powf(red_f, 1.55f);
|
||||
}
|
||||
// 0 outside the sensor's range
|
||||
const float ethanol = (red_f > 1.0f || red_f < 0.02f) ? 0.0f : 1.52f / powf(red_f, 1.55f);
|
||||
this->ethanol_sensor_->publish_state(ethanol);
|
||||
}
|
||||
|
||||
if (this->hydrogen_sensor_ != nullptr) {
|
||||
float hydrogen = 0.0f;
|
||||
if (red_f > 0.9f || red_f < 0.02f) { // outside the range->unlikely
|
||||
hydrogen = 0.0;
|
||||
} else {
|
||||
hydrogen = 0.85f / powf(red_f, 1.75f);
|
||||
}
|
||||
// 0 outside the sensor's range
|
||||
const float hydrogen = (red_f > 0.9f || red_f < 0.02f) ? 0.0f : 0.85f / powf(red_f, 1.75f);
|
||||
this->hydrogen_sensor_->publish_state(hydrogen);
|
||||
}
|
||||
|
||||
if (this->ammonia_sensor_ != nullptr) {
|
||||
float ammonia = 0.0f;
|
||||
if (red_f > 0.98f || red_f < 0.2532f) { // outside the ammonia range->unlikely
|
||||
ammonia = 0.0;
|
||||
} else {
|
||||
ammonia = 0.9f / powf(red_f, 4.6f);
|
||||
}
|
||||
// 0 outside the sensor's range
|
||||
const float ammonia = (red_f > 0.98f || red_f < 0.2532f) ? 0.0f : 0.9f / powf(red_f, 4.6f);
|
||||
this->ammonia_sensor_->publish_state(ammonia);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,21 +147,9 @@ void MipiRgb::common_setup_() {
|
||||
}
|
||||
config.data_width = data_pin_count;
|
||||
config.disp_gpio_num = GPIO_NUM_NC;
|
||||
if (this->hsync_pin_) {
|
||||
config.hsync_gpio_num = static_cast<gpio_num_t>(this->hsync_pin_->get_pin());
|
||||
} else {
|
||||
config.hsync_gpio_num = GPIO_NUM_NC;
|
||||
}
|
||||
if (this->vsync_pin_) {
|
||||
config.vsync_gpio_num = static_cast<gpio_num_t>(this->vsync_pin_->get_pin());
|
||||
} else {
|
||||
config.vsync_gpio_num = GPIO_NUM_NC;
|
||||
}
|
||||
if (this->de_pin_) {
|
||||
config.de_gpio_num = static_cast<gpio_num_t>(this->de_pin_->get_pin());
|
||||
} else {
|
||||
config.de_gpio_num = GPIO_NUM_NC;
|
||||
}
|
||||
config.hsync_gpio_num = this->hsync_pin_ ? static_cast<gpio_num_t>(this->hsync_pin_->get_pin()) : GPIO_NUM_NC;
|
||||
config.vsync_gpio_num = this->vsync_pin_ ? static_cast<gpio_num_t>(this->vsync_pin_->get_pin()) : GPIO_NUM_NC;
|
||||
config.de_gpio_num = this->de_pin_ ? static_cast<gpio_num_t>(this->de_pin_->get_pin()) : GPIO_NUM_NC;
|
||||
config.pclk_gpio_num = static_cast<gpio_num_t>(this->pclk_pin_->get_pin());
|
||||
esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_);
|
||||
if (err == ESP_OK)
|
||||
|
||||
@@ -174,18 +174,10 @@ void MitsubishiClimate::transmit_state() {
|
||||
remote_state[9] = 1;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_MEDIUM:
|
||||
if (this->fan_mode_ == MITSUBISHI_FAN_3L) {
|
||||
remote_state[9] = 2;
|
||||
} else {
|
||||
remote_state[9] = 3;
|
||||
}
|
||||
remote_state[9] = this->fan_mode_ == MITSUBISHI_FAN_3L ? 2 : 3;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
if (this->fan_mode_ == MITSUBISHI_FAN_3L) {
|
||||
remote_state[9] = 3;
|
||||
} else {
|
||||
remote_state[9] = 4;
|
||||
}
|
||||
remote_state[9] = this->fan_mode_ == MITSUBISHI_FAN_3L ? 3 : 4;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_MIDDLE:
|
||||
remote_state[9] = 2;
|
||||
|
||||
@@ -17,7 +17,7 @@ CODEOWNERS = ["@crnjan"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
DOMAIN = "mitsubishi_cn105"
|
||||
|
||||
CONF_MITSUBISHI_CN105_ID = f"{DOMAIN}_id"
|
||||
CONF_MITSUBISHI_CN105_ID = "mitsubishi_cn105_id"
|
||||
CONF_TELEMETRY_REQUEST_MIN_INTERVAL = "telemetry_request_min_interval"
|
||||
CONF_HORIZONTAL = "horizontal"
|
||||
CONF_VANE = "vane"
|
||||
|
||||
@@ -680,13 +680,7 @@ PduBuffer create_write_coils_pdu(uint16_t start_address, PackedBits bits);
|
||||
* @param value_type defines if 16/32/64 bits or FP32 is used
|
||||
*/
|
||||
template<typename Container> void float_to_payload(Container &data, float value, SensorValueType value_type) {
|
||||
int64_t val;
|
||||
|
||||
if (value_type_is_float(value_type)) {
|
||||
val = bit_cast<uint32_t>(value);
|
||||
} else {
|
||||
val = llroundf(value);
|
||||
}
|
||||
const int64_t val = value_type_is_float(value_type) ? bit_cast<uint32_t>(value) : llroundf(value);
|
||||
|
||||
number_to_payload(data, val, value_type);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ def small_distance(value: Any) -> float:
|
||||
# empty and full distance values.
|
||||
# Format is - tank name: (empty distance in mm, full distance in mm)
|
||||
#
|
||||
CONF_SUPPORTED_TANKS_MAP = {
|
||||
SUPPORTED_TANKS_MAP = {
|
||||
TANK_TYPE_CUSTOM: (0, 100),
|
||||
"20LB_V": (38, 254), # empty/full readings for 20lb US tank
|
||||
"30LB_V": (38, 381),
|
||||
@@ -81,7 +81,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Optional(CONF_CUSTOM_DISTANCE_FULL): small_distance,
|
||||
cv.Optional(CONF_CUSTOM_DISTANCE_EMPTY): small_distance,
|
||||
cv.Required(CONF_TANK_TYPE): cv.enum(CONF_SUPPORTED_TANKS_MAP, upper=True),
|
||||
cv.Required(CONF_TANK_TYPE): cv.enum(SUPPORTED_TANKS_MAP, upper=True),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
@@ -140,16 +140,16 @@ async def to_code(config: ConfigType) -> None:
|
||||
if CONF_CUSTOM_DISTANCE_EMPTY in config:
|
||||
cg.add(var.set_tank_empty(config[CONF_CUSTOM_DISTANCE_EMPTY]))
|
||||
else:
|
||||
cg.add(var.set_tank_empty(CONF_SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][0]))
|
||||
cg.add(var.set_tank_empty(SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][0]))
|
||||
if CONF_CUSTOM_DISTANCE_FULL in config:
|
||||
cg.add(var.set_tank_full(config[CONF_CUSTOM_DISTANCE_FULL]))
|
||||
else:
|
||||
cg.add(var.set_tank_full(CONF_SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][1]))
|
||||
cg.add(var.set_tank_full(SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][1]))
|
||||
else:
|
||||
# Set the Tank empty and full based on map - User is requesting standard tank
|
||||
t = config[CONF_TANK_TYPE]
|
||||
cg.add(var.set_tank_empty(CONF_SUPPORTED_TANKS_MAP[t][0]))
|
||||
cg.add(var.set_tank_full(CONF_SUPPORTED_TANKS_MAP[t][1]))
|
||||
cg.add(var.set_tank_empty(SUPPORTED_TANKS_MAP[t][0]))
|
||||
cg.add(var.set_tank_full(SUPPORTED_TANKS_MAP[t][1]))
|
||||
|
||||
if (
|
||||
minimum_signal_quality := config.get(CONF_MINIMUM_SIGNAL_QUALITY, None)
|
||||
|
||||
@@ -41,7 +41,7 @@ def small_distance(value: Any) -> float:
|
||||
# empty and full distance values.
|
||||
# Format is - tank name: (empty distance in mm, full distance in mm)
|
||||
#
|
||||
CONF_SUPPORTED_TANKS_MAP = {
|
||||
SUPPORTED_TANKS_MAP = {
|
||||
TANK_TYPE_CUSTOM: (38, 100),
|
||||
"NORTH_AMERICA_20LB_VERTICAL": (38, 254), # empty/full readings for 20lb US tank
|
||||
"NORTH_AMERICA_30LB_VERTICAL": (38, 381),
|
||||
@@ -68,7 +68,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_CUSTOM_DISTANCE_FULL): small_distance,
|
||||
cv.Optional(CONF_CUSTOM_DISTANCE_EMPTY): small_distance,
|
||||
cv.Optional(CONF_PROPANE_BUTANE_MIX, default="100%"): cv.percentage,
|
||||
cv.Required(CONF_TANK_TYPE): cv.enum(CONF_SUPPORTED_TANKS_MAP, upper=True),
|
||||
cv.Required(CONF_TANK_TYPE): cv.enum(SUPPORTED_TANKS_MAP, upper=True),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
@@ -111,16 +111,16 @@ async def to_code(config: ConfigType) -> None:
|
||||
if CONF_CUSTOM_DISTANCE_EMPTY in config:
|
||||
cg.add(var.set_tank_empty(config[CONF_CUSTOM_DISTANCE_EMPTY]))
|
||||
else:
|
||||
cg.add(var.set_tank_empty(CONF_SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][0]))
|
||||
cg.add(var.set_tank_empty(SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][0]))
|
||||
if CONF_CUSTOM_DISTANCE_FULL in config:
|
||||
cg.add(var.set_tank_full(config[CONF_CUSTOM_DISTANCE_FULL]))
|
||||
else:
|
||||
cg.add(var.set_tank_full(CONF_SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][1]))
|
||||
cg.add(var.set_tank_full(SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][1]))
|
||||
else:
|
||||
# Set the Tank empty and full based on map - User is requesting standard tank
|
||||
t = config[CONF_TANK_TYPE]
|
||||
cg.add(var.set_tank_empty(CONF_SUPPORTED_TANKS_MAP[t][0]))
|
||||
cg.add(var.set_tank_full(CONF_SUPPORTED_TANKS_MAP[t][1]))
|
||||
cg.add(var.set_tank_empty(SUPPORTED_TANKS_MAP[t][0]))
|
||||
cg.add(var.set_tank_full(SUPPORTED_TANKS_MAP[t][1]))
|
||||
|
||||
cg.add_optional(var.set_propane_butane_mix, config.get(CONF_PROPANE_BUTANE_MIX))
|
||||
|
||||
|
||||
@@ -203,11 +203,7 @@ static uint8_t crc4(uint16_t *buffer, size_t length) {
|
||||
auto apply_crc = [&crc_remainder](uint8_t next) {
|
||||
crc_remainder ^= next;
|
||||
for (uint8_t bit = 8; bit > 0; --bit) {
|
||||
if (crc_remainder & 0x8000) {
|
||||
crc_remainder = (crc_remainder << 1) ^ 0x3000;
|
||||
} else {
|
||||
crc_remainder = (crc_remainder << 1);
|
||||
}
|
||||
crc_remainder = crc_remainder & 0x8000 ? (crc_remainder << 1) ^ 0x3000 : (crc_remainder << 1);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -292,11 +292,7 @@ void MSA3xxComponent::set_transform(bool mirror_x, bool mirror_y, bool mirror_z,
|
||||
void MSA3xxComponent::setup_odr_(DataRate rate) {
|
||||
RegOutputDataRate reg_odr;
|
||||
auto reg = this->read_byte(static_cast<uint8_t>(RegisterMap::ODR));
|
||||
if (reg.has_value()) {
|
||||
reg_odr.raw = reg.value();
|
||||
} else {
|
||||
reg_odr.raw = 0x0F; // defaut from datasheet
|
||||
}
|
||||
reg_odr.raw = reg.value_or(0x0F); // default from datasheet
|
||||
|
||||
reg_odr.x_axis_disable = false;
|
||||
reg_odr.y_axis_disable = false;
|
||||
@@ -311,11 +307,7 @@ void MSA3xxComponent::setup_power_mode_bandwidth_(PowerMode power_mode, Bandwidt
|
||||
auto reg = this->read_byte(static_cast<uint8_t>(RegisterMap::POWER_MODE_BANDWIDTH));
|
||||
|
||||
RegPowerModeBandwidth power_mode_bandwidth;
|
||||
if (reg.has_value()) {
|
||||
power_mode_bandwidth.raw = reg.value();
|
||||
} else {
|
||||
power_mode_bandwidth.raw = 0xde; // defaut from datasheet
|
||||
}
|
||||
power_mode_bandwidth.raw = reg.value_or(0xde); // default from datasheet
|
||||
|
||||
power_mode_bandwidth.power_mode = power_mode;
|
||||
power_mode_bandwidth.low_power_bandwidth = bandwidth;
|
||||
|
||||
@@ -18,11 +18,7 @@ void OpenthermNumber::setup() {
|
||||
} else {
|
||||
this->pref_ = this->make_entity_preference<float>();
|
||||
if (!this->pref_.load(&value)) {
|
||||
if (!std::isnan(this->initial_value_)) {
|
||||
value = this->initial_value_;
|
||||
} else {
|
||||
value = this->traits.get_min_value();
|
||||
}
|
||||
value = !std::isnan(this->initial_value_) ? this->initial_value_ : this->traits.get_min_value();
|
||||
}
|
||||
}
|
||||
this->publish_state(value);
|
||||
|
||||
@@ -103,7 +103,7 @@ IDF_TO_OT_LOG_LEVEL = {
|
||||
"VERBOSE": "DEBG",
|
||||
}
|
||||
|
||||
CONF_DEVICE_TYPES = [
|
||||
DEVICE_TYPES = [
|
||||
"FTD",
|
||||
"MTD",
|
||||
]
|
||||
@@ -430,7 +430,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.GenerateID(CONF_MDNS_ID): cv.use_id(MDNSComponent),
|
||||
cv.Optional(CONF_BORDER_ROUTER): _validate_border_router,
|
||||
cv.Optional(CONF_DEVICE_TYPE, default="FTD"): cv.one_of(
|
||||
*CONF_DEVICE_TYPES, upper=True
|
||||
*DEVICE_TYPES, upper=True
|
||||
),
|
||||
cv.Optional(CONF_FORCE_DATASET): cv.boolean,
|
||||
cv.Optional(CONF_TLV): cv.sensitive(
|
||||
|
||||
@@ -5,6 +5,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import esp32, esp32_rmt, remote_base
|
||||
from esphome.components.libretiny import get_libretiny_family
|
||||
from esphome.components.libretiny.const import FAMILY_BK7238, FAMILY_RTL8720C
|
||||
from esphome.components.remote_base import CONF_TRANSMITTER_ID
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -32,7 +33,6 @@ CONF_EOT_LEVEL = "eot_level"
|
||||
CONF_NON_BLOCKING = "non_blocking"
|
||||
CONF_ON_TRANSMIT = "on_transmit"
|
||||
CONF_ON_COMPLETE = "on_complete"
|
||||
CONF_TRANSMITTER_ID = remote_base.CONF_TRANSMITTER_ID
|
||||
|
||||
remote_transmitter_ns = cg.esphome_ns.namespace("remote_transmitter")
|
||||
RemoteTransmitterComponent = remote_transmitter_ns.class_(
|
||||
|
||||
@@ -22,18 +22,10 @@ void ResistanceSensor::process_(float value) {
|
||||
float res = 0;
|
||||
switch (this->configuration_) {
|
||||
case UPSTREAM:
|
||||
if (value == 0.0f) {
|
||||
res = NAN;
|
||||
} else {
|
||||
res = (this->reference_voltage_ - value) / value;
|
||||
}
|
||||
res = value == 0.0f ? NAN : (this->reference_voltage_ - value) / value;
|
||||
break;
|
||||
case DOWNSTREAM:
|
||||
if (value == this->reference_voltage_) {
|
||||
res = NAN;
|
||||
} else {
|
||||
res = value / (this->reference_voltage_ - value);
|
||||
}
|
||||
res = value == this->reference_voltage_ ? NAN : value / (this->reference_voltage_ - value);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,12 +39,7 @@ void SCD30Component::setup() {
|
||||
ESP_LOGD(TAG, "SCD30 Firmware v%0d.%02d", (uint16_t(raw_firmware_version[0]) >> 8),
|
||||
uint16_t(raw_firmware_version[0] & 0xFF));
|
||||
|
||||
uint16_t temp_offset;
|
||||
if (this->temperature_offset_ > 0) {
|
||||
temp_offset = (this->temperature_offset_ * 100);
|
||||
} else {
|
||||
temp_offset = 0;
|
||||
}
|
||||
const uint16_t temp_offset = this->temperature_offset_ > 0 ? (this->temperature_offset_ * 100) : 0;
|
||||
|
||||
if (!this->setup_check(this->write_command(SCD30_CMD_TEMPERATURE_OFFSET, temp_offset),
|
||||
LOG_STR("Error setting temperature offset")))
|
||||
|
||||
@@ -47,23 +47,11 @@ void SHT4XComponent::setup() {
|
||||
ESP_LOGD(TAG, "Heater interval: %" PRIu32, this->heater_interval_);
|
||||
|
||||
if (this->heater_power_ == SHT4X_HEATERPOWER_HIGH) {
|
||||
if (this->heater_time_ == SHT4X_HEATERTIME_LONG) {
|
||||
this->heater_command_ = 0x39;
|
||||
} else {
|
||||
this->heater_command_ = 0x32;
|
||||
}
|
||||
this->heater_command_ = this->heater_time_ == SHT4X_HEATERTIME_LONG ? 0x39 : 0x32;
|
||||
} else if (this->heater_power_ == SHT4X_HEATERPOWER_MED) {
|
||||
if (this->heater_time_ == SHT4X_HEATERTIME_LONG) {
|
||||
this->heater_command_ = 0x2F;
|
||||
} else {
|
||||
this->heater_command_ = 0x24;
|
||||
}
|
||||
this->heater_command_ = this->heater_time_ == SHT4X_HEATERTIME_LONG ? 0x2F : 0x24;
|
||||
} else {
|
||||
if (this->heater_time_ == SHT4X_HEATERTIME_LONG) {
|
||||
this->heater_command_ = 0x1E;
|
||||
} else {
|
||||
this->heater_command_ = 0x15;
|
||||
}
|
||||
this->heater_command_ = this->heater_time_ == SHT4X_HEATERTIME_LONG ? 0x1E : 0x15;
|
||||
}
|
||||
ESP_LOGD(TAG, "Heater command: %x", this->heater_command_);
|
||||
}
|
||||
|
||||
@@ -36,11 +36,7 @@ void SHTCXComponent::setup() {
|
||||
this->sensor_id_ = device_id_register;
|
||||
|
||||
if ((device_id_register & 0x3F) == 0x07) {
|
||||
if (device_id_register & 0x800) {
|
||||
this->type_ = SHTCX_TYPE_SHTC3;
|
||||
} else {
|
||||
this->type_ = SHTCX_TYPE_SHTC1;
|
||||
}
|
||||
this->type_ = device_id_register & 0x800 ? SHTCX_TYPE_SHTC3 : SHTCX_TYPE_SHTC1;
|
||||
} else {
|
||||
this->type_ = SHTCX_TYPE_UNKNOWN;
|
||||
}
|
||||
|
||||
@@ -12,12 +12,7 @@ SmlTextSensor::SmlTextSensor(std::string server_id, std::string obis_code, SmlTy
|
||||
: SmlListener(std::move(server_id), std::move(obis_code)), format_(format) {}
|
||||
|
||||
void SmlTextSensor::publish_val(const ObisInfo &obis_info) {
|
||||
uint8_t value_type;
|
||||
if (this->format_ == SML_UNDEFINED) {
|
||||
value_type = obis_info.value_type;
|
||||
} else {
|
||||
value_type = this->format_;
|
||||
}
|
||||
const uint8_t value_type = this->format_ == SML_UNDEFINED ? obis_info.value_type : this->format_;
|
||||
|
||||
switch (value_type) {
|
||||
case SML_HEX: {
|
||||
|
||||
@@ -335,11 +335,7 @@ void AudioPipeline::read_task(void *params) {
|
||||
this_pipeline->raw_file_ring_buffer_ = temp_ring_buffer;
|
||||
}
|
||||
|
||||
if (temp_ring_buffer == nullptr) {
|
||||
err = ESP_ERR_NO_MEM;
|
||||
} else {
|
||||
err = reader->add_sink(temp_ring_buffer);
|
||||
}
|
||||
err = temp_ring_buffer == nullptr ? ESP_ERR_NO_MEM : reader->add_sink(temp_ring_buffer);
|
||||
}
|
||||
|
||||
if (err != ESP_OK) {
|
||||
|
||||
@@ -524,11 +524,7 @@ void SpeakerMediaPlayer::play_file(const audio::AudioFile *media_file, bool anno
|
||||
|
||||
media_command.file = media_file;
|
||||
media_command.is_announcement = announcement;
|
||||
if (this->single_pipeline_() || announcement) {
|
||||
media_command.announce = true;
|
||||
} else {
|
||||
media_command.announce = false;
|
||||
}
|
||||
media_command.announce = this->single_pipeline_() || announcement;
|
||||
media_command.enqueue = enqueue;
|
||||
xQueueSend(this->media_control_command_queue_, &media_command, portMAX_DELAY);
|
||||
}
|
||||
@@ -543,11 +539,7 @@ void SpeakerMediaPlayer::control(const media_player::MediaPlayerCall &call) {
|
||||
|
||||
auto ann = call.get_announcement();
|
||||
media_command.is_announcement = ann.has_value() && *ann;
|
||||
if (this->single_pipeline_() || media_command.is_announcement) {
|
||||
media_command.announce = true;
|
||||
} else {
|
||||
media_command.announce = false;
|
||||
}
|
||||
media_command.announce = this->single_pipeline_() || media_command.is_announcement;
|
||||
|
||||
const auto &media_url = call.get_media_url();
|
||||
if (media_url.has_value()) {
|
||||
|
||||
@@ -748,11 +748,7 @@ void SpeakerSourceMediaPlayer::control(const media_player::MediaPlayerCall &call
|
||||
auto command = call.get_command();
|
||||
bool enqueue = command.has_value() && command.value() == media_player::MEDIA_PLAYER_COMMAND_ENQUEUE;
|
||||
|
||||
if (enqueue) {
|
||||
control_command.type = MediaPlayerControlCommand::ENQUEUE_URI;
|
||||
} else {
|
||||
control_command.type = MediaPlayerControlCommand::PLAY_URI;
|
||||
}
|
||||
control_command.type = enqueue ? MediaPlayerControlCommand::ENQUEUE_URI : MediaPlayerControlCommand::PLAY_URI;
|
||||
// Heap allocation is unavoidable: URIs from Home Assistant are arbitrary-length (media URLs with tokens
|
||||
// can easily exceed 500 bytes). Deleted in process_control_queue_() after the command is consumed. FreeRTOS queues
|
||||
// require items to be copyable, so we store a pointer to the string in the queue rather than the string itself.
|
||||
|
||||
@@ -95,12 +95,7 @@ uint16_t SPIDelegateBitBash::transfer_(uint16_t data, size_t num_bits) {
|
||||
uint16_t out_data = 0;
|
||||
|
||||
for (uint8_t i = 0; i != num_bits; i++) {
|
||||
uint8_t shift;
|
||||
if (bit_order_ == BIT_ORDER_MSB_FIRST) {
|
||||
shift = num_bits - 1 - i;
|
||||
} else {
|
||||
shift = i;
|
||||
}
|
||||
const uint8_t shift = this->bit_order_ == BIT_ORDER_MSB_FIRST ? num_bits - 1 - i : i;
|
||||
|
||||
if (clock_phase_ == CLOCK_PHASE_LEADING) {
|
||||
// sampling on leading edge
|
||||
|
||||
@@ -19,11 +19,7 @@ void SprinklerControllerNumber::setup() {
|
||||
} else {
|
||||
this->pref_ = this->make_entity_preference<float>();
|
||||
if (!this->pref_.load(&value)) {
|
||||
if (!std::isnan(this->initial_value_)) {
|
||||
value = this->initial_value_;
|
||||
} else {
|
||||
value = this->traits.get_min_value();
|
||||
}
|
||||
value = !std::isnan(this->initial_value_) ? this->initial_value_ : this->traits.get_min_value();
|
||||
}
|
||||
}
|
||||
this->publish_state(value);
|
||||
|
||||
@@ -135,11 +135,7 @@ void SSD1331::fill(Color color) {
|
||||
|
||||
const uint32_t color565 = display::ColorUtil::color_to_565(color);
|
||||
for (uint32_t i = 0; i < this->get_buffer_length_(); i++) {
|
||||
if (i & 1) {
|
||||
this->buffer_[i] = color565 & 0xff;
|
||||
} else {
|
||||
this->buffer_[i] = (color565 >> 8) & 0xff;
|
||||
}
|
||||
this->buffer_[i] = i & 1 ? color565 & 0xff : (color565 >> 8) & 0xff;
|
||||
}
|
||||
}
|
||||
void SSD1331::init_reset_() { pulse_reset(this->reset_pin_, {.high_before_ms = 1, .low_ms = 10}); }
|
||||
|
||||
@@ -167,11 +167,7 @@ void SSD1351::fill(Color color) {
|
||||
|
||||
const uint32_t color565 = display::ColorUtil::color_to_565(color);
|
||||
for (uint32_t i = 0; i < this->get_buffer_length_(); i++) {
|
||||
if (i & 1) {
|
||||
this->buffer_[i] = color565 & 0xff;
|
||||
} else {
|
||||
this->buffer_[i] = (color565 >> 8) & 0xff;
|
||||
}
|
||||
this->buffer_[i] = i & 1 ? color565 & 0xff : (color565 >> 8) & 0xff;
|
||||
}
|
||||
}
|
||||
void SSD1351::init_reset_() { pulse_reset(this->reset_pin_, {.high_before_ms = 1, .low_ms = 10}); }
|
||||
|
||||
@@ -18,12 +18,8 @@ class SunTextSensor final : public text_sensor::TextSensor, public PollingCompon
|
||||
void set_format(const std::string &format) = delete;
|
||||
|
||||
void update() override {
|
||||
optional<ESPTime> res;
|
||||
if (this->sunrise_) {
|
||||
res = this->parent_->sunrise(this->elevation_);
|
||||
} else {
|
||||
res = this->parent_->sunset(this->elevation_);
|
||||
}
|
||||
optional<ESPTime> res =
|
||||
this->sunrise_ ? this->parent_->sunrise(this->elevation_) : this->parent_->sunset(this->elevation_);
|
||||
if (!res) {
|
||||
this->publish_state("");
|
||||
return;
|
||||
|
||||
@@ -79,21 +79,9 @@ void TemplateDate::control(const datetime::DateCall &call) {
|
||||
|
||||
if (this->restore_value_) {
|
||||
datetime::DateEntityRestoreState temp = {};
|
||||
if (has_year) {
|
||||
temp.year = *opt_year;
|
||||
} else {
|
||||
temp.year = this->year_;
|
||||
}
|
||||
if (has_month) {
|
||||
temp.month = *opt_month;
|
||||
} else {
|
||||
temp.month = this->month_;
|
||||
}
|
||||
if (has_day) {
|
||||
temp.day = *opt_day;
|
||||
} else {
|
||||
temp.day = this->day_;
|
||||
}
|
||||
temp.year = opt_year.value_or(this->year_);
|
||||
temp.month = opt_month.value_or(this->month_);
|
||||
temp.day = opt_day.value_or(this->day_);
|
||||
|
||||
this->pref_.save(&temp);
|
||||
}
|
||||
|
||||
@@ -106,36 +106,12 @@ void TemplateDateTime::control(const datetime::DateTimeCall &call) {
|
||||
|
||||
if (this->restore_value_) {
|
||||
datetime::DateTimeEntityRestoreState temp = {};
|
||||
if (has_year) {
|
||||
temp.year = *opt_year;
|
||||
} else {
|
||||
temp.year = this->year_;
|
||||
}
|
||||
if (has_month) {
|
||||
temp.month = *opt_month;
|
||||
} else {
|
||||
temp.month = this->month_;
|
||||
}
|
||||
if (has_day) {
|
||||
temp.day = *opt_day;
|
||||
} else {
|
||||
temp.day = this->day_;
|
||||
}
|
||||
if (has_hour) {
|
||||
temp.hour = *opt_hour;
|
||||
} else {
|
||||
temp.hour = this->hour_;
|
||||
}
|
||||
if (has_minute) {
|
||||
temp.minute = *opt_minute;
|
||||
} else {
|
||||
temp.minute = this->minute_;
|
||||
}
|
||||
if (has_second) {
|
||||
temp.second = *opt_second;
|
||||
} else {
|
||||
temp.second = this->second_;
|
||||
}
|
||||
temp.year = opt_year.value_or(this->year_);
|
||||
temp.month = opt_month.value_or(this->month_);
|
||||
temp.day = opt_day.value_or(this->day_);
|
||||
temp.hour = opt_hour.value_or(this->hour_);
|
||||
temp.minute = opt_minute.value_or(this->minute_);
|
||||
temp.second = opt_second.value_or(this->second_);
|
||||
|
||||
this->pref_.save(&temp);
|
||||
}
|
||||
|
||||
@@ -79,21 +79,9 @@ void TemplateTime::control(const datetime::TimeCall &call) {
|
||||
|
||||
if (this->restore_value_) {
|
||||
datetime::TimeEntityRestoreState temp = {};
|
||||
if (has_hour) {
|
||||
temp.hour = *opt_hour;
|
||||
} else {
|
||||
temp.hour = this->hour_;
|
||||
}
|
||||
if (has_minute) {
|
||||
temp.minute = *opt_minute;
|
||||
} else {
|
||||
temp.minute = this->minute_;
|
||||
}
|
||||
if (has_second) {
|
||||
temp.second = *opt_second;
|
||||
} else {
|
||||
temp.second = this->second_;
|
||||
}
|
||||
temp.hour = opt_hour.value_or(this->hour_);
|
||||
temp.minute = opt_minute.value_or(this->minute_);
|
||||
temp.second = opt_second.value_or(this->second_);
|
||||
|
||||
this->pref_.save(&temp);
|
||||
}
|
||||
|
||||
@@ -15,11 +15,7 @@ void TemplateNumber::setup() {
|
||||
} else {
|
||||
this->pref_ = this->make_entity_preference<float>();
|
||||
if (!this->pref_.load(&value)) {
|
||||
if (!std::isnan(this->initial_value_)) {
|
||||
value = this->initial_value_;
|
||||
} else {
|
||||
value = this->traits.get_min_value();
|
||||
}
|
||||
value = !std::isnan(this->initial_value_) ? this->initial_value_ : this->traits.get_min_value();
|
||||
}
|
||||
}
|
||||
this->publish_state(value);
|
||||
|
||||
@@ -210,11 +210,7 @@ void ESP8266UartComponent::write_array(const uint8_t *data, size_t len) {
|
||||
bool ESP8266UartComponent::peek_byte(uint8_t *data) {
|
||||
if (!this->check_read_timeout_())
|
||||
return false;
|
||||
if (this->hw_serial_ != nullptr) {
|
||||
*data = this->hw_serial_->peek();
|
||||
} else {
|
||||
*data = this->sw_serial_->peek_byte();
|
||||
}
|
||||
*data = this->hw_serial_ != nullptr ? this->hw_serial_->peek() : this->sw_serial_->peek_byte();
|
||||
return true;
|
||||
}
|
||||
bool ESP8266UartComponent::read_array(uint8_t *data, size_t len) {
|
||||
|
||||
@@ -145,12 +145,7 @@ void RP2UartComponent::setup() {
|
||||
this->serial_ = serial;
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Using Hardware Serial");
|
||||
SerialUART *serial;
|
||||
if (hw_uart == 0) {
|
||||
serial = &Serial1;
|
||||
} else {
|
||||
serial = &Serial2;
|
||||
}
|
||||
SerialUART *serial = hw_uart == 0 ? &Serial1 : &Serial2;
|
||||
if (this->tx_pin_ != nullptr)
|
||||
serial->setTX(this->tx_pin_->get_pin());
|
||||
if (this->rx_pin_ != nullptr)
|
||||
|
||||
@@ -70,11 +70,7 @@ static int ftdi_to_clkbits_am(int baudrate, uint32_t *encoded_divisor) {
|
||||
}
|
||||
}
|
||||
baud_estimate = (24000000 + (try_divisor / 2)) / try_divisor;
|
||||
if (baud_estimate < baudrate) {
|
||||
baud_diff = baudrate - baud_estimate;
|
||||
} else {
|
||||
baud_diff = baud_estimate - baudrate;
|
||||
}
|
||||
baud_diff = baud_estimate < baudrate ? baudrate - baud_estimate : baud_estimate - baudrate;
|
||||
if (i == 0 || baud_diff < best_baud_diff) {
|
||||
best_divisor = try_divisor;
|
||||
best_baud = baud_estimate;
|
||||
@@ -108,19 +104,11 @@ static int ftdi_to_clkbits(int baudrate, unsigned int clk, int clk_div, uint32_t
|
||||
best_baud = clk / (2 * clk_div);
|
||||
} else {
|
||||
divisor = clk * 16 / clk_div / baudrate;
|
||||
if (divisor & 1) {
|
||||
best_divisor = divisor / 2 + 1;
|
||||
} else {
|
||||
best_divisor = divisor / 2;
|
||||
}
|
||||
best_divisor = divisor & 1 ? divisor / 2 + 1 : divisor / 2;
|
||||
if (best_divisor > 0x20000)
|
||||
best_divisor = 0x1ffff;
|
||||
best_baud = clk * 16 / clk_div / best_divisor;
|
||||
if (best_baud & 1) {
|
||||
best_baud = best_baud / 2 + 1;
|
||||
} else {
|
||||
best_baud = best_baud / 2;
|
||||
}
|
||||
best_baud = best_baud & 1 ? best_baud / 2 + 1 : best_baud / 2;
|
||||
*encoded_divisor = (best_divisor >> 3) | (FRAC_CODE[best_divisor & 0x7] << 14);
|
||||
}
|
||||
return best_baud;
|
||||
|
||||
@@ -27,7 +27,7 @@ def validate_pwm_limits(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
CONF_SAFE_PWM_LEVELS_SCHEMA = cv.Schema(
|
||||
_SAFE_PWM_LEVELS_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_MIN_VALUE, default=DUTY_DEFAULT_MIN): cv.uint8_t,
|
||||
cv.Optional(CONF_MAX_VALUE, default=DUTY_DEFAULT_MAX): cv.uint8_t,
|
||||
@@ -41,7 +41,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.GenerateID(CONF_WAVESHARE_IO_CH32V003_ID): cv.use_id(
|
||||
WaveshareIOCH32V003Component
|
||||
),
|
||||
cv.Optional(CONF_SAFE_PWM_LEVELS): CONF_SAFE_PWM_LEVELS_SCHEMA,
|
||||
cv.Optional(CONF_SAFE_PWM_LEVELS): _SAFE_PWM_LEVELS_SCHEMA,
|
||||
}
|
||||
),
|
||||
validate_pwm_limits,
|
||||
|
||||
@@ -96,11 +96,7 @@ uint64_t Wl134Component::hex_lsb_ascii_to_uint64_(const uint8_t *text, uint8_t t
|
||||
i--;
|
||||
|
||||
uint8_t digit = text[i];
|
||||
if (digit >= 'A') {
|
||||
digit = digit - 'A' + 10;
|
||||
} else {
|
||||
digit = digit - '0';
|
||||
}
|
||||
digit = digit >= 'A' ? digit - 'A' + 10 : digit - '0';
|
||||
value = (value << 4) + digit;
|
||||
} while (i != 0);
|
||||
|
||||
|
||||
@@ -23,11 +23,7 @@ void ZHLT01Climate::transmit_state() {
|
||||
|
||||
// Byte 7 : Power | Swing | Fan
|
||||
// -- Power
|
||||
if (this->mode == climate::CLIMATE_MODE_OFF) {
|
||||
ir_message[7] = AC1_POWER_OFF;
|
||||
} else {
|
||||
ir_message[7] = AC1_POWER_ON;
|
||||
}
|
||||
ir_message[7] = this->mode == climate::CLIMATE_MODE_OFF ? AC1_POWER_OFF : AC1_POWER_ON;
|
||||
|
||||
// -- Swing
|
||||
switch (this->swing_mode) {
|
||||
|
||||
@@ -53,9 +53,9 @@ from .const import ( # noqa: F401
|
||||
)
|
||||
from .const_zephyr import (
|
||||
CONF_IEEE802154_VENDOR_OUI,
|
||||
CONF_MAX_EP_NUMBER_ZEPHYR,
|
||||
CONF_ZIGBEE_ID,
|
||||
KEY_EP_NUMBER,
|
||||
MAX_EP_NUMBER_ZEPHYR,
|
||||
)
|
||||
from .zigbee_esp32 import (
|
||||
final_validate_esp32,
|
||||
@@ -182,8 +182,8 @@ def validate_number_of_ep(config: ConfigType) -> ConfigType:
|
||||
_LOGGER.warning(
|
||||
"Single endpoint requires ZHA or at leatst Zigbee2MQTT 2.8.0. For older versions of Zigbee2MQTT use multiple endpoints"
|
||||
)
|
||||
if count > CONF_MAX_EP_NUMBER_ZEPHYR and not CORE.testing_mode:
|
||||
raise cv.Invalid(f"Maximum number of end points is {CONF_MAX_EP_NUMBER_ZEPHYR}")
|
||||
if count > MAX_EP_NUMBER_ZEPHYR and not CORE.testing_mode:
|
||||
raise cv.Invalid(f"Maximum number of end points is {MAX_EP_NUMBER_ZEPHYR}")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from esphome.const import ( # noqa: F401 # pylint: disable=unused-import
|
||||
CONF_ZIGBEE_SWITCH,
|
||||
)
|
||||
|
||||
CONF_MAX_EP_NUMBER_ZEPHYR = 8
|
||||
MAX_EP_NUMBER_ZEPHYR = 8
|
||||
CONF_IEEE802154_VENDOR_OUI = "ieee802154_vendor_oui"
|
||||
|
||||
# Keys for CORE.data storage
|
||||
|
||||
+10
-40
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "defines.h"
|
||||
#include "component.h"
|
||||
#include "helpers.h"
|
||||
@@ -116,26 +118,10 @@ struct Color {
|
||||
}
|
||||
inline Color operator+(const Color &add) const ESPHOME_ALWAYS_INLINE {
|
||||
Color ret;
|
||||
if (uint8_t(add.r + this->r) < this->r) {
|
||||
ret.r = 255;
|
||||
} else {
|
||||
ret.r = this->r + add.r;
|
||||
}
|
||||
if (uint8_t(add.g + this->g) < this->g) {
|
||||
ret.g = 255;
|
||||
} else {
|
||||
ret.g = this->g + add.g;
|
||||
}
|
||||
if (uint8_t(add.b + this->b) < this->b) {
|
||||
ret.b = 255;
|
||||
} else {
|
||||
ret.b = this->b + add.b;
|
||||
}
|
||||
if (uint8_t(add.w + this->w) < this->w) {
|
||||
ret.w = 255;
|
||||
} else {
|
||||
ret.w = this->w + add.w;
|
||||
}
|
||||
ret.r = std::min(this->r + add.r, 255);
|
||||
ret.g = std::min(this->g + add.g, 255);
|
||||
ret.b = std::min(this->b + add.b, 255);
|
||||
ret.w = std::min(this->w + add.w, 255);
|
||||
return ret;
|
||||
}
|
||||
inline Color &operator+=(const Color &add) ESPHOME_ALWAYS_INLINE { return *this = (*this) + add; }
|
||||
@@ -143,26 +129,10 @@ struct Color {
|
||||
inline Color &operator+=(uint8_t add) ESPHOME_ALWAYS_INLINE { return *this = (*this) + add; }
|
||||
inline Color operator-(const Color &subtract) const ESPHOME_ALWAYS_INLINE {
|
||||
Color ret;
|
||||
if (subtract.r > this->r) {
|
||||
ret.r = 0;
|
||||
} else {
|
||||
ret.r = this->r - subtract.r;
|
||||
}
|
||||
if (subtract.g > this->g) {
|
||||
ret.g = 0;
|
||||
} else {
|
||||
ret.g = this->g - subtract.g;
|
||||
}
|
||||
if (subtract.b > this->b) {
|
||||
ret.b = 0;
|
||||
} else {
|
||||
ret.b = this->b - subtract.b;
|
||||
}
|
||||
if (subtract.w > this->w) {
|
||||
ret.w = 0;
|
||||
} else {
|
||||
ret.w = this->w - subtract.w;
|
||||
}
|
||||
ret.r = std::max(this->r - subtract.r, 0);
|
||||
ret.g = std::max(this->g - subtract.g, 0);
|
||||
ret.b = std::max(this->b - subtract.b, 0);
|
||||
ret.w = std::max(this->w - subtract.w, 0);
|
||||
return ret;
|
||||
}
|
||||
inline Color &operator-=(const Color &subtract) ESPHOME_ALWAYS_INLINE { return *this = (*this) - subtract; }
|
||||
|
||||
@@ -723,6 +723,9 @@
|
||||
#define USE_UDP_OMR_SOURCE
|
||||
#define USE_ZIGBEE
|
||||
#endif
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
#define USE_I2C_ISR_GUARD
|
||||
#endif
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
// Border router with a UART RCP; a native-radio border router (C5/C6) adds only the coexistence call
|
||||
#define USE_OPENTHREAD
|
||||
|
||||
@@ -803,11 +803,7 @@ void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation,
|
||||
hue = int(fmodf((60.0f * ((red - green) / delta)) + 240.0f, 360.0f));
|
||||
}
|
||||
|
||||
if (max_color_value == 0) {
|
||||
saturation = 0;
|
||||
} else {
|
||||
saturation = delta / max_color_value;
|
||||
}
|
||||
saturation = max_color_value == 0 ? 0 : delta / max_color_value;
|
||||
|
||||
value = max_color_value;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ class MillisInternal {
|
||||
static ESPHOME_ALWAYS_INLINE uint32_t get() {
|
||||
#if defined(USE_ESP32) && CONFIG_FREERTOS_HZ == 1000
|
||||
return xTaskGetTickCount();
|
||||
#elif defined(USE_LIBRETINY_VARIANT_RTL8710B)
|
||||
// The tick is 976.6 us here, so millis() reads the system timer (rtl8710b_micros.c)
|
||||
return millis();
|
||||
#elif defined(USE_LIBRETINY) && (defined(USE_RTL87XX) || defined(USE_LN882X))
|
||||
// 1 kHz: xTaskGetTickCount() is already ms.
|
||||
static_assert(configTICK_RATE_HZ == 1000, "MillisInternal fast path requires 1 kHz FreeRTOS tick");
|
||||
|
||||
@@ -85,10 +85,10 @@ def generate_cmakelists_txt(component: IDFComponent) -> str:
|
||||
# Extract the values
|
||||
build_src_dir = component.data.get("build", {}).get("srcDir", None)
|
||||
if not build_src_dir:
|
||||
for d in ["src", "Src", "."]:
|
||||
if (read_path / Path(d)).is_dir():
|
||||
build_src_dir = d
|
||||
break
|
||||
# read_path always exists (checked when the library is downloaded)
|
||||
build_src_dir = next(
|
||||
(d for d in ("src", "Src") if (read_path / d).is_dir()), "."
|
||||
)
|
||||
|
||||
build_include_dir = component.data.get("build", {}).get(
|
||||
"includeDir", DEFAULT_BUILD_INCLUDE_DIR
|
||||
|
||||
+84
-1
@@ -4,7 +4,7 @@ import argparse
|
||||
import ast
|
||||
import codecs
|
||||
import collections
|
||||
from collections.abc import Iterator
|
||||
from collections.abc import Iterable, Iterator
|
||||
import fnmatch
|
||||
import functools
|
||||
import os.path
|
||||
@@ -610,6 +610,89 @@ def lint_unit_from_const_py(fname, match):
|
||||
return err
|
||||
|
||||
|
||||
# Names that start with CONF_ or UNIT_ but are not config key or unit constants.
|
||||
NON_LITERAL_CONSTANT_ALLOWLIST = {"UNIT_OF_MEASUREMENT_MAX_LENGTH"}
|
||||
|
||||
|
||||
def _constant_names(target: ast.expr) -> Iterator[ast.Name]:
|
||||
"""Yield the CONF_/UNIT_ names bound by an assignment target."""
|
||||
if isinstance(target, ast.Name):
|
||||
if (
|
||||
target.id.startswith(("CONF_", "UNIT_"))
|
||||
and target.id not in NON_LITERAL_CONSTANT_ALLOWLIST
|
||||
):
|
||||
yield target
|
||||
elif isinstance(target, ast.Starred):
|
||||
yield from _constant_names(target.value)
|
||||
elif isinstance(target, (ast.Tuple, ast.List)):
|
||||
for elt in target.elts:
|
||||
yield from _constant_names(elt)
|
||||
|
||||
|
||||
def _module_level_statements(nodes: Iterable[ast.AST]) -> Iterator[ast.AST]:
|
||||
"""Yield statements at module level, including those inside if/try/with/for blocks.
|
||||
|
||||
Function and class bodies are a different scope and are not entered.
|
||||
"""
|
||||
for node in nodes:
|
||||
yield node
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
||||
continue
|
||||
yield from _module_level_statements(
|
||||
child
|
||||
for child in ast.iter_child_nodes(node)
|
||||
if isinstance(child, (ast.stmt, ast.excepthandler, ast.match_case))
|
||||
)
|
||||
|
||||
|
||||
@lint_content_check(include=["esphome/*.py"])
|
||||
def lint_constant_is_plain_string(
|
||||
fname: Path, content: str
|
||||
) -> list[tuple[int, int, str]]:
|
||||
"""Require module-level CONF_ and UNIT_ constants to be plain string literals.
|
||||
|
||||
Other scripts and lints (for example the duplicate constant checks above) read these
|
||||
definitions as text, so a computed value would be missed or moved incorrectly.
|
||||
"""
|
||||
try:
|
||||
tree = ast.parse(content, filename=str(fname))
|
||||
except SyntaxError:
|
||||
# Reported by the Python linters; nothing useful to add here.
|
||||
return []
|
||||
errs: list[tuple[int, int, str]] = []
|
||||
for node in _module_level_statements(tree.body):
|
||||
if isinstance(node, ast.Assign):
|
||||
targets = node.targets
|
||||
value = node.value
|
||||
elif isinstance(node, ast.AnnAssign) and node.value is not None:
|
||||
targets = [node.target]
|
||||
value = node.value
|
||||
elif isinstance(node, ast.AugAssign):
|
||||
targets = [node.target]
|
||||
value = None
|
||||
else:
|
||||
continue
|
||||
# Unpacking a string binds single characters or a list, so only a plain name counts.
|
||||
if (
|
||||
isinstance(value, ast.Constant)
|
||||
and type(value.value) is str
|
||||
and all(isinstance(target, ast.Name) for target in targets)
|
||||
):
|
||||
continue
|
||||
for target in targets:
|
||||
for name in _constant_names(target):
|
||||
example = highlight(f'{name.id} = "value"')
|
||||
msg = (
|
||||
f"Constant {highlight(name.id)} must be set to a plain string "
|
||||
f"literal, such as {example}. Do not build it from other "
|
||||
"constants, f-strings, operators or function calls. If the value "
|
||||
"is not a config key or a unit, give it a name without the CONF_ "
|
||||
"or UNIT_ prefix."
|
||||
)
|
||||
errs.append((name.lineno, name.col_offset + 1, msg))
|
||||
return errs
|
||||
|
||||
|
||||
RAW_PIN_ACCESS_RE = (
|
||||
r"^\s(pinMode|digitalWrite|digitalRead)\((.*)->get_pin\(\),\s*([^)]+).*\)"
|
||||
)
|
||||
|
||||
@@ -212,6 +212,8 @@ def clang_options(idedata, environment):
|
||||
and not flag.startswith("-mtune=esp")
|
||||
)
|
||||
cmd.append("-std=gnu++20")
|
||||
# clang otherwise reads the `module:` bit-field in IDF's soc/regdma.h as a C++20 module directive
|
||||
cmd.append("-fno-cxx-modules")
|
||||
|
||||
if environment.startswith(("bk72xx", "ln882h", "rtl87xx")):
|
||||
# LibreTiny leaves function-like macro values unparenthesized
|
||||
|
||||
+52
-2
@@ -6,6 +6,7 @@ Windows entry points cannot drift apart. Uses only the standard library: it runs
|
||||
before any dependency has been installed.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
@@ -18,6 +19,8 @@ MIN_PYTHON = (3, 12)
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DEFAULT_VENV = ROOT / "venv"
|
||||
POST_CHECKOUT_HOOK = ROOT / "script" / "git-hooks" / "post-checkout"
|
||||
VSCODE_SETTINGS = ROOT / ".vscode" / "settings.json"
|
||||
VSCODE_DEFAULT_SETTINGS = ROOT / ".vscode" / "settings.default.json"
|
||||
|
||||
# State of the environment the dependencies end up in, used for the closing
|
||||
# message.
|
||||
@@ -70,6 +73,22 @@ def git_output(*args: str) -> str:
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def environment_ready() -> bool:
|
||||
"""Return whether ESPHome is already installed in the environment setup uses."""
|
||||
active = os.environ.get("VIRTUAL_ENV")
|
||||
python = venv_python(Path(active) if active else DEFAULT_VENV)
|
||||
if not python.is_file():
|
||||
return False
|
||||
# -I keeps the checkout itself off sys.path, so only an installed copy counts.
|
||||
result = subprocess.run(
|
||||
[str(python), "-I", "-c", "import esphome"],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def create_venv(venv: Path) -> None:
|
||||
"""Create a virtual environment, replacing anything already at the path."""
|
||||
# --clear replaces a partial environment left behind by an interrupted run.
|
||||
@@ -159,6 +178,18 @@ def install_git_hooks(env: dict[str, str]) -> None:
|
||||
installed.chmod(0o755)
|
||||
|
||||
|
||||
def install_vscode_settings() -> None:
|
||||
"""Copy the default VS Code workspace settings, unless settings already exist.
|
||||
|
||||
settings.json is not tracked, so contributors can change it freely; it is
|
||||
only seeded on the first run.
|
||||
"""
|
||||
if VSCODE_SETTINGS.exists():
|
||||
return
|
||||
shutil.copyfile(VSCODE_DEFAULT_SETTINGS, VSCODE_SETTINGS)
|
||||
print(f"Created {VSCODE_SETTINGS.relative_to(ROOT).as_posix()} from the defaults.")
|
||||
|
||||
|
||||
def activate_hint() -> str:
|
||||
"""Return the command that activates the environment this script creates."""
|
||||
activate = bin_dir(DEFAULT_VENV).relative_to(ROOT) / "activate"
|
||||
@@ -190,14 +221,32 @@ def report(state: str, venv: Path) -> None:
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
def parse_args(argv: list[str]) -> argparse.Namespace:
|
||||
"""Parse the command line."""
|
||||
parser = argparse.ArgumentParser(description="Set up the ESPHome dev environment.")
|
||||
parser.add_argument(
|
||||
"--if-needed",
|
||||
action="store_true",
|
||||
help="do nothing if ESPHome is already installed (used when VS Code opens "
|
||||
"the folder)",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> None:
|
||||
"""Set up the development environment."""
|
||||
args = parse_args([] if argv is None else argv)
|
||||
if sys.version_info < MIN_PYTHON:
|
||||
raise SystemExit(
|
||||
f"ESPHome needs Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]} or newer, "
|
||||
f"but this is Python {sys.version.split()[0]}."
|
||||
)
|
||||
|
||||
if args.if_needed and environment_ready():
|
||||
install_vscode_settings()
|
||||
print("Development environment already set up. Run script/setup to update it.")
|
||||
return
|
||||
|
||||
# A virtual environment that is already active (for example the
|
||||
# devcontainer's pre-provisioned esphome-venv) is installed into rather than
|
||||
# creating a ./venv in the workspace.
|
||||
@@ -214,9 +263,10 @@ def main() -> None:
|
||||
env = venv_environment(venv)
|
||||
install_dependencies(venv, env)
|
||||
install_git_hooks(env)
|
||||
install_vscode_settings()
|
||||
(ROOT / ".temp").mkdir(exist_ok=True)
|
||||
report(state, venv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main(sys.argv[1:])
|
||||
@@ -670,11 +670,11 @@ def test_uc8179_e1001_code_generation(
|
||||
assert "97, 4, 3, 32, 1, 224" in main_cpp
|
||||
|
||||
|
||||
def test_gdey0583t81_defaults_and_driver_class(
|
||||
def test_uc8179_named_models(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""The GDEY0583T81 models default to 648x480 and use the UC8179 driver class."""
|
||||
"""The named UC8179 models use the UC8179 driver class with their own dimensions."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
@@ -682,7 +682,12 @@ def test_gdey0583t81_defaults_and_driver_class(
|
||||
|
||||
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
|
||||
|
||||
for name in ("goodisplay-gdey0583t81-5.83", "0583BN-T81-D2"):
|
||||
for name, dimensions in (
|
||||
("goodisplay-gdew075t7-7.5", (800, 480)),
|
||||
("075BN-T7-D2", (800, 480)),
|
||||
("goodisplay-gdey0583t81-5.83", (648, 480)),
|
||||
("0583BN-T81-D2", (648, 480)),
|
||||
):
|
||||
model = MODELS[name.upper()]
|
||||
assert model.class_name == "EPaperUC8179"
|
||||
config = run_schema_validation(
|
||||
@@ -693,7 +698,7 @@ def test_gdey0583t81_defaults_and_driver_class(
|
||||
"full_update_every": 5,
|
||||
}
|
||||
)
|
||||
assert model.get_dimensions(config) == (648, 480)
|
||||
assert model.get_dimensions(config) == dimensions
|
||||
|
||||
|
||||
def test_uc8279_width_must_be_a_multiple_of_8(
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,11 @@
|
||||
esphome:
|
||||
name: isr-guard-esp32
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
i2c:
|
||||
sda: GPIO21
|
||||
scl: GPIO22
|
||||
@@ -0,0 +1,11 @@
|
||||
esphome:
|
||||
name: isr-guard-esp32c3
|
||||
|
||||
esp32:
|
||||
board: esp32-c3-devkitm-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
i2c:
|
||||
sda: GPIO5
|
||||
scl: GPIO6
|
||||
@@ -0,0 +1,31 @@
|
||||
"""The I2C ISR guard define and its linker wrap are emitted together, only for classic ESP32."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.core import CORE
|
||||
|
||||
# Spelled out so a typo in the component's flag fails here
|
||||
_WRAP_FLAG = "-Wl,--wrap=esp_intr_alloc_intrstatus"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "expected"),
|
||||
[
|
||||
pytest.param("isr_guard_esp32.yaml", True, id="esp32"),
|
||||
pytest.param("isr_guard_esp32c3.yaml", False, id="esp32c3"),
|
||||
],
|
||||
)
|
||||
def test_guard_define_and_wrap_flag_go_together(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
expected: bool,
|
||||
) -> None:
|
||||
generate_main(component_config_path(config_file))
|
||||
defines = {define.name for define in CORE.defines}
|
||||
# A define without the flag leaves the guard uninstalled; a flag without the define fails to link
|
||||
assert ("USE_I2C_ISR_GUARD" in defines) is expected
|
||||
assert (_WRAP_FLAG in CORE.build_flags) is expected
|
||||
@@ -0,0 +1,42 @@
|
||||
"""RTL8710B builds wrap the timing functions that rtl8710b_micros.c replaces and compile that file; other families
|
||||
get neither, since a flag without the wrappers fails the link."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.libretiny import FILTER_SOURCE_FILES, RTL8710B_TIME_WRAPS
|
||||
from esphome.components.libretiny.const import KEY_LIBRETINY
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "wrapped"),
|
||||
[
|
||||
("rtl8710b.yaml", True),
|
||||
("rtl8720c.yaml", False),
|
||||
("ln882x.yaml", False),
|
||||
("bk72xx.yaml", False),
|
||||
("bk7238.yaml", False),
|
||||
],
|
||||
)
|
||||
def test_rtl8710b_time_wraps(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config: str,
|
||||
wrapped: bool,
|
||||
) -> None:
|
||||
generate_main(component_config_path(config))
|
||||
for func in RTL8710B_TIME_WRAPS:
|
||||
assert (f"-Wl,--wrap={func}" in CORE.build_flags) is wrapped
|
||||
assert ("rtl8710b_micros.c" not in FILTER_SOURCE_FILES()) is wrapped
|
||||
|
||||
|
||||
@pytest.mark.parametrize("core_data", [{}, {KEY_LIBRETINY: {}}])
|
||||
def test_nothing_excluded_before_the_family_is_known(core_data: dict) -> None:
|
||||
with patch("esphome.components.libretiny.CORE.data", core_data):
|
||||
assert FILTER_SOURCE_FILES() == []
|
||||
@@ -567,6 +567,43 @@ display:
|
||||
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
|
||||
it.circle(it.get_width() / 2, it.get_height() / 2, 100, Color::BLACK);
|
||||
|
||||
# Good Display GDEW075T7 7.5" mono e-paper (800x480, UC8179)
|
||||
- platform: epaper_spi
|
||||
spi_id: spi_bus
|
||||
model: goodisplay-gdew075t7-7.5
|
||||
full_update_every: 5
|
||||
cs_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO5
|
||||
dc_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO17
|
||||
reset_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO16
|
||||
busy_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO4
|
||||
inverted: true
|
||||
|
||||
# Seeed 075BN-T7-D2, the label name of the GDEW075T7 panel
|
||||
- platform: epaper_spi
|
||||
spi_id: spi_bus
|
||||
model: 075BN-T7-D2
|
||||
cs_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO5
|
||||
dc_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO17
|
||||
reset_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO16
|
||||
busy_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO4
|
||||
inverted: true
|
||||
|
||||
# Good Display GDEY0583T81 5.83" mono e-paper (648x480, UC8179)
|
||||
- platform: epaper_spi
|
||||
spi_id: spi_bus
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
"""Tests for the lint in script/ci-custom.py that requires CONF_ and UNIT_ constants to be plain strings."""
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
SCRIPT_DIR = (Path(__file__).parent / ".." / ".." / "script").resolve()
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
|
||||
def _load_ci_custom() -> ModuleType:
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"ci_custom", SCRIPT_DIR / "ci-custom.py"
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
ci_custom = _load_ci_custom()
|
||||
|
||||
|
||||
def _lint(content: str) -> list[tuple[int, int, str]]:
|
||||
return ci_custom.lint_constant_is_plain_string(
|
||||
Path("esphome/components/x/sensor.py"), content
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"content",
|
||||
[
|
||||
'CONF_FOO = "foo"\n',
|
||||
"UNIT_FOO = 'foo'\n",
|
||||
'CONF_FOO = "fo" "o"\n',
|
||||
'CONF_FOO: str = "foo"\n',
|
||||
"CONF_FOO: str\n",
|
||||
"UNIT_OF_MEASUREMENT_MAX_LENGTH = 63\n",
|
||||
"OTHER = 1 + 2\n",
|
||||
"ICON_FOO = some_call()\n",
|
||||
"MAX_FOO = CONF_BAR\n",
|
||||
"import os\n",
|
||||
'def f() -> None:\n CONF_FOO = "a" + "b"\n',
|
||||
'class A:\n CONF_FOO = f"{X}"\n',
|
||||
"a.CONF_FOO = 1\n",
|
||||
'try:\n CONF_FOO = "a"\nexcept ImportError:\n CONF_FOO = "b"\n',
|
||||
'if X:\n def f() -> None:\n CONF_FOO = "a" + "b"\n',
|
||||
"if X:\n async def f() -> None:\n CONF_FOO = X\n",
|
||||
],
|
||||
)
|
||||
def test_accepts(content: str) -> None:
|
||||
assert _lint(content) == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("content", "name", "col"),
|
||||
[
|
||||
('UNIT_FOO = "a" + SUFFIX + "b"\n', "UNIT_FOO", 1),
|
||||
('UNIT_FOO = "x" if A else "y"\n', "UNIT_FOO", 1),
|
||||
('CONF_FOO = "a" + "b"\n', "CONF_FOO", 1),
|
||||
('UNIT_FOO = "%s" % "b"\n', "UNIT_FOO", 1),
|
||||
('UNIT_FOO = f"{X}"\n', "UNIT_FOO", 1),
|
||||
('UNIT_FOO = b"x"\n', "UNIT_FOO", 1),
|
||||
("CONF_FOO = OTHER_CONST\n", "CONF_FOO", 1),
|
||||
("CONF_FOO = module.CONF_FOO\n", "CONF_FOO", 1),
|
||||
("CONF_FOO = some_call()\n", "CONF_FOO", 1),
|
||||
("CONF_FOO = 8\n", "CONF_FOO", 1),
|
||||
('CONF_FOO = ["a", "b"]\n', "CONF_FOO", 1),
|
||||
("CONF_FOO: str = OTHER\n", "CONF_FOO", 1),
|
||||
('CONF_FOO += "x"\n', "CONF_FOO", 1),
|
||||
('X = CONF_FOO = "a" + "b"\n', "CONF_FOO", 5),
|
||||
('OTHER, CONF_FOO = "a", "b"\n', "CONF_FOO", 8),
|
||||
('[OTHER, CONF_FOO] = "a", "b"\n', "CONF_FOO", 9),
|
||||
('OTHER, *CONF_FOO = ["a", "b", "c"]\n', "CONF_FOO", 9),
|
||||
('*CONF_FOO, = "xy"\n', "CONF_FOO", 2),
|
||||
('OTHER, CONF_FOO = "ab"\n', "CONF_FOO", 8),
|
||||
],
|
||||
)
|
||||
def test_rejects(content: str, name: str, col: int) -> None:
|
||||
errs = _lint(content)
|
||||
assert len(errs) == 1
|
||||
lineno, err_col, msg = errs[0]
|
||||
assert (lineno, err_col) == (1, col)
|
||||
assert name in msg
|
||||
assert "plain string literal" in msg
|
||||
|
||||
|
||||
def test_reports_each_constant_at_its_own_line() -> None:
|
||||
content = 'CONF_A = "a"\nCONF_B = X\nOTHER = 1\nCONF_C, UNIT_D = Y\n'
|
||||
assert [(line, col) for line, col, _ in _lint(content)] == [
|
||||
(2, 1),
|
||||
(4, 1),
|
||||
(4, 9),
|
||||
]
|
||||
|
||||
|
||||
def test_checks_assignments_inside_module_level_blocks() -> None:
|
||||
content = (
|
||||
"if X:\n"
|
||||
" CONF_A = A\n"
|
||||
"else:\n"
|
||||
" CONF_B = B\n"
|
||||
"try:\n"
|
||||
" CONF_C = C\n"
|
||||
"except ImportError:\n"
|
||||
" CONF_D = D\n"
|
||||
"finally:\n"
|
||||
" CONF_E = E\n"
|
||||
"with X:\n"
|
||||
" CONF_F = F\n"
|
||||
"for x in X:\n"
|
||||
" CONF_G = G\n"
|
||||
"match X:\n"
|
||||
" case 1:\n"
|
||||
" CONF_H = H\n"
|
||||
)
|
||||
assert [(line, col) for line, col, _ in _lint(content)] == [
|
||||
(2, 5),
|
||||
(4, 5),
|
||||
(6, 5),
|
||||
(8, 5),
|
||||
(10, 5),
|
||||
(12, 5),
|
||||
(14, 5),
|
||||
(17, 9),
|
||||
]
|
||||
|
||||
|
||||
def test_syntax_error_is_left_to_python_linters() -> None:
|
||||
assert _lint("CONF_FOO = (\n") == []
|
||||
|
||||
|
||||
def _registered_check() -> dict:
|
||||
return next(
|
||||
check
|
||||
for check in ci_custom.LINT_CONTENT_CHECKS
|
||||
if check["func"] is ci_custom.lint_constant_is_plain_string
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"fname",
|
||||
[
|
||||
"esphome/const.py",
|
||||
"esphome/components/const/__init__.py",
|
||||
"esphome/components/x/sensor.py",
|
||||
"esphome/core/config.py",
|
||||
],
|
||||
)
|
||||
def test_runs_on_esphome_python_files(fname: str) -> None:
|
||||
errs = ci_custom.run_check(_registered_check(), fname, Path(fname), "CONF_A = B\n")
|
||||
assert len(errs) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"fname",
|
||||
[
|
||||
"tests/component_tests/x/test_x.py",
|
||||
"script/ci-custom.py",
|
||||
"esphome/components/x/x.cpp",
|
||||
],
|
||||
)
|
||||
def test_skips_files_outside_esphome(fname: str) -> None:
|
||||
assert (
|
||||
ci_custom.run_check(_registered_check(), fname, Path(fname), "CONF_A = B\n")
|
||||
is None
|
||||
)
|
||||
@@ -146,6 +146,69 @@ def test_git_output_oserror_is_empty(script_setup: ModuleType) -> None:
|
||||
assert script_setup.git_output("status") == ""
|
||||
|
||||
|
||||
def _make_python(script_setup: ModuleType, venv: Path) -> Path:
|
||||
python = script_setup.venv_python(venv)
|
||||
python.parent.mkdir(parents=True)
|
||||
python.touch()
|
||||
return python
|
||||
|
||||
|
||||
def test_environment_ready_false_without_interpreter(
|
||||
script_setup: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.delenv("VIRTUAL_ENV", raising=False)
|
||||
with (
|
||||
patch.object(script_setup, "DEFAULT_VENV", tmp_path / "venv"),
|
||||
patch.object(script_setup.subprocess, "run") as mock_run,
|
||||
):
|
||||
assert script_setup.environment_ready() is False
|
||||
mock_run.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("returncode", "expected"), [(0, True), (1, False)])
|
||||
def test_environment_ready_checks_default_venv_import(
|
||||
script_setup: ModuleType,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
returncode: int,
|
||||
expected: bool,
|
||||
) -> None:
|
||||
monkeypatch.delenv("VIRTUAL_ENV", raising=False)
|
||||
python = _make_python(script_setup, tmp_path / "venv")
|
||||
with (
|
||||
patch.object(script_setup, "DEFAULT_VENV", tmp_path / "venv"),
|
||||
patch.object(
|
||||
script_setup.subprocess, "run", return_value=Mock(returncode=returncode)
|
||||
) as mock_run,
|
||||
):
|
||||
assert script_setup.environment_ready() is expected
|
||||
mock_run.assert_called_once_with(
|
||||
[str(python), "-I", "-c", "import esphome"],
|
||||
cwd=script_setup.ROOT,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
def test_environment_ready_uses_active_virtual_env(
|
||||
script_setup: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
active = tmp_path / "active-venv"
|
||||
monkeypatch.setenv("VIRTUAL_ENV", str(active))
|
||||
python = _make_python(script_setup, active)
|
||||
with patch.object(
|
||||
script_setup.subprocess, "run", return_value=Mock(returncode=0)
|
||||
) as mock_run:
|
||||
assert script_setup.environment_ready() is True
|
||||
assert mock_run.call_args.args[0][0] == str(python)
|
||||
|
||||
|
||||
def test_environment_ready_against_real_interpreter(script_setup: ModuleType) -> None:
|
||||
"""The import check runs for real: the test venv has ESPHome installed."""
|
||||
with patch.dict(os.environ, {"VIRTUAL_ENV": sys.prefix}):
|
||||
assert script_setup.environment_ready() is True
|
||||
|
||||
|
||||
# --- create_venv -----------------------------------------------------------
|
||||
|
||||
|
||||
@@ -436,6 +499,47 @@ def test_install_git_hooks_skips_copy_when_hooks_dir_missing(
|
||||
assert not (common_dir / "hooks").exists()
|
||||
|
||||
|
||||
def test_install_vscode_settings_copies_defaults(
|
||||
script_setup: ModuleType, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
vscode = tmp_path / ".vscode"
|
||||
vscode.mkdir()
|
||||
defaults = vscode / "settings.default.json"
|
||||
defaults.write_text('{"a": 1}')
|
||||
settings = vscode / "settings.json"
|
||||
with (
|
||||
patch.object(script_setup, "ROOT", tmp_path),
|
||||
patch.object(script_setup, "VSCODE_SETTINGS", settings),
|
||||
patch.object(script_setup, "VSCODE_DEFAULT_SETTINGS", defaults),
|
||||
):
|
||||
script_setup.install_vscode_settings()
|
||||
assert settings.read_text() == '{"a": 1}'
|
||||
assert "Created .vscode/settings.json" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_install_vscode_settings_keeps_existing(
|
||||
script_setup: ModuleType, tmp_path: Path
|
||||
) -> None:
|
||||
vscode = tmp_path / ".vscode"
|
||||
vscode.mkdir()
|
||||
defaults = vscode / "settings.default.json"
|
||||
defaults.write_text('{"a": 1}')
|
||||
settings = vscode / "settings.json"
|
||||
settings.write_text('{"mine": true}')
|
||||
with (
|
||||
patch.object(script_setup, "ROOT", tmp_path),
|
||||
patch.object(script_setup, "VSCODE_SETTINGS", settings),
|
||||
patch.object(script_setup, "VSCODE_DEFAULT_SETTINGS", defaults),
|
||||
):
|
||||
script_setup.install_vscode_settings()
|
||||
assert settings.read_text() == '{"mine": true}'
|
||||
|
||||
|
||||
def test_vscode_default_settings_file_exists(script_setup: ModuleType) -> None:
|
||||
"""The template script/setup copies from is present in the repository."""
|
||||
assert script_setup.VSCODE_DEFAULT_SETTINGS.is_file()
|
||||
|
||||
|
||||
# --- report ------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -489,12 +593,14 @@ def test_main_uses_active_virtual_env(
|
||||
patch.object(script_setup, "create_venv") as mock_create_venv,
|
||||
patch.object(script_setup, "install_dependencies") as mock_install_deps,
|
||||
patch.object(script_setup, "install_git_hooks") as mock_install_hooks,
|
||||
patch.object(script_setup, "install_vscode_settings") as mock_vscode,
|
||||
patch.object(script_setup, "report") as mock_report,
|
||||
):
|
||||
script_setup.main()
|
||||
mock_create_venv.assert_not_called()
|
||||
mock_install_deps.assert_called_once()
|
||||
mock_install_hooks.assert_called_once()
|
||||
mock_vscode.assert_called_once_with()
|
||||
mock_report.assert_called_once_with(script_setup.VENV_ACTIVE, active_venv)
|
||||
assert (tmp_path / ".temp").is_dir()
|
||||
|
||||
@@ -514,12 +620,14 @@ def test_main_reuses_existing_venv(
|
||||
patch.object(script_setup, "create_venv") as mock_create_venv,
|
||||
patch.object(script_setup, "install_dependencies") as mock_install_deps,
|
||||
patch.object(script_setup, "install_git_hooks") as mock_install_hooks,
|
||||
patch.object(script_setup, "install_vscode_settings") as mock_vscode,
|
||||
patch.object(script_setup, "report") as mock_report,
|
||||
):
|
||||
script_setup.main()
|
||||
mock_create_venv.assert_not_called()
|
||||
mock_install_deps.assert_called_once()
|
||||
mock_install_hooks.assert_called_once()
|
||||
mock_vscode.assert_called_once_with()
|
||||
mock_report.assert_called_once_with(script_setup.VENV_REUSED, default_venv)
|
||||
assert (tmp_path / ".temp").is_dir()
|
||||
|
||||
@@ -536,12 +644,14 @@ def test_main_creates_new_venv(
|
||||
patch.object(script_setup, "create_venv") as mock_create_venv,
|
||||
patch.object(script_setup, "install_dependencies") as mock_install_deps,
|
||||
patch.object(script_setup, "install_git_hooks") as mock_install_hooks,
|
||||
patch.object(script_setup, "install_vscode_settings") as mock_vscode,
|
||||
patch.object(script_setup, "report") as mock_report,
|
||||
):
|
||||
script_setup.main()
|
||||
mock_create_venv.assert_called_once_with(default_venv)
|
||||
mock_install_deps.assert_called_once()
|
||||
mock_install_hooks.assert_called_once()
|
||||
mock_vscode.assert_called_once_with()
|
||||
mock_report.assert_called_once_with(script_setup.VENV_CREATED, default_venv)
|
||||
assert (tmp_path / ".temp").is_dir()
|
||||
|
||||
@@ -553,10 +663,75 @@ def test_run_as_script_calls_main(tmp_path: Path) -> None:
|
||||
patch("subprocess.run", return_value=completed) as mock_run,
|
||||
patch("shutil.which", return_value="/usr/bin/uv"),
|
||||
patch("pathlib.Path.mkdir") as mock_mkdir,
|
||||
patch("shutil.copyfile"),
|
||||
patch.dict(os.environ, {"VIRTUAL_ENV": str(tmp_path / "env")}),
|
||||
patch.object(sys, "argv", [str(_SCRIPT)]),
|
||||
):
|
||||
runpy.run_path(str(_SCRIPT), run_name="__main__")
|
||||
|
||||
# The dependency install ran, and git reported no hooks directory to touch.
|
||||
assert mock_run.called
|
||||
mock_mkdir.assert_called_once_with(exist_ok=True)
|
||||
|
||||
|
||||
def test_parse_args_defaults(script_setup: ModuleType) -> None:
|
||||
assert script_setup.parse_args([]).if_needed is False
|
||||
assert script_setup.parse_args(["--if-needed"]).if_needed is True
|
||||
|
||||
|
||||
def test_main_if_needed_skips_when_ready(
|
||||
script_setup: ModuleType, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
with (
|
||||
patch.object(script_setup, "environment_ready", return_value=True),
|
||||
patch.object(script_setup, "create_venv") as mock_create_venv,
|
||||
patch.object(script_setup, "install_dependencies") as mock_install_deps,
|
||||
patch.object(script_setup, "install_git_hooks") as mock_install_hooks,
|
||||
patch.object(script_setup, "install_vscode_settings") as mock_vscode,
|
||||
patch.object(script_setup, "report") as mock_report,
|
||||
):
|
||||
script_setup.main(["--if-needed"])
|
||||
mock_vscode.assert_called_once_with()
|
||||
mock_create_venv.assert_not_called()
|
||||
mock_install_deps.assert_not_called()
|
||||
mock_install_hooks.assert_not_called()
|
||||
mock_report.assert_not_called()
|
||||
assert "already set up" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_main_if_needed_runs_setup_when_not_ready(
|
||||
script_setup: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.delenv("VIRTUAL_ENV", raising=False)
|
||||
default_venv = tmp_path / "venv"
|
||||
with (
|
||||
patch.object(script_setup, "ROOT", tmp_path),
|
||||
patch.object(script_setup, "DEFAULT_VENV", default_venv),
|
||||
patch.object(script_setup, "environment_ready", return_value=False),
|
||||
patch.object(script_setup, "create_venv") as mock_create_venv,
|
||||
patch.object(script_setup, "install_dependencies") as mock_install_deps,
|
||||
patch.object(script_setup, "install_git_hooks"),
|
||||
patch.object(script_setup, "install_vscode_settings"),
|
||||
patch.object(script_setup, "report") as mock_report,
|
||||
):
|
||||
script_setup.main(["--if-needed"])
|
||||
mock_create_venv.assert_called_once_with(default_venv)
|
||||
mock_install_deps.assert_called_once()
|
||||
mock_report.assert_called_once_with(script_setup.VENV_CREATED, default_venv)
|
||||
|
||||
|
||||
def test_main_without_if_needed_does_not_check_readiness(
|
||||
script_setup: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("VIRTUAL_ENV", str(tmp_path / "env"))
|
||||
with (
|
||||
patch.object(script_setup, "ROOT", tmp_path),
|
||||
patch.object(script_setup, "environment_ready") as mock_ready,
|
||||
patch.object(script_setup, "install_dependencies") as mock_install_deps,
|
||||
patch.object(script_setup, "install_git_hooks"),
|
||||
patch.object(script_setup, "install_vscode_settings"),
|
||||
patch.object(script_setup, "report"),
|
||||
):
|
||||
script_setup.main([])
|
||||
mock_ready.assert_not_called()
|
||||
mock_install_deps.assert_called_once()
|
||||
@@ -40,6 +40,7 @@ from esphome.automation import (
|
||||
WhileAction,
|
||||
XorCondition,
|
||||
_shadow_iteration,
|
||||
_validate_while_waits,
|
||||
apply_action_builder,
|
||||
apply_statements_action,
|
||||
build_action,
|
||||
@@ -385,6 +386,112 @@ def test_owning_args_swaps_every_string_ref(
|
||||
]
|
||||
|
||||
|
||||
_BUTTON_ON = {"binary_sensor.is_on": ID("button")}
|
||||
_LOG = [{"logger.log": "hi"}]
|
||||
|
||||
|
||||
def _if(condition: ConfigType) -> list[ConfigType]:
|
||||
return [{"if": {**condition, CONF_THEN: _LOG}}]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "body", "outcome"),
|
||||
[
|
||||
# Only the main loop could change the condition, and the body never lets it run
|
||||
(_BUTTON_ON, _LOG, "error"),
|
||||
(
|
||||
_BUTTON_ON,
|
||||
[{"binary_sensor.template.publish": {"id": ID("other")}}],
|
||||
"error",
|
||||
),
|
||||
(_BUTTON_ON, [{"logger.log": {"args": [Lambda("id(button).state")]}}], "error"),
|
||||
# Conditions inside the body only read
|
||||
(_BUTTON_ON, _if({"any": [{"lambda": Lambda("return true;")}]}), "error"),
|
||||
(
|
||||
_BUTTON_ON,
|
||||
[
|
||||
{
|
||||
"choose": [
|
||||
{
|
||||
"if": {"and": [{"lambda": Lambda("return true;")}]},
|
||||
CONF_THEN: _LOG,
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"error",
|
||||
),
|
||||
# Ids generated later are still None and match nothing
|
||||
(
|
||||
{"binary_sensor.is_on": {"id": ID(None)}},
|
||||
[{"switch.turn_off": {"id": ID(None)}}],
|
||||
"error",
|
||||
),
|
||||
# A body that waits, or an empty one, is fine
|
||||
(_BUTTON_ON, [{"delay": "1s"}], None),
|
||||
(_BUTTON_ON, [{"if": {CONF_THEN: [{"delay": "1s"}]}}], None),
|
||||
(_BUTTON_ON, [], None),
|
||||
# The body acts on what the condition reads
|
||||
(_BUTTON_ON, [{"binary_sensor.template.publish": {"id": ID("button")}}], None),
|
||||
(
|
||||
{"lambda": Lambda("return id(s).state;")},
|
||||
[{"switch.turn_off": {"id": ID("s")}}],
|
||||
None,
|
||||
),
|
||||
(
|
||||
{"lambda": Lambda("return id(n) < 5;")},
|
||||
[{"lambda": Lambda("id(n) += 1;")}],
|
||||
None,
|
||||
),
|
||||
(
|
||||
_BUTTON_ON,
|
||||
[
|
||||
{
|
||||
"if": {
|
||||
CONF_THEN: [
|
||||
{"lambda": Lambda("id(button).publish_state(false);")}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
None,
|
||||
),
|
||||
# Code in the condition or the body might still end the loop
|
||||
({"lambda": Lambda("return id(s).state;")}, _LOG, "warn"),
|
||||
({"and": [{"lambda": Lambda("return millis() < 5;")}]}, _LOG, "warn"),
|
||||
({"binary_sensor.is_on": {"id": ID("button"), "for": "1s"}}, _LOG, "warn"),
|
||||
(_BUTTON_ON, [{"lambda": Lambda("id(n) += 1;")}], "warn"),
|
||||
(_BUTTON_ON, [{"script.execute": ID("release")}], "warn"),
|
||||
(_BUTTON_ON, [{"component.update": ID("other")}], "warn"),
|
||||
],
|
||||
)
|
||||
def test_validate_while_waits(
|
||||
mock_registry: dict[str, RegistryEntry],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
condition: ConfigType,
|
||||
body: list[ConfigType],
|
||||
outcome: str | None,
|
||||
) -> None:
|
||||
config = {CONF_CONDITION: condition, CONF_THEN: body}
|
||||
if outcome == "error":
|
||||
with pytest.raises(cv.Invalid, match="never waits.*the device would crash"):
|
||||
_validate_while_waits(config)
|
||||
return
|
||||
assert _validate_while_waits(config) is config
|
||||
assert ("never waits" in caplog.text) is (outcome == "warn")
|
||||
|
||||
|
||||
def test_validate_while_waits_names_the_condition_inside_a_list(
|
||||
mock_registry: dict[str, RegistryEntry],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A list condition is wrapped in an `and:`; the warning names the condition in it."""
|
||||
_validate_while_waits(
|
||||
{CONF_CONDITION: {"and": [{"lambda": Lambda("return true;")}]}, CONF_THEN: _LOG}
|
||||
)
|
||||
assert "on 'lambda'" in caplog.text
|
||||
|
||||
|
||||
def test_validate_no_deferring_actions_accepts_synchronous(
|
||||
mock_registry: dict[str, RegistryEntry],
|
||||
) -> None:
|
||||
|
||||
@@ -216,6 +216,33 @@ def test_generate_cmakelists_txt_external_source_root_srcdir(tmp_component, tmp_
|
||||
assert str((source / "thing.cpp").resolve()).replace("\\", "/") in content
|
||||
|
||||
|
||||
def test_generate_cmakelists_txt_capitalised_src_dir(tmp_component, tmp_path):
|
||||
source = tmp_path / "caps_lib"
|
||||
(source / "Src").mkdir(parents=True)
|
||||
(source / "Src" / "thing.cpp").write_text("int t;")
|
||||
tmp_component.source_path = source
|
||||
tmp_component.data = {}
|
||||
|
||||
content = generate_cmakelists_txt(tmp_component)
|
||||
|
||||
# Compare case-insensitively: on macOS "src" also matches the "Src" directory
|
||||
expected = str((source / "Src" / "thing.cpp").resolve()).replace("\\", "/")
|
||||
assert expected.lower() in content.lower()
|
||||
|
||||
|
||||
def test_generate_cmakelists_txt_explicit_src_dir(tmp_component):
|
||||
(tmp_component.path / "src").mkdir()
|
||||
(tmp_component.path / "src" / "ignored.c").write_text("int i;")
|
||||
(tmp_component.path / "custom").mkdir()
|
||||
(tmp_component.path / "custom" / "used.c").write_text("int u;")
|
||||
tmp_component.data = {"build": {"srcDir": "custom"}}
|
||||
|
||||
content = generate_cmakelists_txt(tmp_component)
|
||||
|
||||
assert "used.c" in content
|
||||
assert "ignored.c" not in content
|
||||
|
||||
|
||||
def test_generate_cmakelists_txt_with_flags(tmp_component, tmp_path):
|
||||
src_dir = tmp_component.path / "src"
|
||||
src_dir.mkdir()
|
||||
@@ -470,6 +497,21 @@ def test_generate_idf_component_yml_with_dependencies(tmp_component, tmp_path):
|
||||
)
|
||||
|
||||
|
||||
def test_generate_idf_component_yml_with_two_dependencies(tmp_component, tmp_path):
|
||||
deps = []
|
||||
for name in ("dep_a", "dep_b"):
|
||||
dep = IDFComponent(name, "1.0", source=URLSource("http://dummy.com"))
|
||||
dep.path = tmp_path / name
|
||||
deps.append(dep)
|
||||
tmp_component.dependencies = deps
|
||||
tmp_component.data = {}
|
||||
|
||||
result = generate_idf_component_yml(tmp_component)
|
||||
|
||||
assert f" dep_a:\n override_path: {deps[0].path}\n" in result
|
||||
assert f" dep_b:\n override_path: {deps[1].path}\n" in result
|
||||
|
||||
|
||||
def test_generate_idf_component_yml_missing_path_raises(tmp_component):
|
||||
# A dep without a path is a contract violation — every dep is expected
|
||||
# to have been downloaded before YAML generation. Raise loudly.
|
||||
|
||||
Reference in new issue
Block a user