1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-11 10:12:38 +00:00

Compare commits

..

10 Commits

Author SHA1 Message Date
Jonathan Swoboda
a3a2a6d965 Merge pull request #12396 from esphome/bump-2025.12.0b1
2025.12.0b1
2025-12-09 21:33:58 -05:00
Jonathan Swoboda
26770e09dc Bump version to 2025.12.0b1 2025-12-09 20:08:35 -05:00
Javier Peletier
9f2693ead5 [core] Packages refactor and conditional package inclusion (package refactor part 1) (#11605)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-10 00:59:58 +01:00
J. Nick Koston
3642399460 [tests] Fix clang-tidy warnings in custom_api_device_component fixture (#12390) 2025-12-10 00:50:26 +01:00
J. Nick Koston
3a6edbc2c7 [micronova] Fix test UART package key to match directory name (#12391) 2025-12-10 00:49:44 +01:00
J. Nick Koston
608f834eaa [ci] Isolate usb_cdc_acm in component tests due to tinyusb/usb_host conflict (#12392) 2025-12-10 00:49:29 +01:00
J. Nick Koston
5919355d18 [ci] Allow memory impact target branch build to fail without blocking CI (#12381) 2025-12-10 00:26:24 +01:00
dependabot[bot]
1e23b10eed Bump aioesphomeapi from 43.1.0 to 43.2.1 (#12385)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-09 22:02:42 +00:00
Clyde Stubbs
ad0218fd40 [mipi_rgb] Add Waveshare 3.16 (#12309) 2025-12-10 08:17:59 +11:00
Clyde Stubbs
87142efbb4 [epaper_spi] Set reasonable default update interval (#12331) 2025-12-10 06:42:11 +11:00
26 changed files with 968 additions and 531 deletions

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2025.12.0-dev
PROJECT_NUMBER = 2025.12.0b1
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a

View File

@@ -41,6 +41,7 @@ AUTO_LOAD = ["split_buffer"]
DEPENDENCIES = ["spi"]
CONF_INIT_SEQUENCE_ID = "init_sequence_id"
CONF_MINIMUM_UPDATE_INTERVAL = "minimum_update_interval"
epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi")
EPaperBase = epaper_spi_ns.class_(
@@ -71,6 +72,9 @@ TRANSFORM_OPTIONS = {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY}
def model_schema(config):
model = MODELS[config[CONF_MODEL]]
class_name = epaper_spi_ns.class_(model.class_name, EPaperBase)
minimum_update_interval = update_interval(
model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s")
)
cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required
return (
display.FULL_DISPLAY_SCHEMA.extend(
@@ -90,9 +94,9 @@ def model_schema(config):
{
cv.Optional(CONF_ROTATION, default=0): validate_rotation,
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
cv.Optional(
CONF_UPDATE_INTERVAL, default=cv.UNDEFINED
): update_interval,
cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): cv.All(
update_interval, cv.Range(min=minimum_update_interval)
),
cv.Optional(CONF_TRANSFORM): cv.Schema(
{
cv.Required(CONF_MIRROR_X): cv.boolean,
@@ -153,9 +157,8 @@ def _final_validate(config):
else:
# If no drawing methods are configured, and LVGL is not enabled, show a test card
config[CONF_SHOW_TEST_CARD] = True
config[CONF_UPDATE_INTERVAL] = core.TimePeriod(
seconds=60
).total_milliseconds
elif CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
return config

View File

@@ -286,7 +286,7 @@ void EPaperBase::initialise_() {
* @param y
* @return false if the coordinates are out of bounds
*/
bool EPaperBase::rotate_coordinates_(int &x, int &y) const {
bool EPaperBase::rotate_coordinates_(int &x, int &y) {
if (!this->get_clipping().inside(x, y))
return false;
if (this->transform_ & SWAP_XY)
@@ -297,6 +297,10 @@ bool EPaperBase::rotate_coordinates_(int &x, int &y) const {
y = this->height_ - y - 1;
if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0)
return false;
this->x_low_ = clamp_at_most(this->x_low_, x);
this->x_high_ = clamp_at_least(this->x_high_, x + 1);
this->y_low_ = clamp_at_most(this->y_low_, y);
this->y_high_ = clamp_at_least(this->y_high_, y + 1);
return true;
}
@@ -319,10 +323,6 @@ void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) {
} else {
this->buffer_[byte_position] = original | pixel_bit;
}
this->x_low_ = clamp_at_most(this->x_low_, x);
this->x_high_ = clamp_at_least(this->x_high_, x + 1);
this->y_low_ = clamp_at_most(this->y_low_, y);
this->y_high_ = clamp_at_least(this->y_high_, y + 1);
}
void EPaperBase::dump_config() {

View File

@@ -106,7 +106,7 @@ class EPaperBase : public Display,
void initialise_();
void wait_for_idle_(bool should_wait);
bool init_buffer_(size_t buffer_length);
bool rotate_coordinates_(int &x, int &y) const;
bool rotate_coordinates_(int &x, int &y);
/**
* Methods that must be implemented by concrete classes to control the display

View File

@@ -4,8 +4,8 @@ from . import EpaperModel
class SpectraE6(EpaperModel):
def __init__(self, name, class_name="EPaperSpectraE6", **kwargs):
super().__init__(name, class_name, **kwargs)
def __init__(self, name, class_name="EPaperSpectraE6", **defaults):
super().__init__(name, class_name, **defaults)
# fmt: off
def get_init_sequence(self, config: dict):
@@ -30,7 +30,7 @@ class SpectraE6(EpaperModel):
return self.defaults.get(key, fallback)
spectra_e6 = SpectraE6("spectra-e6")
spectra_e6 = SpectraE6("spectra-e6", minimum_update_interval="30s")
spectra_e6_7p3 = spectra_e6.extend(
"7.3in-Spectra-E6",

View File

@@ -498,12 +498,12 @@ void LvglComponent::setup() {
buf_bytes /= MIN_BUFFER_FRAC;
buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT
}
this->buffer_frac_ = frac;
if (buffer == nullptr) {
this->status_set_error(LOG_STR("Memory allocation failure"));
this->mark_failed();
return;
}
this->buffer_frac_ = frac;
lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buffer_pixels);
this->disp_drv_.hor_res = display->get_width();
this->disp_drv_.ver_res = display->get_height();

View File

@@ -24,7 +24,7 @@ from esphome.components.mipi import (
CONF_VSYNC_BACK_PORCH,
CONF_VSYNC_FRONT_PORCH,
CONF_VSYNC_PULSE_WIDTH,
MODE_BGR,
MODE_RGB,
PIXEL_MODE_16BIT,
PIXEL_MODE_18BIT,
DriverChip,
@@ -157,7 +157,7 @@ def model_schema(config):
model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list(
pins.gpio_output_pin_schema
),
model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(COLOR_ORDERS, upper=True),
model.option(CONF_COLOR_ORDER, MODE_RGB): cv.enum(COLOR_ORDERS, upper=True),
model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of(
*pixel_modes, lower=True
@@ -280,14 +280,9 @@ async def to_code(config):
red_pins = config[CONF_DATA_PINS][CONF_RED]
green_pins = config[CONF_DATA_PINS][CONF_GREEN]
blue_pins = config[CONF_DATA_PINS][CONF_BLUE]
if config[CONF_COLOR_ORDER] == "BGR":
dpins.extend(red_pins)
dpins.extend(green_pins)
dpins.extend(blue_pins)
else:
dpins.extend(blue_pins)
dpins.extend(green_pins)
dpins.extend(red_pins)
dpins.extend(blue_pins)
dpins.extend(green_pins)
dpins.extend(red_pins)
# swap bytes to match big-endian format
dpins = dpins[8:16] + dpins[0:8]
else:

View File

@@ -371,17 +371,10 @@ void MipiRgb::dump_config() {
get_pin_name(this->de_pin_).c_str(), get_pin_name(this->pclk_pin_).c_str(),
get_pin_name(this->hsync_pin_).c_str(), get_pin_name(this->vsync_pin_).c_str());
if (this->madctl_ & MADCTL_BGR) {
this->dump_pins_(8, 13, "Blue", 0);
this->dump_pins_(13, 16, "Green", 0);
this->dump_pins_(0, 3, "Green", 3);
this->dump_pins_(3, 8, "Red", 0);
} else {
this->dump_pins_(8, 13, "Red", 0);
this->dump_pins_(13, 16, "Green", 0);
this->dump_pins_(0, 3, "Green", 3);
this->dump_pins_(3, 8, "Blue", 0);
}
this->dump_pins_(8, 13, "Blue", 0);
this->dump_pins_(13, 16, "Green", 0);
this->dump_pins_(0, 3, "Green", 3);
this->dump_pins_(3, 8, "Red", 0);
}
} // namespace mipi_rgb

View File

@@ -7,7 +7,6 @@ ST7701S(
"T-PANEL-S3",
width=480,
height=480,
color_order="BGR",
invert_colors=False,
swap_xy=UNDEFINED,
spi_mode="MODE3",
@@ -56,7 +55,6 @@ t_rgb = ST7701S(
"T-RGB-2.1",
width=480,
height=480,
color_order="BGR",
pixel_mode="18bit",
invert_colors=False,
swap_xy=UNDEFINED,

View File

@@ -82,7 +82,6 @@ st7701s.extend(
"MAKERFABS-4",
width=480,
height=480,
color_order="RGB",
invert_colors=True,
pixel_mode="18bit",
cs_pin=1,

View File

@@ -1,13 +1,13 @@
from esphome.components.mipi import DriverChip
from esphome.components.mipi import DriverChip, delay
from esphome.config_validation import UNDEFINED
from .st7701s import st7701s
# fmt: off
wave_4_3 = DriverChip(
"ESP32-S3-TOUCH-LCD-4.3",
swap_xy=UNDEFINED,
initsequence=(),
color_order="RGB",
width=800,
height=480,
pclk_frequency="16MHz",
@@ -55,10 +55,9 @@ wave_4_3.extend(
)
st7701s.extend(
"WAVESHARE-4-480x480",
"WAVESHARE-4-480X480",
data_rate="2MHz",
spi_mode="MODE3",
color_order="BGR",
pixel_mode="18bit",
width=480,
height=480,
@@ -76,3 +75,72 @@ st7701s.extend(
"blue": [5, 45, 48, 47, 21],
},
)
st7701s.extend(
"WAVESHARE-3.16-320X820",
width=320,
height=820,
de_pin=40,
hsync_pin=38,
vsync_pin=39,
pclk_pin=41,
cs_pin={
"number": 0,
"ignore_strapping_warning": True,
},
pclk_frequency="18MHz",
reset_pin=16,
hsync_back_porch=30,
hsync_front_porch=30,
hsync_pulse_width=6,
vsync_back_porch=20,
vsync_front_porch=20,
vsync_pulse_width=40,
data_pins={
"red": [17, 46, 3, 8, 18],
"green": [14, 13, 12, 11, 10, 9],
"blue": [21, 5, 45, 48, 47],
},
initsequence=(
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x13),
(0xEF, 0x08),
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x10),
(0xC0, 0xE5, 0x02),
(0xC1, 0x15, 0x0A),
(0xC2, 0x07, 0x02),
(0xCC, 0x10),
(0xB0, 0x00, 0x08, 0x51, 0x0D, 0xCE, 0x06, 0x00, 0x08, 0x08, 0x24, 0x05, 0xD0, 0x0F, 0x6F, 0x36, 0x1F),
(0xB1, 0x00, 0x10, 0x4F, 0x0C, 0x11, 0x05, 0x00, 0x07, 0x07, 0x18, 0x02, 0xD3, 0x11, 0x6E, 0x34, 0x1F),
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x11),
(0xB0, 0x4D),
(0xB1, 0x37),
(0xB2, 0x87),
(0xB3, 0x80),
(0xB5, 0x4A),
(0xB7, 0x85),
(0xB8, 0x21),
(0xB9, 0x00, 0x13),
(0xC0, 0x09),
(0xC1, 0x78),
(0xC2, 0x78),
(0xD0, 0x88),
(0xE0, 0x80, 0x00, 0x02),
(0xE1, 0x0F, 0xA0, 0x00, 0x00, 0x10, 0xA0, 0x00, 0x00, 0x00, 0x60, 0x60),
(0xE2, 0x30, 0x30, 0x60, 0x60, 0x45, 0xA0, 0x00, 0x00, 0x46, 0xA0, 0x00, 0x00, 0x00),
(0xE3, 0x00, 0x00, 0x33, 0x33),
(0xE4, 0x44, 0x44),
(0xE5, 0x0F, 0x4A, 0xA0, 0xA0, 0x11, 0x4A, 0xA0, 0xA0, 0x13, 0x4A, 0xA0, 0xA0, 0x15, 0x4A, 0xA0, 0xA0),
(0xE6, 0x00, 0x00, 0x33, 0x33),
(0xE7, 0x44, 0x44),
(0xE8, 0x10, 0x4A, 0xA0, 0xA0, 0x12, 0x4A, 0xA0, 0xA0, 0x14, 0x4A, 0xA0, 0xA0, 0x16, 0x4A, 0xA0, 0xA0),
(0xEB, 0x02, 0x00, 0x4E, 0x4E, 0xEE, 0x44, 0x00),
(0xED, 0xFF, 0xFF, 0x04, 0x56, 0x72, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x27, 0x65, 0x40, 0xFF, 0xFF),
(0xEF, 0x08, 0x08, 0x08, 0x40, 0x3F, 0x64),
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x13),
(0xE8, 0x00, 0x0E),
(0xE8, 0x00, 0x0C),
delay(10),
(0xE8, 0x00, 0x00),
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x00),
)
)

View File

@@ -1,5 +1,9 @@
from collections import UserDict
from collections.abc import Callable
from functools import reduce
import logging
from pathlib import Path
from typing import Any
from esphome import git, yaml_util
from esphome.components.substitutions.jinja import has_jinja
@@ -15,6 +19,7 @@ from esphome.const import (
CONF_PATH,
CONF_REF,
CONF_REFRESH,
CONF_SUBSTITUTIONS,
CONF_URL,
CONF_USERNAME,
CONF_VARS,
@@ -27,32 +32,43 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = CONF_PACKAGES
def valid_package_contents(package_config: dict):
"""Validates that a package_config that will be merged looks as much as possible to a valid config
to fail early on obvious mistakes."""
if isinstance(package_config, dict):
if CONF_URL in package_config:
# If a URL key is found, then make sure the config conforms to a remote package schema:
return REMOTE_PACKAGE_SCHEMA(package_config)
def validate_has_jinja(value: Any):
if not isinstance(value, str) or not has_jinja(value):
raise cv.Invalid("string does not contain Jinja syntax")
return value
# Validate manually since Voluptuous would regenerate dicts and lose metadata
# such as ESPHomeDataBase
for k, v in package_config.items():
if not isinstance(k, str):
raise cv.Invalid("Package content keys must be strings")
if isinstance(v, (dict, list, Remove)):
continue # e.g. script: [], psram: !remove, logger: {level: debug}
if v is None:
continue # e.g. web_server:
if isinstance(v, str) and has_jinja(v):
# e.g: remote package shorthand:
# package_name: github://esphome/repo/file.yaml@${ branch }
continue
raise cv.Invalid("Invalid component content in package definition")
return package_config
def valid_package_contents(allow_jinja: bool = True) -> Callable[[Any], dict]:
"""Returns a validator that checks if a package_config that will be merged looks as
much as possible to a valid config to fail early on obvious mistakes."""
raise cv.Invalid("Package contents must be a dict")
def validator(package_config: dict) -> dict:
if isinstance(package_config, dict):
if CONF_URL in package_config:
# If a URL key is found, then make sure the config conforms to a remote package schema:
return REMOTE_PACKAGE_SCHEMA(package_config)
# Validate manually since Voluptuous would regenerate dicts and lose metadata
# such as ESPHomeDataBase
for k, v in package_config.items():
if not isinstance(k, str):
raise cv.Invalid("Package content keys must be strings")
if isinstance(v, (dict, list, Remove)):
continue # e.g. script: [], psram: !remove, logger: {level: debug}
if v is None:
continue # e.g. web_server:
if allow_jinja and isinstance(v, str) and has_jinja(v):
# e.g: remote package shorthand:
# package_name: github://esphome/repo/file.yaml@${ branch }, or:
# switch: ${ expression that evals to a switch }
continue
raise cv.Invalid("Invalid component content in package definition")
return package_config
raise cv.Invalid("Package contents must be a dict")
return validator
def expand_file_to_files(config: dict):
@@ -142,7 +158,10 @@ REMOTE_PACKAGE_SCHEMA = cv.All(
PACKAGE_SCHEMA = cv.Any( # A package definition is either:
validate_source_shorthand, # A git URL shorthand string that expands to a remote package schema, or
REMOTE_PACKAGE_SCHEMA, # a valid remote package schema, or
valid_package_contents, # Something that at least looks like an actual package, e.g. {wifi:{ssid: xxx}}
validate_has_jinja, # a Jinja string that may resolve to a package, or
valid_package_contents(
allow_jinja=True
), # Something that at least looks like an actual package, e.g. {wifi:{ssid: xxx}}
# which will have to be fully validated later as per each component's schema.
)
@@ -235,32 +254,84 @@ def _process_remote_package(config: dict, skip_update: bool = False) -> dict:
return {"packages": packages}
def _process_package(package_config, config, skip_update: bool = False):
recursive_package = package_config
if CONF_URL in package_config:
package_config = _process_remote_package(package_config, skip_update)
if isinstance(package_config, dict):
recursive_package = do_packages_pass(package_config, skip_update)
return merge_config(recursive_package, config)
def do_packages_pass(config: dict, skip_update: bool = False):
def _walk_packages(
config: dict, callback: Callable[[dict], dict], validate_deprecated: bool = True
) -> dict:
if CONF_PACKAGES not in config:
return config
packages = config[CONF_PACKAGES]
with cv.prepend_path(CONF_PACKAGES):
# The following block and `validate_deprecated` parameter can be safely removed
# once single-package deprecation is effective
if validate_deprecated:
packages = CONFIG_SCHEMA(packages)
with cv.prepend_path(CONF_PACKAGES):
if isinstance(packages, dict):
for package_name, package_config in reversed(packages.items()):
with cv.prepend_path(package_name):
config = _process_package(package_config, config, skip_update)
package_config = callback(package_config)
packages[package_name] = _walk_packages(package_config, callback)
elif isinstance(packages, list):
for package_config in reversed(packages):
config = _process_package(package_config, config, skip_update)
for idx in reversed(range(len(packages))):
with cv.prepend_path(idx):
package_config = callback(packages[idx])
packages[idx] = _walk_packages(package_config, callback)
else:
raise cv.Invalid(
f"Packages must be a key to value mapping or list, got {type(packages)} instead"
)
del config[CONF_PACKAGES]
config[CONF_PACKAGES] = packages
return config
def do_packages_pass(config: dict, skip_update: bool = False) -> dict:
"""Processes, downloads and validates all packages in the config.
Also extracts and merges all substitutions found in packages into the main config substitutions.
"""
if CONF_PACKAGES not in config:
return config
substitutions = UserDict(config.pop(CONF_SUBSTITUTIONS, {}))
def process_package_callback(package_config: dict) -> dict:
"""This will be called for each package found in the config."""
package_config = PACKAGE_SCHEMA(package_config)
if isinstance(package_config, str):
return package_config # Jinja string, skip processing
if CONF_URL in package_config:
package_config = _process_remote_package(package_config, skip_update)
# Extract substitutions from the package and merge them into the main substitutions:
substitutions.data = merge_config(
package_config.pop(CONF_SUBSTITUTIONS, {}), substitutions.data
)
return package_config
_walk_packages(config, process_package_callback)
if substitutions:
config[CONF_SUBSTITUTIONS] = substitutions.data
return config
def merge_packages(config: dict) -> dict:
"""Merges all packages into the main config and removes the `packages:` key."""
if CONF_PACKAGES not in config:
return config
# Build flat list of all package configs to merge in priority order:
merge_list: list[dict] = []
validate_package = valid_package_contents(allow_jinja=False)
def process_package_callback(package_config: dict) -> dict:
"""This will be called for each package found in the config."""
merge_list.append(validate_package(package_config))
return package_config
_walk_packages(config, process_package_callback, validate_deprecated=False)
# Merge all packages into the main config:
config = reduce(lambda new, old: merge_config(old, new), merge_list, config)
del config[CONF_PACKAGES]
return config

View File

@@ -1012,14 +1012,20 @@ def validate_config(
CORE.raw_config = config
# 1.1. Resolve !extend and !remove and check for REPLACEME
# 1.1. Merge packages
if CONF_PACKAGES in config:
from esphome.components.packages import merge_packages
config = merge_packages(config)
# 1.2. Resolve !extend and !remove and check for REPLACEME
# After this step, there will not be any Extend or Remove values in the config anymore
try:
resolve_extend_remove(config)
except vol.Invalid as err:
result.add_error(err)
# 1.2. Load external_components
# 1.3. Load external_components
if CONF_EXTERNAL_COMPONENTS in config:
from esphome.components.external_components import do_external_components_pass

View File

@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2025.12.0-dev"
__version__ = "2025.12.0b1"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -1,6 +1,5 @@
#include "esphome/core/component.h"
THIS IS A SYNTAX ERROR !
#include <cinttypes>
#include <limits>
#include <memory>
@@ -14,498 +13,494 @@ THIS IS A SYNTAX ERROR !
#include "esphome/components/runtime_stats/runtime_stats.h"
#endif
namespace esphome {
namespace esphome {
static const char *const TAG = "component";
static const char *const TAG = "component";
// Global vectors for component data that doesn't belong in every instance.
// Using vector instead of unordered_map for both because:
// - Much lower memory overhead (8 bytes per entry vs 20+ for unordered_map)
// - Linear search is fine for small n (typically < 5 entries)
// - These are rarely accessed (setup only or error cases only)
// Global vectors for component data that doesn't belong in every instance.
// Using vector instead of unordered_map for both because:
// - Much lower memory overhead (8 bytes per entry vs 20+ for unordered_map)
// - Linear search is fine for small n (typically < 5 entries)
// - These are rarely accessed (setup only or error cases only)
// Component error messages - only stores messages for failed components
// Lazy allocated since most configs have zero failures
// Note: We don't clear this vector because:
// 1. Components are never destroyed in ESPHome
// 2. Failed components remain failed (no recovery mechanism)
// 3. Memory usage is minimal (only failures with custom messages are stored)
// Component error messages - only stores messages for failed components
// Lazy allocated since most configs have zero failures
// Note: We don't clear this vector because:
// 1. Components are never destroyed in ESPHome
// 2. Failed components remain failed (no recovery mechanism)
// 3. Memory usage is minimal (only failures with custom messages are stored)
// Using namespace-scope static to avoid guard variables (saves 16 bytes total)
// This is safe because ESPHome is single-threaded during initialization
namespace {
struct ComponentErrorMessage {
const Component *component;
const char *message;
// Track if message is flash pointer (needs LOG_STR_ARG) or RAM pointer
// Remove before 2026.6.0 when deprecated const char* API is removed
bool is_flash_ptr;
};
struct ComponentPriorityOverride {
const Component *component;
float priority;
};
// Error messages for failed components
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::unique_ptr<std::vector<ComponentErrorMessage>> component_error_messages;
// Setup priority overrides - freed after setup completes
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::unique_ptr<std::vector<ComponentPriorityOverride>> setup_priority_overrides;
// Helper to store error messages - reduces duplication between deprecated and new API
// Using namespace-scope static to avoid guard variables (saves 16 bytes total)
// This is safe because ESPHome is single-threaded during initialization
namespace {
struct ComponentErrorMessage {
const Component *component;
const char *message;
// Track if message is flash pointer (needs LOG_STR_ARG) or RAM pointer
// Remove before 2026.6.0 when deprecated const char* API is removed
void store_component_error_message(const Component *component, const char *message, bool is_flash_ptr) {
// Lazy allocate the error messages vector if needed
if (!component_error_messages) {
component_error_messages = std::make_unique<std::vector<ComponentErrorMessage>>();
bool is_flash_ptr;
};
struct ComponentPriorityOverride {
const Component *component;
float priority;
};
// Error messages for failed components
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::unique_ptr<std::vector<ComponentErrorMessage>> component_error_messages;
// Setup priority overrides - freed after setup completes
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::unique_ptr<std::vector<ComponentPriorityOverride>> setup_priority_overrides;
// Helper to store error messages - reduces duplication between deprecated and new API
// Remove before 2026.6.0 when deprecated const char* API is removed
void store_component_error_message(const Component *component, const char *message, bool is_flash_ptr) {
// Lazy allocate the error messages vector if needed
if (!component_error_messages) {
component_error_messages = std::make_unique<std::vector<ComponentErrorMessage>>();
}
// Check if this component already has an error message
for (auto &entry : *component_error_messages) {
if (entry.component == component) {
entry.message = message;
entry.is_flash_ptr = is_flash_ptr;
return;
}
// Check if this component already has an error message
for (auto &entry : *component_error_messages) {
if (entry.component == component) {
entry.message = message;
entry.is_flash_ptr = is_flash_ptr;
return;
}
}
// Add new error message
component_error_messages->emplace_back(ComponentErrorMessage{component, message, is_flash_ptr});
}
} // namespace
// Add new error message
component_error_messages->emplace_back(ComponentErrorMessage{component, message, is_flash_ptr});
}
} // namespace
namespace setup_priority {
namespace setup_priority {
const float BUS = 1000.0f;
const float IO = 900.0f;
const float HARDWARE = 800.0f;
const float DATA = 600.0f;
const float PROCESSOR = 400.0;
const float BLUETOOTH = 350.0f;
const float AFTER_BLUETOOTH = 300.0f;
const float WIFI = 250.0f;
const float ETHERNET = 250.0f;
const float BEFORE_CONNECTION = 220.0f;
const float AFTER_WIFI = 200.0f;
const float AFTER_CONNECTION = 100.0f;
const float LATE = -100.0f;
const float BUS = 1000.0f;
const float IO = 900.0f;
const float HARDWARE = 800.0f;
const float DATA = 600.0f;
const float PROCESSOR = 400.0;
const float BLUETOOTH = 350.0f;
const float AFTER_BLUETOOTH = 300.0f;
const float WIFI = 250.0f;
const float ETHERNET = 250.0f;
const float BEFORE_CONNECTION = 220.0f;
const float AFTER_WIFI = 200.0f;
const float AFTER_CONNECTION = 100.0f;
const float LATE = -100.0f;
} // namespace setup_priority
} // namespace setup_priority
// Component state uses bits 0-2 (8 states, 5 used)
const uint8_t COMPONENT_STATE_MASK = 0x07;
const uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00;
const uint8_t COMPONENT_STATE_SETUP = 0x01;
const uint8_t COMPONENT_STATE_LOOP = 0x02;
const uint8_t COMPONENT_STATE_FAILED = 0x03;
const uint8_t COMPONENT_STATE_LOOP_DONE = 0x04;
// Status LED uses bits 3-4
const uint8_t STATUS_LED_MASK = 0x18;
const uint8_t STATUS_LED_OK = 0x00;
const uint8_t STATUS_LED_WARNING = 0x08; // Bit 3
const uint8_t STATUS_LED_ERROR = 0x10; // Bit 4
// Component state uses bits 0-2 (8 states, 5 used)
const uint8_t COMPONENT_STATE_MASK = 0x07;
const uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00;
const uint8_t COMPONENT_STATE_SETUP = 0x01;
const uint8_t COMPONENT_STATE_LOOP = 0x02;
const uint8_t COMPONENT_STATE_FAILED = 0x03;
const uint8_t COMPONENT_STATE_LOOP_DONE = 0x04;
// Status LED uses bits 3-4
const uint8_t STATUS_LED_MASK = 0x18;
const uint8_t STATUS_LED_OK = 0x00;
const uint8_t STATUS_LED_WARNING = 0x08; // Bit 3
const uint8_t STATUS_LED_ERROR = 0x10; // Bit 4
const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning
const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again
const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning
const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again
uint32_t global_state = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
uint32_t global_state = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
float Component::get_loop_priority() const { return 0.0f; }
float Component::get_loop_priority() const { return 0.0f; }
float Component::get_setup_priority() const { return setup_priority::DATA; }
float Component::get_setup_priority() const { return setup_priority::DATA; }
void Component::setup() {}
void Component::setup() {}
void Component::loop() {}
void Component::loop() {}
void Component::set_interval(const std::string &name, uint32_t interval, std::function<void()> &&f) { // NOLINT
App.scheduler.set_interval(this, name, interval, std::move(f));
}
void Component::set_interval(const std::string &name, uint32_t interval, std::function<void()> &&f) { // NOLINT
App.scheduler.set_interval(this, name, interval, std::move(f));
}
void Component::set_interval(const char *name, uint32_t interval, std::function<void()> &&f) { // NOLINT
App.scheduler.set_interval(this, name, interval, std::move(f));
}
void Component::set_interval(const char *name, uint32_t interval, std::function<void()> &&f) { // NOLINT
App.scheduler.set_interval(this, name, interval, std::move(f));
}
bool Component::cancel_interval(const std::string &name) { // NOLINT
return App.scheduler.cancel_interval(this, name);
}
bool Component::cancel_interval(const std::string &name) { // NOLINT
return App.scheduler.cancel_interval(this, name);
}
bool Component::cancel_interval(const char *name) { // NOLINT
return App.scheduler.cancel_interval(this, name);
}
bool Component::cancel_interval(const char *name) { // NOLINT
return App.scheduler.cancel_interval(this, name);
}
void Component::set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor) { // NOLINT
App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
}
void Component::set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor) { // NOLINT
App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
}
void Component::set_retry(const char *name, uint32_t initial_wait_time, uint8_t max_attempts,
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor) { // NOLINT
App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
}
void Component::set_retry(const char *name, uint32_t initial_wait_time, uint8_t max_attempts,
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor) { // NOLINT
App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
}
bool Component::cancel_retry(const std::string &name) { // NOLINT
return App.scheduler.cancel_retry(this, name);
}
bool Component::cancel_retry(const std::string &name) { // NOLINT
return App.scheduler.cancel_retry(this, name);
}
bool Component::cancel_retry(const char *name) { // NOLINT
return App.scheduler.cancel_retry(this, name);
}
bool Component::cancel_retry(const char *name) { // NOLINT
return App.scheduler.cancel_retry(this, name);
}
void Component::set_timeout(const std::string &name, uint32_t timeout, std::function<void()> &&f) { // NOLINT
App.scheduler.set_timeout(this, name, timeout, std::move(f));
}
void Component::set_timeout(const std::string &name, uint32_t timeout, std::function<void()> &&f) { // NOLINT
App.scheduler.set_timeout(this, name, timeout, std::move(f));
}
void Component::set_timeout(const char *name, uint32_t timeout, std::function<void()> &&f) { // NOLINT
App.scheduler.set_timeout(this, name, timeout, std::move(f));
}
void Component::set_timeout(const char *name, uint32_t timeout, std::function<void()> &&f) { // NOLINT
App.scheduler.set_timeout(this, name, timeout, std::move(f));
}
bool Component::cancel_timeout(const std::string &name) { // NOLINT
return App.scheduler.cancel_timeout(this, name);
}
bool Component::cancel_timeout(const std::string &name) { // NOLINT
return App.scheduler.cancel_timeout(this, name);
}
bool Component::cancel_timeout(const char *name) { // NOLINT
return App.scheduler.cancel_timeout(this, name);
}
bool Component::cancel_timeout(const char *name) { // NOLINT
return App.scheduler.cancel_timeout(this, name);
}
void Component::call_loop() { this->loop(); }
void Component::call_setup() { this->setup(); }
void Component::call_dump_config() {
this->dump_config();
if (this->is_failed()) {
// Look up error message from global vector
const char *error_msg = nullptr;
bool is_flash_ptr = false;
if (component_error_messages) {
for (const auto &entry : *component_error_messages) {
if (entry.component == this) {
error_msg = entry.message;
is_flash_ptr = entry.is_flash_ptr;
break;
}
void Component::call_loop() { this->loop(); }
void Component::call_setup() { this->setup(); }
void Component::call_dump_config() {
this->dump_config();
if (this->is_failed()) {
// Look up error message from global vector
const char *error_msg = nullptr;
bool is_flash_ptr = false;
if (component_error_messages) {
for (const auto &entry : *component_error_messages) {
if (entry.component == this) {
error_msg = entry.message;
is_flash_ptr = entry.is_flash_ptr;
break;
}
}
// Log with appropriate format based on pointer type
ESP_LOGE(TAG, " %s is marked FAILED: %s", LOG_STR_ARG(this->get_component_log_str()),
error_msg ? (is_flash_ptr ? LOG_STR_ARG((const LogString *) error_msg) : error_msg)
: LOG_STR_LITERAL("unspecified"));
}
// Log with appropriate format based on pointer type
ESP_LOGE(TAG, " %s is marked FAILED: %s", LOG_STR_ARG(this->get_component_log_str()),
error_msg ? (is_flash_ptr ? LOG_STR_ARG((const LogString *) error_msg) : error_msg)
: LOG_STR_LITERAL("unspecified"));
}
}
uint8_t Component::get_component_state() const { return this->component_state_; }
void Component::call() {
uint8_t state = this->component_state_ & COMPONENT_STATE_MASK;
switch (state) {
case COMPONENT_STATE_CONSTRUCTION: {
// State Construction: Call setup and set state to setup
this->set_component_state_(COMPONENT_STATE_SETUP);
ESP_LOGV(TAG, "Setup %s", LOG_STR_ARG(this->get_component_log_str()));
uint8_t Component::get_component_state() const { return this->component_state_; }
void Component::call() {
uint8_t state = this->component_state_ & COMPONENT_STATE_MASK;
switch (state) {
case COMPONENT_STATE_CONSTRUCTION: {
// State Construction: Call setup and set state to setup
this->set_component_state_(COMPONENT_STATE_SETUP);
ESP_LOGV(TAG, "Setup %s", LOG_STR_ARG(this->get_component_log_str()));
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
uint32_t start_time = millis();
uint32_t start_time = millis();
#endif
this->call_setup();
this->call_setup();
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
uint32_t setup_time = millis() - start_time;
ESP_LOGCONFIG(TAG, "Setup %s took %ums", LOG_STR_ARG(this->get_component_log_str()), (unsigned) setup_time);
uint32_t setup_time = millis() - start_time;
ESP_LOGCONFIG(TAG, "Setup %s took %ums", LOG_STR_ARG(this->get_component_log_str()), (unsigned) setup_time);
#endif
break;
}
case COMPONENT_STATE_SETUP:
// State setup: Call first loop and set state to loop
this->set_component_state_(COMPONENT_STATE_LOOP);
this->call_loop();
break;
case COMPONENT_STATE_LOOP:
// State loop: Call loop
this->call_loop();
break;
case COMPONENT_STATE_FAILED:
// State failed: Do nothing
case COMPONENT_STATE_LOOP_DONE:
// State loop done: Do nothing, component has finished its work
default:
break;
break;
}
case COMPONENT_STATE_SETUP:
// State setup: Call first loop and set state to loop
this->set_component_state_(COMPONENT_STATE_LOOP);
this->call_loop();
break;
case COMPONENT_STATE_LOOP:
// State loop: Call loop
this->call_loop();
break;
case COMPONENT_STATE_FAILED:
// State failed: Do nothing
case COMPONENT_STATE_LOOP_DONE:
// State loop done: Do nothing, component has finished its work
default:
break;
}
const LogString *Component::get_component_log_str() const {
return this->component_source_ == nullptr ? LOG_STR("<unknown>") : this->component_source_;
}
bool Component::should_warn_of_blocking(uint32_t blocking_time) {
if (blocking_time > this->warn_if_blocking_over_) {
// Prevent overflow when adding increment - if we're about to overflow, just max out
if (blocking_time + WARN_IF_BLOCKING_INCREMENT_MS < blocking_time ||
blocking_time + WARN_IF_BLOCKING_INCREMENT_MS > std::numeric_limits<uint16_t>::max()) {
this->warn_if_blocking_over_ = std::numeric_limits<uint16_t>::max();
} else {
this->warn_if_blocking_over_ = static_cast<uint16_t>(blocking_time + WARN_IF_BLOCKING_INCREMENT_MS);
}
return true;
}
const LogString *Component::get_component_log_str() const {
return this->component_source_ == nullptr ? LOG_STR("<unknown>") : this->component_source_;
}
bool Component::should_warn_of_blocking(uint32_t blocking_time) {
if (blocking_time > this->warn_if_blocking_over_) {
// Prevent overflow when adding increment - if we're about to overflow, just max out
if (blocking_time + WARN_IF_BLOCKING_INCREMENT_MS < blocking_time ||
blocking_time + WARN_IF_BLOCKING_INCREMENT_MS > std::numeric_limits<uint16_t>::max()) {
this->warn_if_blocking_over_ = std::numeric_limits<uint16_t>::max();
} else {
this->warn_if_blocking_over_ = static_cast<uint16_t>(blocking_time + WARN_IF_BLOCKING_INCREMENT_MS);
}
return false;
return true;
}
void Component::mark_failed() {
ESP_LOGE(TAG, "%s was marked as failed", LOG_STR_ARG(this->get_component_log_str()));
this->set_component_state_(COMPONENT_STATE_FAILED);
this->status_set_error();
// Also remove from loop since failed components shouldn't loop
return false;
}
void Component::mark_failed() {
ESP_LOGE(TAG, "%s was marked as failed", LOG_STR_ARG(this->get_component_log_str()));
this->set_component_state_(COMPONENT_STATE_FAILED);
this->status_set_error();
// Also remove from loop since failed components shouldn't loop
App.disable_component_loop_(this);
}
void Component::set_component_state_(uint8_t state) {
this->component_state_ &= ~COMPONENT_STATE_MASK;
this->component_state_ |= state;
}
void Component::disable_loop() {
if ((this->component_state_ & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) {
ESP_LOGVV(TAG, "%s loop disabled", LOG_STR_ARG(this->get_component_log_str()));
this->set_component_state_(COMPONENT_STATE_LOOP_DONE);
App.disable_component_loop_(this);
}
void Component::set_component_state_(uint8_t state) {
this->component_state_ &= ~COMPONENT_STATE_MASK;
this->component_state_ |= state;
}
void Component::enable_loop() {
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) {
ESP_LOGVV(TAG, "%s loop enabled", LOG_STR_ARG(this->get_component_log_str()));
this->set_component_state_(COMPONENT_STATE_LOOP);
App.enable_component_loop_(this);
}
void Component::disable_loop() {
if ((this->component_state_ & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) {
ESP_LOGVV(TAG, "%s loop disabled", LOG_STR_ARG(this->get_component_log_str()));
this->set_component_state_(COMPONENT_STATE_LOOP_DONE);
App.disable_component_loop_(this);
}
}
void IRAM_ATTR HOT Component::enable_loop_soon_any_context() {
// This method is thread and ISR-safe because:
// 1. Only performs simple assignments to volatile variables (atomic on all platforms)
// 2. No read-modify-write operations that could be interrupted
// 3. No memory allocation, object construction, or function calls
// 4. IRAM_ATTR ensures code is in IRAM, not flash (required for ISR execution)
// 5. Components are never destroyed, so no use-after-free concerns
// 6. App is guaranteed to be initialized before any ISR could fire
// 7. Multiple ISR/thread calls are safe - just sets the same flags to true
// 8. Race condition with main loop is handled by clearing flag before processing
this->pending_enable_loop_ = true;
App.has_pending_enable_loop_requests_ = true;
}
void Component::reset_to_construction_state() {
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) {
ESP_LOGI(TAG, "%s is being reset to construction state", LOG_STR_ARG(this->get_component_log_str()));
this->set_component_state_(COMPONENT_STATE_CONSTRUCTION);
// Clear error status when resetting
this->status_clear_error();
}
void Component::enable_loop() {
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) {
ESP_LOGVV(TAG, "%s loop enabled", LOG_STR_ARG(this->get_component_log_str()));
this->set_component_state_(COMPONENT_STATE_LOOP);
App.enable_component_loop_(this);
}
}
void IRAM_ATTR HOT Component::enable_loop_soon_any_context() {
// This method is thread and ISR-safe because:
// 1. Only performs simple assignments to volatile variables (atomic on all platforms)
// 2. No read-modify-write operations that could be interrupted
// 3. No memory allocation, object construction, or function calls
// 4. IRAM_ATTR ensures code is in IRAM, not flash (required for ISR execution)
// 5. Components are never destroyed, so no use-after-free concerns
// 6. App is guaranteed to be initialized before any ISR could fire
// 7. Multiple ISR/thread calls are safe - just sets the same flags to true
// 8. Race condition with main loop is handled by clearing flag before processing
this->pending_enable_loop_ = true;
App.has_pending_enable_loop_requests_ = true;
}
void Component::reset_to_construction_state() {
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) {
ESP_LOGI(TAG, "%s is being reset to construction state", LOG_STR_ARG(this->get_component_log_str()));
this->set_component_state_(COMPONENT_STATE_CONSTRUCTION);
// Clear error status when resetting
this->status_clear_error();
}
}
bool Component::is_in_loop_state() const {
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP;
}
void Component::defer(std::function<void()> && f) { // NOLINT
App.scheduler.set_timeout(this, static_cast<const char *>(nullptr), 0, std::move(f));
}
bool Component::cancel_defer(const std::string &name) { // NOLINT
return App.scheduler.cancel_timeout(this, name);
}
void Component::defer(const std::string &name, std::function<void()> &&f) { // NOLINT
App.scheduler.set_timeout(this, name, 0, std::move(f));
}
void Component::defer(const char *name, std::function<void()> &&f) { // NOLINT
App.scheduler.set_timeout(this, name, 0, std::move(f));
}
void Component::set_timeout(uint32_t timeout, std::function<void()> && f) { // NOLINT
App.scheduler.set_timeout(this, static_cast<const char *>(nullptr), timeout, std::move(f));
}
void Component::set_interval(uint32_t interval, std::function<void()> && f) { // NOLINT
App.scheduler.set_interval(this, static_cast<const char *>(nullptr), interval, std::move(f));
}
void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> && f,
float backoff_increase_factor) { // NOLINT
App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
}
bool Component::is_failed() const {
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED;
}
bool Component::is_ready() const {
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP ||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE ||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP;
}
bool Component::is_idle() const {
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE;
}
bool Component::can_proceed() { return true; }
bool Component::status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; }
bool Component::status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; }
}
bool Component::is_in_loop_state() const {
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP;
}
void Component::defer(std::function<void()> &&f) { // NOLINT
App.scheduler.set_timeout(this, static_cast<const char *>(nullptr), 0, std::move(f));
}
bool Component::cancel_defer(const std::string &name) { // NOLINT
return App.scheduler.cancel_timeout(this, name);
}
void Component::defer(const std::string &name, std::function<void()> &&f) { // NOLINT
App.scheduler.set_timeout(this, name, 0, std::move(f));
}
void Component::defer(const char *name, std::function<void()> &&f) { // NOLINT
App.scheduler.set_timeout(this, name, 0, std::move(f));
}
void Component::set_timeout(uint32_t timeout, std::function<void()> &&f) { // NOLINT
App.scheduler.set_timeout(this, static_cast<const char *>(nullptr), timeout, std::move(f));
}
void Component::set_interval(uint32_t interval, std::function<void()> &&f) { // NOLINT
App.scheduler.set_interval(this, static_cast<const char *>(nullptr), interval, std::move(f));
}
void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f,
float backoff_increase_factor) { // NOLINT
App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
}
bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
bool Component::is_ready() const {
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP ||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE ||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP;
}
bool Component::is_idle() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE; }
bool Component::can_proceed() { return true; }
bool Component::status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; }
bool Component::status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; }
void Component::status_set_warning(const char *message) {
// Don't spam the log. This risks missing different warning messages though.
if ((this->component_state_ & STATUS_LED_WARNING) != 0)
return;
this->component_state_ |= STATUS_LED_WARNING;
App.app_state_ |= STATUS_LED_WARNING;
ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()),
message ? message : LOG_STR_LITERAL("unspecified"));
void Component::status_set_warning(const char *message) {
// Don't spam the log. This risks missing different warning messages though.
if ((this->component_state_ & STATUS_LED_WARNING) != 0)
return;
this->component_state_ |= STATUS_LED_WARNING;
App.app_state_ |= STATUS_LED_WARNING;
ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()),
message ? message : LOG_STR_LITERAL("unspecified"));
}
void Component::status_set_warning(const LogString *message) {
// Don't spam the log. This risks missing different warning messages though.
if ((this->component_state_ & STATUS_LED_WARNING) != 0)
return;
this->component_state_ |= STATUS_LED_WARNING;
App.app_state_ |= STATUS_LED_WARNING;
ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()),
message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified"));
}
void Component::status_set_error() { this->status_set_error((const LogString *) nullptr); }
void Component::status_set_error(const char *message) {
if ((this->component_state_ & STATUS_LED_ERROR) != 0)
return;
this->component_state_ |= STATUS_LED_ERROR;
App.app_state_ |= STATUS_LED_ERROR;
ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()),
message ? message : LOG_STR_LITERAL("unspecified"));
if (message != nullptr) {
store_component_error_message(this, message, false);
}
void Component::status_set_warning(const LogString *message) {
// Don't spam the log. This risks missing different warning messages though.
if ((this->component_state_ & STATUS_LED_WARNING) != 0)
return;
this->component_state_ |= STATUS_LED_WARNING;
App.app_state_ |= STATUS_LED_WARNING;
ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()),
message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified"));
}
void Component::status_set_error(const LogString *message) {
if ((this->component_state_ & STATUS_LED_ERROR) != 0)
return;
this->component_state_ |= STATUS_LED_ERROR;
App.app_state_ |= STATUS_LED_ERROR;
ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()),
message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified"));
if (message != nullptr) {
// Store the LogString pointer directly (safe because LogString is always in flash/static memory)
store_component_error_message(this, LOG_STR_ARG(message), true);
}
void Component::status_set_error() { this->status_set_error((const LogString *) nullptr); }
void Component::status_set_error(const char *message) {
if ((this->component_state_ & STATUS_LED_ERROR) != 0)
return;
this->component_state_ |= STATUS_LED_ERROR;
App.app_state_ |= STATUS_LED_ERROR;
ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()),
message ? message : LOG_STR_LITERAL("unspecified"));
if (message != nullptr) {
store_component_error_message(this, message, false);
}
}
void Component::status_set_error(const LogString *message) {
if ((this->component_state_ & STATUS_LED_ERROR) != 0)
return;
this->component_state_ |= STATUS_LED_ERROR;
App.app_state_ |= STATUS_LED_ERROR;
ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()),
message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified"));
if (message != nullptr) {
// Store the LogString pointer directly (safe because LogString is always in flash/static memory)
store_component_error_message(this, LOG_STR_ARG(message), true);
}
}
void Component::status_clear_warning() {
if ((this->component_state_ & STATUS_LED_WARNING) == 0)
return;
this->component_state_ &= ~STATUS_LED_WARNING;
ESP_LOGW(TAG, "%s cleared Warning flag", LOG_STR_ARG(this->get_component_log_str()));
}
void Component::status_clear_error() {
if ((this->component_state_ & STATUS_LED_ERROR) == 0)
return;
this->component_state_ &= ~STATUS_LED_ERROR;
ESP_LOGE(TAG, "%s cleared Error flag", LOG_STR_ARG(this->get_component_log_str()));
}
void Component::status_momentary_warning(const char *name, uint32_t length) {
this->status_set_warning();
this->set_timeout(name, length, [this]() { this->status_clear_warning(); });
}
void Component::status_momentary_error(const char *name, uint32_t length) {
this->status_set_error();
this->set_timeout(name, length, [this]() { this->status_clear_error(); });
}
void Component::dump_config() {}
}
void Component::status_clear_warning() {
if ((this->component_state_ & STATUS_LED_WARNING) == 0)
return;
this->component_state_ &= ~STATUS_LED_WARNING;
ESP_LOGW(TAG, "%s cleared Warning flag", LOG_STR_ARG(this->get_component_log_str()));
}
void Component::status_clear_error() {
if ((this->component_state_ & STATUS_LED_ERROR) == 0)
return;
this->component_state_ &= ~STATUS_LED_ERROR;
ESP_LOGE(TAG, "%s cleared Error flag", LOG_STR_ARG(this->get_component_log_str()));
}
void Component::status_momentary_warning(const char *name, uint32_t length) {
this->status_set_warning();
this->set_timeout(name, length, [this]() { this->status_clear_warning(); });
}
void Component::status_momentary_error(const char *name, uint32_t length) {
this->status_set_error();
this->set_timeout(name, length, [this]() { this->status_clear_error(); });
}
void Component::dump_config() {}
// Function implementation of LOG_UPDATE_INTERVAL macro to reduce code size
void log_update_interval(const char *tag, PollingComponent *component) {
uint32_t update_interval = component->get_update_interval();
if (update_interval == SCHEDULER_DONT_RUN) {
ESP_LOGCONFIG(tag, " Update Interval: never");
} else if (update_interval < 100) {
ESP_LOGCONFIG(tag, " Update Interval: %.3fs", update_interval / 1000.0f);
} else {
ESP_LOGCONFIG(tag, " Update Interval: %.1fs", update_interval / 1000.0f);
}
// Function implementation of LOG_UPDATE_INTERVAL macro to reduce code size
void log_update_interval(const char *tag, PollingComponent *component) {
uint32_t update_interval = component->get_update_interval();
if (update_interval == SCHEDULER_DONT_RUN) {
ESP_LOGCONFIG(tag, " Update Interval: never");
} else if (update_interval < 100) {
ESP_LOGCONFIG(tag, " Update Interval: %.3fs", update_interval / 1000.0f);
} else {
ESP_LOGCONFIG(tag, " Update Interval: %.1fs", update_interval / 1000.0f);
}
float Component::get_actual_setup_priority() const {
// Check if there's an override in the global vector
if (setup_priority_overrides) {
// Linear search is fine for small n (typically < 5 overrides)
for (const auto &entry : *setup_priority_overrides) {
if (entry.component == this) {
return entry.priority;
}
}
}
return this->get_setup_priority();
}
void Component::set_setup_priority(float priority) {
// Lazy allocate the vector if needed
if (!setup_priority_overrides) {
setup_priority_overrides = std::make_unique<std::vector<ComponentPriorityOverride>>();
// Reserve some space to avoid reallocations (most configs have < 10 overrides)
setup_priority_overrides->reserve(10);
}
// Check if this component already has an override
for (auto &entry : *setup_priority_overrides) {
}
float Component::get_actual_setup_priority() const {
// Check if there's an override in the global vector
if (setup_priority_overrides) {
// Linear search is fine for small n (typically < 5 overrides)
for (const auto &entry : *setup_priority_overrides) {
if (entry.component == this) {
entry.priority = priority;
return;
return entry.priority;
}
}
// Add new override
setup_priority_overrides->emplace_back(ComponentPriorityOverride{this, priority});
}
return this->get_setup_priority();
}
void Component::set_setup_priority(float priority) {
// Lazy allocate the vector if needed
if (!setup_priority_overrides) {
setup_priority_overrides = std::make_unique<std::vector<ComponentPriorityOverride>>();
// Reserve some space to avoid reallocations (most configs have < 10 overrides)
setup_priority_overrides->reserve(10);
}
bool Component::has_overridden_loop() const {
// Check if this component already has an override
for (auto &entry : *setup_priority_overrides) {
if (entry.component == this) {
entry.priority = priority;
return;
}
}
// Add new override
setup_priority_overrides->emplace_back(ComponentPriorityOverride{this, priority});
}
bool Component::has_overridden_loop() const {
#if defined(USE_HOST) || defined(CLANG_TIDY)
bool loop_overridden = true;
bool call_loop_overridden = true;
bool loop_overridden = true;
bool call_loop_overridden = true;
#else
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpmf-conversions"
bool loop_overridden = (void *) (this->*(&Component::loop)) != (void *) (&Component::loop);
bool call_loop_overridden = (void *) (this->*(&Component::call_loop)) != (void *) (&Component::call_loop);
bool loop_overridden = (void *) (this->*(&Component::loop)) != (void *) (&Component::loop);
bool call_loop_overridden = (void *) (this->*(&Component::call_loop)) != (void *) (&Component::call_loop);
#pragma GCC diagnostic pop
#endif
return loop_overridden || call_loop_overridden;
}
return loop_overridden || call_loop_overridden;
}
PollingComponent::PollingComponent(uint32_t update_interval) : update_interval_(update_interval) {}
PollingComponent::PollingComponent(uint32_t update_interval) : update_interval_(update_interval) {}
void PollingComponent::call_setup() {
// init the poller before calling setup, allowing setup to cancel it if desired
this->start_poller();
// Let the polling component subclass setup their HW.
this->setup();
}
void PollingComponent::call_setup() {
// init the poller before calling setup, allowing setup to cancel it if desired
this->start_poller();
// Let the polling component subclass setup their HW.
this->setup();
}
void PollingComponent::start_poller() {
// Register interval.
this->set_interval("update", this->get_update_interval(), [this]() { this->update(); });
}
void PollingComponent::start_poller() {
// Register interval.
this->set_interval("update", this->get_update_interval(), [this]() { this->update(); });
}
void PollingComponent::stop_poller() {
// Clear the interval to suspend component
this->cancel_interval("update");
}
void PollingComponent::stop_poller() {
// Clear the interval to suspend component
this->cancel_interval("update");
}
uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; }
void PollingComponent::set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; }
void PollingComponent::set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
WarnIfComponentBlockingGuard::WarnIfComponentBlockingGuard(Component * component, uint32_t start_time)
: started_(start_time), component_(component) {}
uint32_t WarnIfComponentBlockingGuard::finish() {
uint32_t curr_time = millis();
WarnIfComponentBlockingGuard::WarnIfComponentBlockingGuard(Component *component, uint32_t start_time)
: started_(start_time), component_(component) {}
uint32_t WarnIfComponentBlockingGuard::finish() {
uint32_t curr_time = millis();
uint32_t blocking_time = curr_time - this->started_;
uint32_t blocking_time = curr_time - this->started_;
#ifdef USE_RUNTIME_STATS
// Record component runtime stats
if (global_runtime_stats != nullptr) {
global_runtime_stats->record_component_time(this->component_, blocking_time, curr_time);
}
// Record component runtime stats
if (global_runtime_stats != nullptr) {
global_runtime_stats->record_component_time(this->component_, blocking_time, curr_time);
}
#endif
bool should_warn;
if (this->component_ != nullptr) {
should_warn = this->component_->should_warn_of_blocking(blocking_time);
} else {
should_warn = blocking_time > WARN_IF_BLOCKING_OVER_MS;
}
if (should_warn) {
ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms)",
component_ == nullptr ? LOG_STR_LITERAL("<null>") : LOG_STR_ARG(component_->get_component_log_str()),
blocking_time);
ESP_LOGW(TAG, "Components should block for at most 30 ms");
}
return curr_time;
bool should_warn;
if (this->component_ != nullptr) {
should_warn = this->component_->should_warn_of_blocking(blocking_time);
} else {
should_warn = blocking_time > WARN_IF_BLOCKING_OVER_MS;
}
if (should_warn) {
ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms)",
component_ == nullptr ? LOG_STR_LITERAL("<null>") : LOG_STR_ARG(component_->get_component_log_str()),
blocking_time);
ESP_LOGW(TAG, "Components should block for at most 30 ms");
}
WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {}
return curr_time;
}
void clear_setup_priority_overrides() {
// Free the setup priority map completely
setup_priority_overrides.reset();
}
WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {}
void clear_setup_priority_overrides() {
// Free the setup priority map completely
setup_priority_overrides.reset();
}
} // namespace esphome

View File

@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==5.1.0
click==8.1.7
esphome-dashboard==20251013.0
aioesphomeapi==43.1.0
aioesphomeapi==43.2.1
zeroconf==0.148.0
puremagic==1.30
ruamel.yaml==0.18.16 # dashboard_import

View File

@@ -87,6 +87,7 @@ ISOLATED_COMPONENTS = {
"neopixelbus": "RMT type conflict with ESP32 Arduino/ESP-IDF headers (enum vs struct rmt_channel_t)",
"packages": "cannot merge packages",
"tinyusb": "Conflicts with usb_host component - cannot be used together",
"usb_cdc_acm": "Depends on tinyusb which conflicts with usb_host",
}

View File

@@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch
import pytest
from esphome.components.packages import CONFIG_SCHEMA, do_packages_pass
from esphome.components.packages import CONFIG_SCHEMA, do_packages_pass, merge_packages
from esphome.config import resolve_extend_remove
from esphome.config_helpers import Extend, Remove
import esphome.config_validation as cv
@@ -27,6 +27,7 @@ from esphome.const import (
CONF_REFRESH,
CONF_SENSOR,
CONF_SSID,
CONF_SUBSTITUTIONS,
CONF_UPDATE_INTERVAL,
CONF_URL,
CONF_VARS,
@@ -68,11 +69,12 @@ def fixture_basic_esphome():
def packages_pass(config):
"""Wrapper around packages_pass that also resolves Extend and Remove."""
config = do_packages_pass(config)
config = merge_packages(config)
resolve_extend_remove(config)
return config
def test_package_unused(basic_esphome, basic_wifi):
def test_package_unused(basic_esphome, basic_wifi) -> None:
"""
Ensures do_package_pass does not change a config if packages aren't used.
"""
@@ -82,7 +84,7 @@ def test_package_unused(basic_esphome, basic_wifi):
assert actual == config
def test_package_invalid_dict(basic_esphome, basic_wifi):
def test_package_invalid_dict(basic_esphome, basic_wifi) -> None:
"""
If a url: key is present, it's expected to be well-formed remote package spec. Ensure an error is raised if not.
Any other simple dict passed as a package will be merged as usual but may fail later validation.
@@ -107,7 +109,7 @@ def test_package_invalid_dict(basic_esphome, basic_wifi):
],
],
)
def test_package_shorthand(packages):
def test_package_shorthand(packages) -> None:
CONFIG_SCHEMA(packages)
@@ -133,12 +135,12 @@ def test_package_shorthand(packages):
[3],
],
)
def test_package_invalid(packages):
def test_package_invalid(packages) -> None:
with pytest.raises(cv.Invalid):
CONFIG_SCHEMA(packages)
def test_package_include(basic_wifi, basic_esphome):
def test_package_include(basic_wifi, basic_esphome) -> None:
"""
Tests the simple case where an independent config present in a package is added to the top-level config as is.
@@ -159,7 +161,7 @@ def test_single_package(
basic_esphome,
basic_wifi,
caplog: pytest.LogCaptureFixture,
):
) -> None:
"""
Tests the simple case where a single package is added to the top-level config as is.
In this test, the CONF_WIFI config is expected to be simply added to the top-level config.
@@ -179,7 +181,7 @@ def test_single_package(
assert "This method for including packages will go away in 2026.7.0" in caplog.text
def test_package_append(basic_wifi, basic_esphome):
def test_package_append(basic_wifi, basic_esphome) -> None:
"""
Tests the case where a key is present in both a package and top-level config.
@@ -204,7 +206,7 @@ def test_package_append(basic_wifi, basic_esphome):
assert actual == expected
def test_package_override(basic_wifi, basic_esphome):
def test_package_override(basic_wifi, basic_esphome) -> None:
"""
Ensures that the top-level configuration takes precedence over duplicate keys defined in a package.
@@ -228,7 +230,7 @@ def test_package_override(basic_wifi, basic_esphome):
assert actual == expected
def test_multiple_package_order():
def test_multiple_package_order() -> None:
"""
Ensures that mutiple packages are merged in order.
"""
@@ -257,7 +259,7 @@ def test_multiple_package_order():
assert actual == expected
def test_package_list_merge():
def test_package_list_merge() -> None:
"""
Ensures lists defined in both a package and the top-level config are merged correctly
"""
@@ -313,7 +315,7 @@ def test_package_list_merge():
assert actual == expected
def test_package_list_merge_by_id():
def test_package_list_merge_by_id() -> None:
"""
Ensures that components with matching IDs are merged correctly.
@@ -391,7 +393,7 @@ def test_package_list_merge_by_id():
assert actual == expected
def test_package_merge_by_id_with_list():
def test_package_merge_by_id_with_list() -> None:
"""
Ensures that components with matching IDs are merged correctly when their configuration contains lists.
@@ -430,7 +432,7 @@ def test_package_merge_by_id_with_list():
assert actual == expected
def test_package_merge_by_missing_id():
def test_package_merge_by_missing_id() -> None:
"""
Ensures that a validation error is thrown when trying to extend a missing ID.
"""
@@ -466,7 +468,7 @@ def test_package_merge_by_missing_id():
assert error_raised
def test_package_list_remove_by_id():
def test_package_list_remove_by_id() -> None:
"""
Ensures that components with matching IDs are removed correctly.
@@ -517,7 +519,7 @@ def test_package_list_remove_by_id():
assert actual == expected
def test_multiple_package_list_remove_by_id():
def test_multiple_package_list_remove_by_id() -> None:
"""
Ensures that components with matching IDs are removed correctly.
@@ -563,7 +565,7 @@ def test_multiple_package_list_remove_by_id():
assert actual == expected
def test_package_dict_remove_by_id(basic_wifi, basic_esphome):
def test_package_dict_remove_by_id(basic_wifi, basic_esphome) -> None:
"""
Ensures that components with missing IDs are removed from dict.
Ensures that the top-level configuration takes precedence over duplicate keys defined in a package.
@@ -584,7 +586,7 @@ def test_package_dict_remove_by_id(basic_wifi, basic_esphome):
assert actual == expected
def test_package_remove_by_missing_id():
def test_package_remove_by_missing_id() -> None:
"""
Ensures that components with missing IDs are not merged.
"""
@@ -632,7 +634,7 @@ def test_package_remove_by_missing_id():
@patch("esphome.git.clone_or_update")
def test_remote_packages_with_files_list(
mock_clone_or_update, mock_is_file, mock_load_yaml
):
) -> None:
"""
Ensures that packages are loaded as mixed list of dictionary and strings
"""
@@ -704,7 +706,7 @@ def test_remote_packages_with_files_list(
@patch("esphome.git.clone_or_update")
def test_remote_packages_with_files_and_vars(
mock_clone_or_update, mock_is_file, mock_load_yaml
):
) -> None:
"""
Ensures that packages are loaded as mixed list of dictionary and strings with vars
"""
@@ -793,3 +795,199 @@ def test_remote_packages_with_files_and_vars(
actual = packages_pass(config)
assert actual == expected
def test_packages_merge_substitutions() -> None:
"""
Tests that substitutions from packages in a complex package hierarchy
are extracted and merged into the top-level config.
"""
config = {
CONF_SUBSTITUTIONS: {
"a": 1,
"b": 2,
"c": 3,
},
CONF_PACKAGES: {
"package1": {
"logger": {
"level": "DEBUG",
},
CONF_PACKAGES: [
{
CONF_SUBSTITUTIONS: {
"a": 10,
"e": 5,
},
"sensor": [
{"platform": "template", "id": "sensor1"},
],
},
],
"sensor": [
{"platform": "template", "id": "sensor2"},
],
},
"package2": {
"logger": {
"level": "VERBOSE",
},
},
"package3": {
CONF_PACKAGES: [
{
CONF_PACKAGES: [
{
CONF_SUBSTITUTIONS: {
"b": 20,
"d": 4,
},
"sensor": [
{"platform": "template", "id": "sensor3"},
],
},
],
CONF_SUBSTITUTIONS: {
"b": 20,
"d": 6,
},
"sensor": [
{"platform": "template", "id": "sensor4"},
],
},
],
},
},
}
expected = {
CONF_SUBSTITUTIONS: {"a": 1, "e": 5, "b": 2, "d": 6, "c": 3},
CONF_PACKAGES: {
"package1": {
"logger": {
"level": "DEBUG",
},
CONF_PACKAGES: [
{
"sensor": [
{"platform": "template", "id": "sensor1"},
],
},
],
"sensor": [
{"platform": "template", "id": "sensor2"},
],
},
"package2": {
"logger": {
"level": "VERBOSE",
},
},
"package3": {
CONF_PACKAGES: [
{
CONF_PACKAGES: [
{
"sensor": [
{"platform": "template", "id": "sensor3"},
],
},
],
"sensor": [
{"platform": "template", "id": "sensor4"},
],
},
],
},
},
}
actual = do_packages_pass(config)
assert actual == expected
def test_package_merge() -> None:
"""
Tests that all packages are merged into the top-level config.
"""
config = {
CONF_SUBSTITUTIONS: {"a": 1, "e": 5, "b": 2, "d": 6, "c": 3},
CONF_PACKAGES: {
"package1": {
"logger": {
"level": "DEBUG",
},
CONF_PACKAGES: [
{
"sensor": [
{"platform": "template", "id": "sensor1"},
],
},
],
"sensor": [
{"platform": "template", "id": "sensor2"},
],
},
"package2": {
"logger": {
"level": "VERBOSE",
},
},
"package3": {
CONF_PACKAGES: [
{
CONF_PACKAGES: [
{
"sensor": [
{"platform": "template", "id": "sensor3"},
],
},
],
"sensor": [
{"platform": "template", "id": "sensor4"},
],
},
],
},
},
}
expected = {
"sensor": [
{"platform": "template", "id": "sensor1"},
{"platform": "template", "id": "sensor2"},
{"platform": "template", "id": "sensor3"},
{"platform": "template", "id": "sensor4"},
],
"logger": {"level": "VERBOSE"},
CONF_SUBSTITUTIONS: {"a": 1, "e": 5, "b": 2, "d": 6, "c": 3},
}
actual = merge_packages(config)
assert actual == expected
@pytest.mark.parametrize(
"invalid_package",
[
6,
"some string",
["some string"],
None,
True,
{"some_component": 8},
{3: 2},
{"some_component": r"${unevaluated expression}"},
],
)
def test_package_merge_invalid(invalid_package) -> None:
"""
Tests that trying to merge an invalid package raises an error.
"""
config = {
CONF_PACKAGES: {
"some_package": invalid_package,
},
}
with pytest.raises(cv.Invalid):
merge_packages(config)

View File

@@ -2,6 +2,6 @@ substitutions:
enable_rx_pin: GPIO13
packages:
uart: !include ../../test_build_components/common/uart_1200_none_2stopbits/esp32-idf.yaml
uart_1200_none_2stopbits: !include ../../test_build_components/common/uart_1200_none_2stopbits/esp32-idf.yaml
<<: !include common.yaml

View File

@@ -2,6 +2,6 @@ substitutions:
enable_rx_pin: GPIO15
packages:
uart: !include ../../test_build_components/common/uart_1200_none_2stopbits/esp8266-ard.yaml
uart_1200_none_2stopbits: !include ../../test_build_components/common/uart_1200_none_2stopbits/esp8266-ard.yaml
<<: !include common.yaml

View File

@@ -2,6 +2,6 @@ substitutions:
enable_rx_pin: GPIO3
packages:
uart: !include ../../test_build_components/common/uart_1200_none_2stopbits/rp2040-ard.yaml
uart_1200_none_2stopbits: !include ../../test_build_components/common/uart_1200_none_2stopbits/rp2040-ard.yaml
<<: !include common.yaml

View File

@@ -52,6 +52,7 @@ void CustomAPIDeviceComponent::on_service_with_arrays(std::vector<bool> bool_arr
}
}
// NOLINTNEXTLINE(performance-unnecessary-value-param)
void CustomAPIDeviceComponent::on_ha_state_changed(std::string entity_id, std::string state) {
ESP_LOGI(TAG, "Home Assistant state changed for %s: %s", entity_id.c_str(), state.c_str());
ESP_LOGI(TAG, "This subscription uses std::string API for backward compatibility");

View File

@@ -24,6 +24,7 @@ class CustomAPIDeviceComponent : public Component, public CustomAPIDevice {
std::vector<float> float_array, std::vector<std::string> string_array);
// Test Home Assistant state subscription with std::string API
// NOLINTNEXTLINE(performance-unnecessary-value-param)
void on_ha_state_changed(std::string entity_id, std::string state);
};

View File

@@ -0,0 +1,43 @@
fancy_component: &id001
- id: component9
value: 9
some_component:
- id: component1
value: 1
- id: component2
value: 2
- id: component3
value: 3
- id: component4
value: 4
- id: component5
value: 79
power: 200
- id: component6
value: 6
- id: component7
value: 7
switch: &id002
- platform: gpio
id: switch1
pin: 12
- platform: gpio
id: switch2
pin: 13
display:
- platform: ili9xxx
dimensions:
width: 100
height: 480
substitutions:
extended_component: component5
package_options:
alternative_package:
alternative_component:
- id: component8
value: 8
fancy_package:
fancy_component: *id001
pin: 12
some_switches: *id002
package_selection: fancy_package

View File

@@ -0,0 +1,61 @@
substitutions:
package_options:
alternative_package:
alternative_component:
- id: component8
value: 8
fancy_package:
fancy_component:
- id: component9
value: 9
pin: 12
some_switches:
- platform: gpio
id: switch1
pin: ${pin}
- platform: gpio
id: switch2
pin: ${pin+1}
package_selection: fancy_package
packages:
- ${ package_options[package_selection] }
- some_component:
- id: component1
value: 1
- some_component:
- id: component2
value: 2
- switch: ${ some_switches }
- packages:
package_with_defaults: !include
file: display.yaml
vars:
native_width: 100
high_dpi: false
my_package:
packages:
- packages:
special_package:
substitutions:
extended_component: component5
some_component:
- id: component3
value: 3
some_component:
- id: component4
value: 4
- id: !extend ${ extended_component }
power: 200
value: 79
some_component:
- id: component5
value: 5
some_component:
- id: component6
value: 6
- id: component7
value: 7

View File

@@ -8,7 +8,7 @@ import pytest
from esphome import config as config_module, yaml_util
from esphome.components import substitutions
from esphome.components.packages import do_packages_pass
from esphome.components.packages import do_packages_pass, merge_packages
from esphome.config import resolve_extend_remove
from esphome.config_helpers import merge_config
from esphome.const import CONF_SUBSTITUTIONS
@@ -74,6 +74,8 @@ def verify_database(value: Any, path: str = "") -> str | None:
return None
if isinstance(value, dict):
for k, v in value.items():
if path == "" and k == CONF_SUBSTITUTIONS:
return None # ignore substitutions key at top level since it is merged.
key_result = verify_database(k, f"{path}/{k}")
if key_result is not None:
return key_result
@@ -144,6 +146,8 @@ def test_substitutions_fixtures(
substitutions.do_substitution_pass(config, command_line_substitutions)
config = merge_packages(config)
resolve_extend_remove(config)
verify_database_result = verify_database(config)
if verify_database_result is not None: