mirror of
https://github.com/esphome/esphome.git
synced 2026-01-21 02:19:10 -07:00
Compare commits
2 Commits
2025.12.0b
...
broken_tar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8622bf1de0 | ||
|
|
0420c00ec3 |
2
Doxyfile
2
Doxyfile
@@ -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.0b2
|
||||
PROJECT_NUMBER = 2025.12.0-dev
|
||||
|
||||
# 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
|
||||
|
||||
@@ -579,7 +579,7 @@ message LightCommandRequest {
|
||||
bool has_flash_length = 16;
|
||||
uint32 flash_length = 17;
|
||||
bool has_effect = 18;
|
||||
string effect = 19 [(pointer_to_buffer) = true];
|
||||
string effect = 19;
|
||||
uint32 device_id = 28 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
|
||||
|
||||
@@ -533,7 +533,7 @@ void APIConnection::light_command(const LightCommandRequest &msg) {
|
||||
if (msg.has_flash_length)
|
||||
call.set_flash_length(msg.flash_length);
|
||||
if (msg.has_effect)
|
||||
call.set_effect(reinterpret_cast<const char *>(msg.effect), msg.effect_len);
|
||||
call.set_effect(msg.effect);
|
||||
call.perform();
|
||||
}
|
||||
#endif
|
||||
@@ -1669,7 +1669,7 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Failed to clear encryption key");
|
||||
}
|
||||
} else if (base64_decode(msg.key, psk.data(), psk.size()) != psk.size()) {
|
||||
} else if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) {
|
||||
ESP_LOGW(TAG, "Invalid encryption key length");
|
||||
} else if (!this->parent_->save_noise_psk(psk, true)) {
|
||||
ESP_LOGW(TAG, "Failed to save encryption key");
|
||||
|
||||
@@ -611,12 +611,9 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
}
|
||||
bool LightCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 19: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->effect = value.data();
|
||||
this->effect_len = value.size();
|
||||
case 19:
|
||||
this->effect = value.as_string();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -840,7 +840,7 @@ class LightStateResponse final : public StateResponseProtoMessage {
|
||||
class LightCommandRequest final : public CommandProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 32;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 122;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 112;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "light_command_request"; }
|
||||
#endif
|
||||
@@ -869,8 +869,7 @@ class LightCommandRequest final : public CommandProtoMessage {
|
||||
bool has_flash_length{false};
|
||||
uint32_t flash_length{0};
|
||||
bool has_effect{false};
|
||||
const uint8_t *effect{nullptr};
|
||||
uint16_t effect_len{0};
|
||||
std::string effect{};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
@@ -999,9 +999,7 @@ void LightCommandRequest::dump_to(std::string &out) const {
|
||||
dump_field(out, "has_flash_length", this->has_flash_length);
|
||||
dump_field(out, "flash_length", this->flash_length);
|
||||
dump_field(out, "has_effect", this->has_effect);
|
||||
out.append(" effect: ");
|
||||
out.append(format_hex_pretty(this->effect, this->effect_len));
|
||||
out.append("\n");
|
||||
dump_field(out, "effect", this->effect);
|
||||
#ifdef USE_DEVICES
|
||||
dump_field(out, "device_id", this->device_id);
|
||||
#endif
|
||||
|
||||
@@ -41,7 +41,6 @@ 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_(
|
||||
@@ -72,9 +71,6 @@ 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(
|
||||
@@ -94,9 +90,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): cv.All(
|
||||
update_interval, cv.Range(min=minimum_update_interval)
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_UPDATE_INTERVAL, default=cv.UNDEFINED
|
||||
): update_interval,
|
||||
cv.Optional(CONF_TRANSFORM): cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_MIRROR_X): cv.boolean,
|
||||
@@ -157,8 +153,9 @@ 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
|
||||
elif CONF_UPDATE_INTERVAL not in config:
|
||||
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
|
||||
config[CONF_UPDATE_INTERVAL] = core.TimePeriod(
|
||||
seconds=60
|
||||
).total_milliseconds
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -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) {
|
||||
bool EPaperBase::rotate_coordinates_(int &x, int &y) const {
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
return false;
|
||||
if (this->transform_ & SWAP_XY)
|
||||
@@ -297,10 +297,6 @@ bool EPaperBase::rotate_coordinates_(int &x, int &y) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -323,6 +319,10 @@ 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() {
|
||||
|
||||
@@ -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);
|
||||
bool rotate_coordinates_(int &x, int &y) const;
|
||||
|
||||
/**
|
||||
* Methods that must be implemented by concrete classes to control the display
|
||||
|
||||
@@ -4,8 +4,8 @@ from . import EpaperModel
|
||||
|
||||
|
||||
class SpectraE6(EpaperModel):
|
||||
def __init__(self, name, class_name="EPaperSpectraE6", **defaults):
|
||||
super().__init__(name, class_name, **defaults)
|
||||
def __init__(self, name, class_name="EPaperSpectraE6", **kwargs):
|
||||
super().__init__(name, class_name, **kwargs)
|
||||
|
||||
# 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", minimum_update_interval="30s")
|
||||
spectra_e6 = SpectraE6("spectra-e6")
|
||||
|
||||
spectra_e6_7p3 = spectra_e6.extend(
|
||||
"7.3in-Spectra-E6",
|
||||
|
||||
@@ -36,10 +36,6 @@ void HttpRequestUpdate::setup() {
|
||||
}
|
||||
|
||||
void HttpRequestUpdate::update() {
|
||||
if (!network::is_connected()) {
|
||||
ESP_LOGD(TAG, "Network not connected, skipping update check");
|
||||
return;
|
||||
}
|
||||
#ifdef USE_ESP32
|
||||
xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_);
|
||||
#else
|
||||
|
||||
@@ -504,8 +504,8 @@ color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() {
|
||||
#undef KEY
|
||||
}
|
||||
|
||||
LightCall &LightCall::set_effect(const char *effect, size_t len) {
|
||||
if (len == 4 && strncasecmp(effect, "none", 4) == 0) {
|
||||
LightCall &LightCall::set_effect(const std::string &effect) {
|
||||
if (strcasecmp(effect.c_str(), "none") == 0) {
|
||||
this->set_effect(0);
|
||||
return *this;
|
||||
}
|
||||
@@ -513,16 +513,15 @@ LightCall &LightCall::set_effect(const char *effect, size_t len) {
|
||||
bool found = false;
|
||||
for (uint32_t i = 0; i < this->parent_->effects_.size(); i++) {
|
||||
LightEffect *e = this->parent_->effects_[i];
|
||||
const char *name = e->get_name();
|
||||
|
||||
if (strncasecmp(effect, name, len) == 0 && name[len] == '\0') {
|
||||
if (strcasecmp(effect.c_str(), e->get_name()) == 0) {
|
||||
this->set_effect(i + 1);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
ESP_LOGW(TAG, "'%s': no such effect '%.*s'", this->parent_->get_name().c_str(), (int) len, effect);
|
||||
ESP_LOGW(TAG, "'%s': no such effect '%s'", this->parent_->get_name().c_str(), effect.c_str());
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -129,9 +129,7 @@ class LightCall {
|
||||
/// Set the effect of the light by its name.
|
||||
LightCall &set_effect(optional<std::string> effect);
|
||||
/// Set the effect of the light by its name.
|
||||
LightCall &set_effect(const std::string &effect) { return this->set_effect(effect.data(), effect.size()); }
|
||||
/// Set the effect of the light by its name and length (zero-copy from API).
|
||||
LightCall &set_effect(const char *effect, size_t len);
|
||||
LightCall &set_effect(const std::string &effect);
|
||||
/// Set the effect of the light by its internal index number (only for internal use).
|
||||
LightCall &set_effect(uint32_t effect_number);
|
||||
LightCall &set_effect(optional<uint32_t> effect_number);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -24,7 +24,7 @@ from esphome.components.mipi import (
|
||||
CONF_VSYNC_BACK_PORCH,
|
||||
CONF_VSYNC_FRONT_PORCH,
|
||||
CONF_VSYNC_PULSE_WIDTH,
|
||||
MODE_RGB,
|
||||
MODE_BGR,
|
||||
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_RGB): cv.enum(COLOR_ORDERS, upper=True),
|
||||
model.option(CONF_COLOR_ORDER, MODE_BGR): 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,9 +280,14 @@ 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]
|
||||
dpins.extend(blue_pins)
|
||||
dpins.extend(green_pins)
|
||||
dpins.extend(red_pins)
|
||||
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)
|
||||
# swap bytes to match big-endian format
|
||||
dpins = dpins[8:16] + dpins[0:8]
|
||||
else:
|
||||
|
||||
@@ -371,10 +371,17 @@ 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());
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mipi_rgb
|
||||
|
||||
@@ -7,6 +7,7 @@ ST7701S(
|
||||
"T-PANEL-S3",
|
||||
width=480,
|
||||
height=480,
|
||||
color_order="BGR",
|
||||
invert_colors=False,
|
||||
swap_xy=UNDEFINED,
|
||||
spi_mode="MODE3",
|
||||
@@ -55,6 +56,7 @@ t_rgb = ST7701S(
|
||||
"T-RGB-2.1",
|
||||
width=480,
|
||||
height=480,
|
||||
color_order="BGR",
|
||||
pixel_mode="18bit",
|
||||
invert_colors=False,
|
||||
swap_xy=UNDEFINED,
|
||||
|
||||
@@ -82,6 +82,7 @@ st7701s.extend(
|
||||
"MAKERFABS-4",
|
||||
width=480,
|
||||
height=480,
|
||||
color_order="RGB",
|
||||
invert_colors=True,
|
||||
pixel_mode="18bit",
|
||||
cs_pin=1,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from esphome.components.mipi import DriverChip, delay
|
||||
from esphome.components.mipi import DriverChip
|
||||
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,9 +55,10 @@ 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,
|
||||
@@ -75,72 +76,3 @@ 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),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
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
|
||||
@@ -19,7 +15,6 @@ from esphome.const import (
|
||||
CONF_PATH,
|
||||
CONF_REF,
|
||||
CONF_REFRESH,
|
||||
CONF_SUBSTITUTIONS,
|
||||
CONF_URL,
|
||||
CONF_USERNAME,
|
||||
CONF_VARS,
|
||||
@@ -32,43 +27,32 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = CONF_PACKAGES
|
||||
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
||||
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("Invalid component content in package definition")
|
||||
return package_config
|
||||
|
||||
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
|
||||
raise cv.Invalid("Package contents must be a dict")
|
||||
|
||||
|
||||
def expand_file_to_files(config: dict):
|
||||
@@ -158,10 +142,7 @@ 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
|
||||
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}}
|
||||
valid_package_contents, # 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.
|
||||
)
|
||||
|
||||
@@ -254,84 +235,32 @@ def _process_remote_package(config: dict, skip_update: bool = False) -> dict:
|
||||
return {"packages": packages}
|
||||
|
||||
|
||||
def _walk_packages(
|
||||
config: dict, callback: Callable[[dict], dict], validate_deprecated: bool = True
|
||||
) -> dict:
|
||||
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):
|
||||
if CONF_PACKAGES not in config:
|
||||
return config
|
||||
packages = config[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):
|
||||
packages = CONFIG_SCHEMA(packages)
|
||||
if isinstance(packages, dict):
|
||||
for package_name, package_config in reversed(packages.items()):
|
||||
with cv.prepend_path(package_name):
|
||||
package_config = callback(package_config)
|
||||
packages[package_name] = _walk_packages(package_config, callback)
|
||||
config = _process_package(package_config, config, skip_update)
|
||||
elif isinstance(packages, list):
|
||||
for idx in reversed(range(len(packages))):
|
||||
with cv.prepend_path(idx):
|
||||
package_config = callback(packages[idx])
|
||||
packages[idx] = _walk_packages(package_config, callback)
|
||||
for package_config in reversed(packages):
|
||||
config = _process_package(package_config, config, skip_update)
|
||||
else:
|
||||
raise cv.Invalid(
|
||||
f"Packages must be a key to value mapping or list, got {type(packages)} instead"
|
||||
)
|
||||
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]
|
||||
|
||||
del config[CONF_PACKAGES]
|
||||
return config
|
||||
|
||||
@@ -14,36 +14,13 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
#include <coredecls.h> // For esp_schedule()
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace socket {
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// Flag to signal socket activity - checked by socket_delay() to exit early
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static volatile bool s_socket_woke = false;
|
||||
|
||||
void socket_delay(uint32_t ms) {
|
||||
// Use esp_delay with a callback that checks if socket data arrived.
|
||||
// This allows the delay to exit early when socket_wake() is called by
|
||||
// lwip recv_fn/accept_fn callbacks, reducing socket latency.
|
||||
s_socket_woke = false;
|
||||
esp_delay(ms, []() { return !s_socket_woke; });
|
||||
}
|
||||
|
||||
void socket_wake() {
|
||||
s_socket_woke = true;
|
||||
esp_schedule();
|
||||
}
|
||||
#endif
|
||||
|
||||
static const char *const TAG = "socket.lwip";
|
||||
|
||||
// set to 1 to enable verbose lwip logging
|
||||
#if 0 // NOLINT(readability-avoid-unconditional-preprocessor-if)
|
||||
#if 0
|
||||
#define LWIP_LOG(msg, ...) ESP_LOGVV(TAG, "socket %p: " msg, this, ##__VA_ARGS__)
|
||||
#else
|
||||
#define LWIP_LOG(msg, ...)
|
||||
@@ -346,10 +323,9 @@ class LWIPRawImpl : public Socket {
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
ssize_t err = read(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
|
||||
if (err == -1) {
|
||||
if (ret != 0) {
|
||||
if (ret != 0)
|
||||
// if we already read some don't return an error
|
||||
break;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
ret += err;
|
||||
@@ -417,10 +393,9 @@ class LWIPRawImpl : public Socket {
|
||||
ssize_t written = internal_write(buf, len);
|
||||
if (written == -1)
|
||||
return -1;
|
||||
if (written == 0) {
|
||||
if (written == 0)
|
||||
// no need to output if nothing written
|
||||
return 0;
|
||||
}
|
||||
if (nodelay_) {
|
||||
int err = internal_output();
|
||||
if (err == -1)
|
||||
@@ -433,20 +408,18 @@ class LWIPRawImpl : public Socket {
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
ssize_t err = internal_write(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
|
||||
if (err == -1) {
|
||||
if (written != 0) {
|
||||
if (written != 0)
|
||||
// if we already read some don't return an error
|
||||
break;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
written += err;
|
||||
if ((size_t) err != iov[i].iov_len)
|
||||
break;
|
||||
}
|
||||
if (written == 0) {
|
||||
if (written == 0)
|
||||
// no need to output if nothing written
|
||||
return 0;
|
||||
}
|
||||
if (nodelay_) {
|
||||
int err = internal_output();
|
||||
if (err == -1)
|
||||
@@ -500,10 +473,6 @@ class LWIPRawImpl : public Socket {
|
||||
} else {
|
||||
pbuf_cat(rx_buf_, pb);
|
||||
}
|
||||
#ifdef USE_ESP8266
|
||||
// Wake the main loop immediately so it can process the received data.
|
||||
socket_wake();
|
||||
#endif
|
||||
return ERR_OK;
|
||||
}
|
||||
|
||||
@@ -643,7 +612,7 @@ class LWIPRawListenImpl : public LWIPRawImpl {
|
||||
}
|
||||
|
||||
private:
|
||||
err_t accept_fn_(struct tcp_pcb *newpcb, err_t err) {
|
||||
err_t accept_fn(struct tcp_pcb *newpcb, err_t err) {
|
||||
LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err);
|
||||
if (err != ERR_OK || newpcb == nullptr) {
|
||||
// "An error code if there has been an error accepting. Only return ERR_ABRT if you have
|
||||
@@ -664,16 +633,12 @@ class LWIPRawListenImpl : public LWIPRawImpl {
|
||||
sock->init();
|
||||
accepted_sockets_[accepted_socket_count_++] = std::move(sock);
|
||||
LWIP_LOG("Accepted connection, queue size: %d", accepted_socket_count_);
|
||||
#ifdef USE_ESP8266
|
||||
// Wake the main loop immediately so it can accept the new connection.
|
||||
socket_wake();
|
||||
#endif
|
||||
return ERR_OK;
|
||||
}
|
||||
|
||||
static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) {
|
||||
LWIPRawListenImpl *arg_this = reinterpret_cast<LWIPRawListenImpl *>(arg);
|
||||
return arg_this->accept_fn_(newpcb, err);
|
||||
return arg_this->accept_fn(newpcb, err);
|
||||
}
|
||||
|
||||
// Accept queue - holds incoming connections briefly until the event loop calls accept()
|
||||
|
||||
@@ -82,15 +82,6 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::stri
|
||||
/// Set a sockaddr to the any address and specified port for the IP version used by socket_ip().
|
||||
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port);
|
||||
|
||||
#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP)
|
||||
/// Delay that can be woken early by socket activity.
|
||||
/// On ESP8266, lwip callbacks set a flag and call esp_schedule() to wake the delay.
|
||||
void socket_delay(uint32_t ms);
|
||||
|
||||
/// Called by lwip callbacks to signal socket activity and wake delay.
|
||||
void socket_wake();
|
||||
#endif
|
||||
|
||||
} // namespace socket
|
||||
} // namespace esphome
|
||||
#endif
|
||||
|
||||
@@ -1012,20 +1012,14 @@ def validate_config(
|
||||
|
||||
CORE.raw_config = config
|
||||
|
||||
# 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
|
||||
# 1.1. 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.3. Load external_components
|
||||
# 1.2. Load external_components
|
||||
if CONF_EXTERNAL_COMPONENTS in config:
|
||||
from esphome.components.external_components import do_external_components_pass
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2025.12.0b2"
|
||||
__version__ = "2025.12.0-dev"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -12,10 +12,6 @@
|
||||
#include "esphome/components/status_led/status_led.h"
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP)
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
#include <cerrno>
|
||||
|
||||
@@ -631,9 +627,6 @@ void Application::yield_with_select_(uint32_t delay_ms) {
|
||||
// No sockets registered, use regular delay
|
||||
delay(delay_ms);
|
||||
}
|
||||
#elif defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP)
|
||||
// No select support but can wake on socket activity via esp_schedule()
|
||||
socket::socket_delay(delay_ms);
|
||||
#else
|
||||
// No select support, use regular delay
|
||||
delay(delay_ms);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
THIS IS A SYNTAX ERROR !
|
||||
#include <cinttypes>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
@@ -13,494 +14,498 @@
|
||||
#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
|
||||
// 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
|
||||
// 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
|
||||
// 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>>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
// Add new error message
|
||||
component_error_messages->emplace_back(ComponentErrorMessage{component, message, is_flash_ptr});
|
||||
}
|
||||
// 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;
|
||||
} // namespace
|
||||
|
||||
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;
|
||||
|
||||
} // 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
|
||||
|
||||
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)
|
||||
|
||||
float Component::get_loop_priority() const { return 0.0f; }
|
||||
|
||||
float Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void Component::setup() {}
|
||||
|
||||
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 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 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 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 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 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 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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"));
|
||||
}
|
||||
}
|
||||
// Add new error message
|
||||
component_error_messages->emplace_back(ComponentErrorMessage{component, message, is_flash_ptr});
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace setup_priority {
|
||||
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();
|
||||
#endif
|
||||
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);
|
||||
#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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
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::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; }
|
||||
|
||||
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;
|
||||
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_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() {}
|
||||
|
||||
} // 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
|
||||
|
||||
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)
|
||||
|
||||
float Component::get_loop_priority() const { return 0.0f; }
|
||||
|
||||
float Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void Component::setup() {}
|
||||
|
||||
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 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 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 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 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 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 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) {
|
||||
// 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) {
|
||||
error_msg = entry.message;
|
||||
is_flash_ptr = entry.is_flash_ptr;
|
||||
break;
|
||||
return entry.priority;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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"));
|
||||
return this->get_setup_priority();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
#endif
|
||||
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);
|
||||
#endif
|
||||
break;
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
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::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; }
|
||||
|
||||
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_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() {}
|
||||
|
||||
// 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) {
|
||||
// Check if this component already has an override
|
||||
for (auto &entry : *setup_priority_overrides) {
|
||||
if (entry.component == this) {
|
||||
return entry.priority;
|
||||
entry.priority = priority;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
// Add new override
|
||||
setup_priority_overrides->emplace_back(ComponentPriorityOverride{this, priority});
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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");
|
||||
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;
|
||||
}
|
||||
|
||||
return curr_time;
|
||||
}
|
||||
WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {}
|
||||
|
||||
WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {}
|
||||
|
||||
void clear_setup_priority_overrides() {
|
||||
// Free the setup priority map completely
|
||||
setup_priority_overrides.reset();
|
||||
}
|
||||
void clear_setup_priority_overrides() {
|
||||
// Free the setup priority map completely
|
||||
setup_priority_overrides.reset();
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -480,13 +480,22 @@ std::string base64_encode(const uint8_t *buf, size_t buf_len) {
|
||||
}
|
||||
|
||||
size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf_len) {
|
||||
std::vector<uint8_t> decoded = base64_decode(encoded_string);
|
||||
if (decoded.size() > buf_len) {
|
||||
ESP_LOGW(TAG, "Base64 decode: buffer too small, truncating");
|
||||
decoded.resize(buf_len);
|
||||
}
|
||||
memcpy(buf, decoded.data(), decoded.size());
|
||||
return decoded.size();
|
||||
}
|
||||
|
||||
std::vector<uint8_t> base64_decode(const std::string &encoded_string) {
|
||||
int in_len = encoded_string.size();
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
int in = 0;
|
||||
size_t out = 0;
|
||||
uint8_t char_array_4[4], char_array_3[3];
|
||||
bool truncated = false;
|
||||
std::vector<uint8_t> ret;
|
||||
|
||||
// SAFETY: The loop condition checks is_base64() before processing each character.
|
||||
// This ensures base64_find_char() is only called on valid base64 characters,
|
||||
@@ -502,13 +511,8 @@ size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf
|
||||
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
|
||||
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
|
||||
|
||||
for (i = 0; i < 3; i++) {
|
||||
if (out < buf_len) {
|
||||
buf[out++] = char_array_3[i];
|
||||
} else {
|
||||
truncated = true;
|
||||
}
|
||||
}
|
||||
for (i = 0; (i < 3); i++)
|
||||
ret.push_back(char_array_3[i]);
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
@@ -524,28 +528,10 @@ size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf
|
||||
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
|
||||
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
|
||||
|
||||
for (j = 0; j < i - 1; j++) {
|
||||
if (out < buf_len) {
|
||||
buf[out++] = char_array_3[j];
|
||||
} else {
|
||||
truncated = true;
|
||||
}
|
||||
}
|
||||
for (j = 0; (j < i - 1); j++)
|
||||
ret.push_back(char_array_3[j]);
|
||||
}
|
||||
|
||||
if (truncated) {
|
||||
ESP_LOGW(TAG, "Base64 decode: buffer too small, truncating");
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> base64_decode(const std::string &encoded_string) {
|
||||
// Calculate maximum decoded size: every 4 base64 chars = 3 bytes
|
||||
size_t max_len = ((encoded_string.size() + 3) / 4) * 3;
|
||||
std::vector<uint8_t> ret(max_len);
|
||||
size_t actual_len = base64_decode(encoded_string, ret.data(), max_len);
|
||||
ret.resize(actual_len);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.2.1
|
||||
aioesphomeapi==43.1.0
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.18.16 # dashboard_import
|
||||
|
||||
@@ -87,7 +87,6 @@ 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",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.packages import CONFIG_SCHEMA, do_packages_pass, merge_packages
|
||||
from esphome.components.packages import CONFIG_SCHEMA, do_packages_pass
|
||||
from esphome.config import resolve_extend_remove
|
||||
from esphome.config_helpers import Extend, Remove
|
||||
import esphome.config_validation as cv
|
||||
@@ -27,7 +27,6 @@ from esphome.const import (
|
||||
CONF_REFRESH,
|
||||
CONF_SENSOR,
|
||||
CONF_SSID,
|
||||
CONF_SUBSTITUTIONS,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
CONF_URL,
|
||||
CONF_VARS,
|
||||
@@ -69,12 +68,11 @@ 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) -> None:
|
||||
def test_package_unused(basic_esphome, basic_wifi):
|
||||
"""
|
||||
Ensures do_package_pass does not change a config if packages aren't used.
|
||||
"""
|
||||
@@ -84,7 +82,7 @@ def test_package_unused(basic_esphome, basic_wifi) -> None:
|
||||
assert actual == config
|
||||
|
||||
|
||||
def test_package_invalid_dict(basic_esphome, basic_wifi) -> None:
|
||||
def test_package_invalid_dict(basic_esphome, basic_wifi):
|
||||
"""
|
||||
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.
|
||||
@@ -109,7 +107,7 @@ def test_package_invalid_dict(basic_esphome, basic_wifi) -> None:
|
||||
],
|
||||
],
|
||||
)
|
||||
def test_package_shorthand(packages) -> None:
|
||||
def test_package_shorthand(packages):
|
||||
CONFIG_SCHEMA(packages)
|
||||
|
||||
|
||||
@@ -135,12 +133,12 @@ def test_package_shorthand(packages) -> None:
|
||||
[3],
|
||||
],
|
||||
)
|
||||
def test_package_invalid(packages) -> None:
|
||||
def test_package_invalid(packages):
|
||||
with pytest.raises(cv.Invalid):
|
||||
CONFIG_SCHEMA(packages)
|
||||
|
||||
|
||||
def test_package_include(basic_wifi, basic_esphome) -> None:
|
||||
def test_package_include(basic_wifi, basic_esphome):
|
||||
"""
|
||||
Tests the simple case where an independent config present in a package is added to the top-level config as is.
|
||||
|
||||
@@ -161,7 +159,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.
|
||||
@@ -181,7 +179,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) -> None:
|
||||
def test_package_append(basic_wifi, basic_esphome):
|
||||
"""
|
||||
Tests the case where a key is present in both a package and top-level config.
|
||||
|
||||
@@ -206,7 +204,7 @@ def test_package_append(basic_wifi, basic_esphome) -> None:
|
||||
assert actual == expected
|
||||
|
||||
|
||||
def test_package_override(basic_wifi, basic_esphome) -> None:
|
||||
def test_package_override(basic_wifi, basic_esphome):
|
||||
"""
|
||||
Ensures that the top-level configuration takes precedence over duplicate keys defined in a package.
|
||||
|
||||
@@ -230,7 +228,7 @@ def test_package_override(basic_wifi, basic_esphome) -> None:
|
||||
assert actual == expected
|
||||
|
||||
|
||||
def test_multiple_package_order() -> None:
|
||||
def test_multiple_package_order():
|
||||
"""
|
||||
Ensures that mutiple packages are merged in order.
|
||||
"""
|
||||
@@ -259,7 +257,7 @@ def test_multiple_package_order() -> None:
|
||||
assert actual == expected
|
||||
|
||||
|
||||
def test_package_list_merge() -> None:
|
||||
def test_package_list_merge():
|
||||
"""
|
||||
Ensures lists defined in both a package and the top-level config are merged correctly
|
||||
"""
|
||||
@@ -315,7 +313,7 @@ def test_package_list_merge() -> None:
|
||||
assert actual == expected
|
||||
|
||||
|
||||
def test_package_list_merge_by_id() -> None:
|
||||
def test_package_list_merge_by_id():
|
||||
"""
|
||||
Ensures that components with matching IDs are merged correctly.
|
||||
|
||||
@@ -393,7 +391,7 @@ def test_package_list_merge_by_id() -> None:
|
||||
assert actual == expected
|
||||
|
||||
|
||||
def test_package_merge_by_id_with_list() -> None:
|
||||
def test_package_merge_by_id_with_list():
|
||||
"""
|
||||
Ensures that components with matching IDs are merged correctly when their configuration contains lists.
|
||||
|
||||
@@ -432,7 +430,7 @@ def test_package_merge_by_id_with_list() -> None:
|
||||
assert actual == expected
|
||||
|
||||
|
||||
def test_package_merge_by_missing_id() -> None:
|
||||
def test_package_merge_by_missing_id():
|
||||
"""
|
||||
Ensures that a validation error is thrown when trying to extend a missing ID.
|
||||
"""
|
||||
@@ -468,7 +466,7 @@ def test_package_merge_by_missing_id() -> None:
|
||||
assert error_raised
|
||||
|
||||
|
||||
def test_package_list_remove_by_id() -> None:
|
||||
def test_package_list_remove_by_id():
|
||||
"""
|
||||
Ensures that components with matching IDs are removed correctly.
|
||||
|
||||
@@ -519,7 +517,7 @@ def test_package_list_remove_by_id() -> None:
|
||||
assert actual == expected
|
||||
|
||||
|
||||
def test_multiple_package_list_remove_by_id() -> None:
|
||||
def test_multiple_package_list_remove_by_id():
|
||||
"""
|
||||
Ensures that components with matching IDs are removed correctly.
|
||||
|
||||
@@ -565,7 +563,7 @@ def test_multiple_package_list_remove_by_id() -> None:
|
||||
assert actual == expected
|
||||
|
||||
|
||||
def test_package_dict_remove_by_id(basic_wifi, basic_esphome) -> None:
|
||||
def test_package_dict_remove_by_id(basic_wifi, basic_esphome):
|
||||
"""
|
||||
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.
|
||||
@@ -586,7 +584,7 @@ def test_package_dict_remove_by_id(basic_wifi, basic_esphome) -> None:
|
||||
assert actual == expected
|
||||
|
||||
|
||||
def test_package_remove_by_missing_id() -> None:
|
||||
def test_package_remove_by_missing_id():
|
||||
"""
|
||||
Ensures that components with missing IDs are not merged.
|
||||
"""
|
||||
@@ -634,7 +632,7 @@ def test_package_remove_by_missing_id() -> None:
|
||||
@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
|
||||
"""
|
||||
@@ -706,7 +704,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
|
||||
"""
|
||||
@@ -795,199 +793,3 @@ 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)
|
||||
|
||||
@@ -2,6 +2,6 @@ substitutions:
|
||||
enable_rx_pin: GPIO13
|
||||
|
||||
packages:
|
||||
uart_1200_none_2stopbits: !include ../../test_build_components/common/uart_1200_none_2stopbits/esp32-idf.yaml
|
||||
uart: !include ../../test_build_components/common/uart_1200_none_2stopbits/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -2,6 +2,6 @@ substitutions:
|
||||
enable_rx_pin: GPIO15
|
||||
|
||||
packages:
|
||||
uart_1200_none_2stopbits: !include ../../test_build_components/common/uart_1200_none_2stopbits/esp8266-ard.yaml
|
||||
uart: !include ../../test_build_components/common/uart_1200_none_2stopbits/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -2,6 +2,6 @@ substitutions:
|
||||
enable_rx_pin: GPIO3
|
||||
|
||||
packages:
|
||||
uart_1200_none_2stopbits: !include ../../test_build_components/common/uart_1200_none_2stopbits/rp2040-ard.yaml
|
||||
uart: !include ../../test_build_components/common/uart_1200_none_2stopbits/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -52,7 +52,6 @@ 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");
|
||||
|
||||
@@ -24,7 +24,6 @@ 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);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
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
|
||||
@@ -1,61 +0,0 @@
|
||||
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
|
||||
@@ -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, merge_packages
|
||||
from esphome.components.packages import do_packages_pass
|
||||
from esphome.config import resolve_extend_remove
|
||||
from esphome.config_helpers import merge_config
|
||||
from esphome.const import CONF_SUBSTITUTIONS
|
||||
@@ -74,8 +74,6 @@ 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
|
||||
@@ -146,8 +144,6 @@ 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:
|
||||
|
||||
Reference in New Issue
Block a user