Compare commits

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

No files matched your search

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