Compare commits

...

29 Commits

Author SHA1 Message Date
Jonathan Swoboda
5b5cede5f9 Merge pull request #12752 from esphome/bump-2025.12.3
2025.12.3
2025-12-30 09:31:31 -05:00
Jonathan Swoboda
c737033cc4 Bump version to 2025.12.3 2025-12-30 09:22:03 -05:00
J. Nick Koston
0194bfd9ea [core] Fix incremental build failures when adding components on ESP32-Arduino (#12745) 2025-12-30 09:22:03 -05:00
J. Nick Koston
339399eb70 [lvgl] Fix lambdas in canvas actions called from outside LVGL context (#12671) 2025-12-30 09:22:03 -05:00
Jonathan Swoboda
99f7e9aeb7 Merge pull request #12632 from esphome/bump-2025.12.2
2025.12.2
2025-12-23 11:17:01 -05:00
Jonathan Swoboda
ebb6babb3d Fix hash 2025-12-23 09:26:38 -05:00
Jonathan Swoboda
0922f240e0 Bump version to 2025.12.2 2025-12-23 09:23:04 -05:00
Jonathan Swoboda
c8fb694dcb [cc1101] Fix packet mode RSSI/LQI (#12630)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-23 09:23:04 -05:00
J. Nick Koston
6054685dae [esp32_camera] Throttle frame logging to reduce overhead and improve throughput (#12586) 2025-12-23 09:23:04 -05:00
Anna Oake
61ec3508ed [cc1101] Fix option defaults and move them to YAML (#12608) 2025-12-23 09:23:04 -05:00
Leo Bergolth
086ec770ea send NIL ("-") as timestamp if time source is not valid (#12588) 2025-12-23 09:23:04 -05:00
Stuart Parmenter
b055f5b4bf [hub75] Bump esp-hub75 version to 0.1.7 (#12564) 2025-12-23 09:23:00 -05:00
Eduard Llull
726db746c8 [display_menu_base] Call on_value_ after updating the select (#12584) 2025-12-23 09:21:54 -05:00
Keith Burzinski
1922455fa7 [wifi] Fix for wifi_info when static IP is configured (#12576) 2025-12-23 09:21:54 -05:00
Thomas Rupprecht
dc943d7e7a [pca9685,sx126x,sx127x] Use frequency/float_range check (#12490)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-23 09:21:54 -05:00
Jonathan Swoboda
93e38f2608 Merge pull request #12569 from esphome/bump-2025.12.1
2025.12.1
2025-12-19 10:53:05 -05:00
Jonathan Swoboda
3a888326d8 Bump version to 2025.12.1 2025-12-19 10:13:35 -05:00
Keith Burzinski
f0d0ea60a7 [esp32_ble, esp32_ble_tracker] Fix crash, error messages when ble.disable called during boot (#12560) 2025-12-19 10:13:35 -05:00
Jonathan Swoboda
7ca11764ab [template.alarm_control_panel] Fix compile without binary_sensor (#12548)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-19 10:13:35 -05:00
Jonathan Swoboda
3e38a5e630 [esp32_camera] Fix I2C driver conflict with other components (#12533)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-19 10:13:35 -05:00
Jonathan Swoboda
636be92c97 [bme68x_bsec2_i2c] Add MULTI_CONF support for multiple sensors (#12535)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-19 10:13:35 -05:00
Jack Wilsdon
195b1c6323 [pm1006] Fix "never" update interval detection (#12529) 2025-12-19 10:13:35 -05:00
Anna Oake
7e08092012 [cc1101] Fix default frequencies (#12539) 2025-12-19 10:13:35 -05:00
Jonathan Swoboda
0ea5f2fd81 Merge pull request #12525 from esphome/bump-2025.12.0
2025.12.0
2025-12-16 18:57:20 -05:00
Jonathan Swoboda
fa3d998c3d Bump version to 2025.12.0 2025-12-16 17:15:50 -05:00
Jonathan Swoboda
864aaeec01 Merge pull request #12520 from esphome/bump-2025.12.0b5
2025.12.0b5
2025-12-16 11:25:57 -05:00
Jonathan Swoboda
9c88e44300 Bump version to 2025.12.0b5 2025-12-16 10:35:31 -05:00
Jonathan Swoboda
4d6a93f92d [uart] Fix UART on default UART0 pins for ESP-IDF (#12519)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-16 10:35:31 -05:00
J. Nick Koston
7216120bfd [socket] Fix getpeername() returning local address instead of remote in LWIP raw TCP (#12475) 2025-12-16 10:35:31 -05:00
39 changed files with 363 additions and 153 deletions

View File

@@ -1 +1 @@
766420905c06eeb6c5f360f68fd965e5ddd9c4a5db6b823263d3ad3accb64a07
5969e705693278d984c5292e998df0cbaf34f7e1f04dfc7f7b7ad7168527bfa7

View File

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

View File

@@ -227,7 +227,7 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(ADE7880),
cv.Optional(CONF_FREQUENCY, default="50Hz"): cv.All(
cv.frequency, cv.Range(min=45.0, max=66.0)
cv.frequency, cv.float_range(min=45.0, max=66.0)
),
cv.Optional(CONF_IRQ0_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_IRQ1_PIN): pins.internal_gpio_input_pin_schema,

View File

@@ -11,6 +11,7 @@ CODEOWNERS = ["@neffs", "@kbx81"]
AUTO_LOAD = ["bme68x_bsec2"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
bme68x_bsec2_i2c_ns = cg.esphome_ns.namespace("bme68x_bsec2_i2c")
BME68xBSEC2I2CComponent = bme68x_bsec2_i2c_ns.class_(

View File

@@ -160,41 +160,63 @@ HYST_LEVEL = {
"High": HystLevel.HYST_LEVEL_HIGH,
}
# Config key -> Validator mapping
# Optional settings to generate setter calls for
CONFIG_MAP = {
CONF_OUTPUT_POWER: cv.float_range(min=-30.0, max=11.0),
CONF_RX_ATTENUATION: cv.enum(RX_ATTENUATION, upper=False),
CONF_DC_BLOCKING_FILTER: cv.boolean,
CONF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=300000000, max=928000000)),
CONF_IF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=25000, max=788000)),
CONF_FILTER_BANDWIDTH: cv.All(cv.frequency, cv.float_range(min=58000, max=812000)),
CONF_CHANNEL: cv.uint8_t,
CONF_CHANNEL_SPACING: cv.All(cv.frequency, cv.float_range(min=25000, max=405000)),
CONF_FSK_DEVIATION: cv.All(cv.frequency, cv.float_range(min=1500, max=381000)),
CONF_MSK_DEVIATION: cv.int_range(min=1, max=8),
CONF_SYMBOL_RATE: cv.float_range(min=600, max=500000),
CONF_SYNC_MODE: cv.enum(SYNC_MODE, upper=False),
CONF_CARRIER_SENSE_ABOVE_THRESHOLD: cv.boolean,
CONF_MODULATION_TYPE: cv.enum(MODULATION, upper=False),
CONF_MANCHESTER: cv.boolean,
CONF_NUM_PREAMBLE: cv.int_range(min=0, max=7),
CONF_SYNC1: cv.hex_uint8_t,
CONF_SYNC0: cv.hex_uint8_t,
CONF_MAGN_TARGET: cv.enum(MAGN_TARGET, upper=False),
CONF_MAX_LNA_GAIN: cv.enum(MAX_LNA_GAIN, upper=False),
CONF_MAX_DVGA_GAIN: cv.enum(MAX_DVGA_GAIN, upper=False),
CONF_CARRIER_SENSE_ABS_THR: cv.int_range(min=-8, max=7),
CONF_CARRIER_SENSE_REL_THR: cv.enum(CARRIER_SENSE_REL_THR, upper=False),
CONF_LNA_PRIORITY: cv.boolean,
CONF_FILTER_LENGTH_FSK_MSK: cv.enum(FILTER_LENGTH_FSK_MSK, upper=False),
CONF_FILTER_LENGTH_ASK_OOK: cv.enum(FILTER_LENGTH_ASK_OOK, upper=False),
CONF_FREEZE: cv.enum(FREEZE, upper=False),
CONF_WAIT_TIME: cv.enum(WAIT_TIME, upper=False),
CONF_HYST_LEVEL: cv.enum(HYST_LEVEL, upper=False),
CONF_PACKET_MODE: cv.boolean,
CONF_PACKET_LENGTH: cv.uint8_t,
CONF_CRC_ENABLE: cv.boolean,
CONF_WHITENING: cv.boolean,
cv.Optional(CONF_OUTPUT_POWER, default=10): cv.float_range(min=-30.0, max=11.0),
cv.Optional(CONF_RX_ATTENUATION, default="0dB"): cv.enum(
RX_ATTENUATION, upper=False
),
cv.Optional(CONF_DC_BLOCKING_FILTER, default=True): cv.boolean,
cv.Optional(CONF_FREQUENCY, default="433.92MHz"): cv.All(
cv.frequency, cv.float_range(min=300.0e6, max=928.0e6)
),
cv.Optional(CONF_IF_FREQUENCY, default="153kHz"): cv.All(
cv.frequency, cv.float_range(min=25000, max=788000)
),
cv.Optional(CONF_FILTER_BANDWIDTH, default="203kHz"): cv.All(
cv.frequency, cv.float_range(min=58000, max=812000)
),
cv.Optional(CONF_CHANNEL, default=0): cv.uint8_t,
cv.Optional(CONF_CHANNEL_SPACING, default="200kHz"): cv.All(
cv.frequency, cv.float_range(min=25000, max=405000)
),
cv.Optional(CONF_FSK_DEVIATION): cv.All(
cv.frequency, cv.float_range(min=1500, max=381000)
),
cv.Optional(CONF_MSK_DEVIATION): cv.int_range(min=1, max=8),
cv.Optional(CONF_SYMBOL_RATE, default=5000): cv.float_range(min=600, max=500000),
cv.Optional(CONF_SYNC_MODE, default="16/16"): cv.enum(SYNC_MODE, upper=False),
cv.Optional(CONF_CARRIER_SENSE_ABOVE_THRESHOLD, default=False): cv.boolean,
cv.Optional(CONF_MODULATION_TYPE, default="ASK/OOK"): cv.enum(
MODULATION, upper=False
),
cv.Optional(CONF_MANCHESTER, default=False): cv.boolean,
cv.Optional(CONF_NUM_PREAMBLE, default=2): cv.int_range(min=0, max=7),
cv.Optional(CONF_SYNC1, default=0xD3): cv.hex_uint8_t,
cv.Optional(CONF_SYNC0, default=0x91): cv.hex_uint8_t,
cv.Optional(CONF_MAGN_TARGET, default="42dB"): cv.enum(MAGN_TARGET, upper=False),
cv.Optional(CONF_MAX_LNA_GAIN, default="Default"): cv.enum(
MAX_LNA_GAIN, upper=False
),
cv.Optional(CONF_MAX_DVGA_GAIN, default="-3"): cv.enum(MAX_DVGA_GAIN, upper=False),
cv.Optional(CONF_CARRIER_SENSE_ABS_THR): cv.int_range(min=-8, max=7),
cv.Optional(CONF_CARRIER_SENSE_REL_THR): cv.enum(
CARRIER_SENSE_REL_THR, upper=False
),
cv.Optional(CONF_LNA_PRIORITY, default=False): cv.boolean,
cv.Optional(CONF_FILTER_LENGTH_FSK_MSK): cv.enum(
FILTER_LENGTH_FSK_MSK, upper=False
),
cv.Optional(CONF_FILTER_LENGTH_ASK_OOK): cv.enum(
FILTER_LENGTH_ASK_OOK, upper=False
),
cv.Optional(CONF_FREEZE): cv.enum(FREEZE, upper=False),
cv.Optional(CONF_WAIT_TIME, default="32"): cv.enum(WAIT_TIME, upper=False),
cv.Optional(CONF_HYST_LEVEL): cv.enum(HYST_LEVEL, upper=False),
cv.Optional(CONF_PACKET_MODE, default=False): cv.boolean,
cv.Optional(CONF_PACKET_LENGTH): cv.uint8_t,
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
cv.Optional(CONF_WHITENING, default=False): cv.boolean,
}
@@ -217,7 +239,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
}
)
.extend({cv.Optional(key): validator for key, validator in CONFIG_MAP.items()})
.extend(CONFIG_MAP)
.extend(cv.COMPONENT_SCHEMA)
.extend(spi.spi_device_schema(cs_pin_required=True)),
_validate_packet_mode,
@@ -229,7 +251,8 @@ async def to_code(config):
await cg.register_component(var, config)
await spi.register_spi_device(var, config)
for key in CONFIG_MAP:
for opt in CONFIG_MAP:
key = opt.schema
if key in config:
cg.add(getattr(var, f"set_{key}")(config[key]))

View File

@@ -98,25 +98,8 @@ CC1101Component::CC1101Component() {
this->state_.LENGTH_CONFIG = 2;
this->state_.FS_AUTOCAL = 1;
// Default Settings
this->set_frequency(433920);
this->set_if_frequency(153);
this->set_filter_bandwidth(203);
this->set_channel(0);
this->set_channel_spacing(200);
this->set_symbol_rate(5000);
this->set_sync_mode(SyncMode::SYNC_MODE_NONE);
this->set_carrier_sense_above_threshold(true);
this->set_modulation_type(Modulation::MODULATION_ASK_OOK);
this->set_magn_target(MagnTarget::MAGN_TARGET_42DB);
this->set_max_lna_gain(MaxLnaGain::MAX_LNA_GAIN_DEFAULT);
this->set_max_dvga_gain(MaxDvgaGain::MAX_DVGA_GAIN_MINUS_3);
this->set_lna_priority(false);
this->set_wait_time(WaitTime::WAIT_TIME_32_SAMPLES);
// CRITICAL: Initialize PA Table to avoid transmitting 0 power (Silence)
memset(this->pa_table_, 0, sizeof(this->pa_table_));
this->set_output_power(10.0f);
}
void CC1101Component::setup() {
@@ -186,14 +169,16 @@ void CC1101Component::loop() {
}
// Read packet
uint8_t payload_length;
uint8_t payload_length, expected_rx;
if (this->state_.LENGTH_CONFIG == static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE)) {
this->read_(Register::FIFO, &payload_length, 1);
expected_rx = payload_length + 1;
} else {
payload_length = this->state_.PKTLEN;
expected_rx = payload_length;
}
if (payload_length == 0 || payload_length > 64) {
ESP_LOGW(TAG, "Invalid payload length: %u", payload_length);
if (payload_length == 0 || payload_length > 64 || rx_bytes != expected_rx) {
ESP_LOGW(TAG, "Invalid packet: rx_bytes %u, payload_length %u", rx_bytes, payload_length);
this->enter_idle_();
this->strobe_(Command::FRX);
this->strobe_(Command::RX);
@@ -203,13 +188,12 @@ void CC1101Component::loop() {
this->packet_.resize(payload_length);
this->read_(Register::FIFO, this->packet_.data(), payload_length);
// Read status and trigger
uint8_t status[2];
this->read_(Register::FIFO, status, 2);
int8_t rssi_raw = static_cast<int8_t>(status[0]);
float rssi = (rssi_raw * RSSI_STEP) - RSSI_OFFSET;
bool crc_ok = (status[1] & STATUS_CRC_OK_MASK) != 0;
uint8_t lqi = status[1] & STATUS_LQI_MASK;
// Read status from registers (more reliable than FIFO status bytes due to timing issues)
this->read_(Register::RSSI);
this->read_(Register::LQI);
float rssi = (this->state_.RSSI * RSSI_STEP) - RSSI_OFFSET;
bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0;
uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK;
if (this->state_.CRC_EN == 0 || crc_ok) {
this->packet_trigger_->trigger(this->packet_, rssi, lqi);
}
@@ -633,12 +617,15 @@ void CC1101Component::set_packet_mode(bool value) {
this->state_.GDO0_CFG = 0x01;
// Set max RX FIFO threshold to ensure we only trigger on end-of-packet
this->state_.FIFO_THR = 15;
// Don't append status bytes to FIFO - we read from registers instead
this->state_.APPEND_STATUS = 0;
} else {
// Configure GDO0 for serial data (async serial mode)
this->state_.GDO0_CFG = 0x0D;
}
if (this->initialized_) {
this->write_(Register::PKTCTRL0);
this->write_(Register::PKTCTRL1);
this->write_(Register::IOCFG0);
this->write_(Register::FIFOTHR);
}

View File

@@ -54,6 +54,7 @@ bool MenuItemSelect::select_next() {
if (this->select_var_ != nullptr) {
this->select_var_->make_call().select_next(true).perform();
this->on_value_();
changed = true;
}
@@ -65,6 +66,7 @@ bool MenuItemSelect::select_prev() {
if (this->select_var_ != nullptr) {
this->select_var_->make_call().select_previous(true).perform();
this->on_value_();
changed = true;
}

View File

@@ -308,13 +308,21 @@ bool ESP32BLE::ble_setup_() {
bool ESP32BLE::ble_dismantle_() {
esp_err_t err = esp_bluedroid_disable();
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err);
return false;
// ESP_ERR_INVALID_STATE means Bluedroid is already disabled, which is fine
if (err != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err);
return false;
}
ESP_LOGD(TAG, "Already disabled");
}
err = esp_bluedroid_deinit();
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_bluedroid_deinit failed: %d", err);
return false;
// ESP_ERR_INVALID_STATE means Bluedroid is already deinitialized, which is fine
if (err != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "esp_bluedroid_deinit failed: %d", err);
return false;
}
ESP_LOGD(TAG, "Already deinitialized");
}
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID

View File

@@ -212,17 +212,23 @@ extern ESP32BLE *global_ble;
template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> {
public:
bool check(const Ts &...x) override { return global_ble->is_active(); }
bool check(const Ts &...x) override { return global_ble != nullptr && global_ble->is_active(); }
};
template<typename... Ts> class BLEEnableAction : public Action<Ts...> {
public:
void play(const Ts &...x) override { global_ble->enable(); }
void play(const Ts &...x) override {
if (global_ble != nullptr)
global_ble->enable();
}
};
template<typename... Ts> class BLEDisableAction : public Action<Ts...> {
public:
void play(const Ts &...x) override { global_ble->disable(); }
void play(const Ts &...x) override {
if (global_ble != nullptr)
global_ble->disable();
}
};
} // namespace esphome::esp32_ble

View File

@@ -185,7 +185,10 @@ void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_();
void ESP32BLETracker::stop_scan_() {
if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
// If scanner is already idle, there's nothing to stop - this is not an error
if (this->scanner_state_ != ScannerState::IDLE) {
ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
}
return;
}
// Reset timeout state machine when stopping scan

View File

@@ -3,7 +3,7 @@ import logging
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.esp32 import add_idf_component
from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
from esphome.components.psram import DOMAIN as psram_domain
import esphome.config_validation as cv
from esphome.const import (
@@ -186,7 +186,7 @@ CONFIG_SCHEMA = cv.All(
{
cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number,
cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All(
cv.frequency, cv.Range(min=8e6, max=20e6)
cv.frequency, cv.float_range(min=8e6, max=20e6)
),
}
),
@@ -352,6 +352,8 @@ async def to_code(config):
cg.add_define("USE_CAMERA")
add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True)
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_LEGACY", False)
for conf in config.get(CONF_ON_STREAM_START, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

View File

@@ -11,6 +11,9 @@ namespace esphome {
namespace esp32_camera {
static const char *const TAG = "esp32_camera";
#if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE
static constexpr uint32_t FRAME_LOG_INTERVAL_MS = 60000;
#endif
/* ---------------- public API (derivated) ---------------- */
void ESP32Camera::setup() {
@@ -204,7 +207,20 @@ void ESP32Camera::loop() {
}
this->current_image_ = std::make_shared<ESP32CameraImage>(fb, this->single_requesters_ | this->stream_requesters_);
ESP_LOGD(TAG, "Got Image: len=%u", fb->len);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGV(TAG, "Got Image: len=%u", fb->len);
#else
// Initialize log time on first frame to ensure accurate interval measurement
if (this->frame_count_ == 0) {
this->last_log_time_ = now;
}
this->frame_count_++;
if (now - this->last_log_time_ >= FRAME_LOG_INTERVAL_MS) {
ESP_LOGD(TAG, "Received %u images in last %us", this->frame_count_, FRAME_LOG_INTERVAL_MS / 1000);
this->last_log_time_ = now;
this->frame_count_ = 0;
}
#endif
for (auto *listener : this->listeners_) {
listener->on_camera_image(this->current_image_);
}

View File

@@ -213,6 +213,10 @@ class ESP32Camera : public camera::Camera {
uint32_t last_idle_request_{0};
uint32_t last_update_{0};
#if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE
uint32_t last_log_time_{0};
uint16_t frame_count_{0};
#endif
#ifdef USE_I2C
i2c::InternalI2CBus *i2c_bus_{nullptr};
#endif // USE_I2C

View File

@@ -16,7 +16,7 @@ def valid_pwm_pin(value):
esp8266_pwm_ns = cg.esphome_ns.namespace("esp8266_pwm")
ESP8266PWM = esp8266_pwm_ns.class_("ESP8266PWM", output.FloatOutput, cg.Component)
SetFrequencyAction = esp8266_pwm_ns.class_("SetFrequencyAction", automation.Action)
validate_frequency = cv.All(cv.frequency, cv.Range(min=1.0e-6))
validate_frequency = cv.All(cv.frequency, cv.float_range(min=1.0e-6))
CONFIG_SCHEMA = cv.All(
output.FLOAT_OUTPUT_SCHEMA.extend(

View File

@@ -93,35 +93,35 @@ CONF_DOUBLE_BUFFER = "double_buffer"
CONF_MIN_REFRESH_RATE = "min_refresh_rate"
# Map to hub75 library enums (in global namespace)
ShiftDriver = cg.global_ns.enum("ShiftDriver", is_class=True)
Hub75ShiftDriver = cg.global_ns.enum("Hub75ShiftDriver", is_class=True)
SHIFT_DRIVERS = {
"GENERIC": ShiftDriver.GENERIC,
"FM6126A": ShiftDriver.FM6126A,
"ICN2038S": ShiftDriver.ICN2038S,
"FM6124": ShiftDriver.FM6124,
"MBI5124": ShiftDriver.MBI5124,
"DP3246": ShiftDriver.DP3246,
"GENERIC": Hub75ShiftDriver.GENERIC,
"FM6126A": Hub75ShiftDriver.FM6126A,
"ICN2038S": Hub75ShiftDriver.ICN2038S,
"FM6124": Hub75ShiftDriver.FM6124,
"MBI5124": Hub75ShiftDriver.MBI5124,
"DP3246": Hub75ShiftDriver.DP3246,
}
PanelLayout = cg.global_ns.enum("PanelLayout", is_class=True)
Hub75PanelLayout = cg.global_ns.enum("Hub75PanelLayout", is_class=True)
PANEL_LAYOUTS = {
"HORIZONTAL": PanelLayout.HORIZONTAL,
"TOP_LEFT_DOWN": PanelLayout.TOP_LEFT_DOWN,
"TOP_RIGHT_DOWN": PanelLayout.TOP_RIGHT_DOWN,
"BOTTOM_LEFT_UP": PanelLayout.BOTTOM_LEFT_UP,
"BOTTOM_RIGHT_UP": PanelLayout.BOTTOM_RIGHT_UP,
"TOP_LEFT_DOWN_ZIGZAG": PanelLayout.TOP_LEFT_DOWN_ZIGZAG,
"TOP_RIGHT_DOWN_ZIGZAG": PanelLayout.TOP_RIGHT_DOWN_ZIGZAG,
"BOTTOM_LEFT_UP_ZIGZAG": PanelLayout.BOTTOM_LEFT_UP_ZIGZAG,
"BOTTOM_RIGHT_UP_ZIGZAG": PanelLayout.BOTTOM_RIGHT_UP_ZIGZAG,
"HORIZONTAL": Hub75PanelLayout.HORIZONTAL,
"TOP_LEFT_DOWN": Hub75PanelLayout.TOP_LEFT_DOWN,
"TOP_RIGHT_DOWN": Hub75PanelLayout.TOP_RIGHT_DOWN,
"BOTTOM_LEFT_UP": Hub75PanelLayout.BOTTOM_LEFT_UP,
"BOTTOM_RIGHT_UP": Hub75PanelLayout.BOTTOM_RIGHT_UP,
"TOP_LEFT_DOWN_ZIGZAG": Hub75PanelLayout.TOP_LEFT_DOWN_ZIGZAG,
"TOP_RIGHT_DOWN_ZIGZAG": Hub75PanelLayout.TOP_RIGHT_DOWN_ZIGZAG,
"BOTTOM_LEFT_UP_ZIGZAG": Hub75PanelLayout.BOTTOM_LEFT_UP_ZIGZAG,
"BOTTOM_RIGHT_UP_ZIGZAG": Hub75PanelLayout.BOTTOM_RIGHT_UP_ZIGZAG,
}
ScanPattern = cg.global_ns.enum("ScanPattern", is_class=True)
Hub75ScanWiring = cg.global_ns.enum("Hub75ScanWiring", is_class=True)
SCAN_PATTERNS = {
"STANDARD_TWO_SCAN": ScanPattern.STANDARD_TWO_SCAN,
"FOUR_SCAN_16PX_HIGH": ScanPattern.FOUR_SCAN_16PX_HIGH,
"FOUR_SCAN_32PX_HIGH": ScanPattern.FOUR_SCAN_32PX_HIGH,
"FOUR_SCAN_64PX_HIGH": ScanPattern.FOUR_SCAN_64PX_HIGH,
"STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN,
"FOUR_SCAN_16PX_HIGH": Hub75ScanWiring.FOUR_SCAN_16PX_HIGH,
"FOUR_SCAN_32PX_HIGH": Hub75ScanWiring.FOUR_SCAN_32PX_HIGH,
"FOUR_SCAN_64PX_HIGH": Hub75ScanWiring.FOUR_SCAN_64PX_HIGH,
}
Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True)
@@ -528,7 +528,7 @@ def _build_config_struct(
async def to_code(config: ConfigType) -> None:
add_idf_component(
name="esphome/esp-hub75",
ref="0.1.6",
ref="0.1.7",
)
# Set compile-time configuration via defines

View File

@@ -121,7 +121,7 @@ CONFIG_SCHEMA = cv.All(
nrf52="100kHz",
): cv.All(
cv.frequency,
cv.Range(min=0, min_included=False),
cv.float_range(min=0, min_included=False),
),
cv.Optional(CONF_TIMEOUT): cv.All(
cv.only_with_framework(["arduino", "esp-idf"]),

View File

@@ -45,7 +45,9 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(LEDCOutput),
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.frequency,
cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.All(
cv.frequency, cv.float_range(min=0, min_included=False)
),
cv.Optional(CONF_CHANNEL): cv.int_range(min=0, max=15),
cv.Optional(CONF_PHASE_ANGLE): cv.All(
cv.only_with_esp_idf, cv.angle, cv.float_range(min=0.0, max=360.0)

View File

@@ -14,7 +14,9 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(LibreTinyPWM),
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.frequency,
cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.All(
cv.frequency, cv.float_range(min=0, min_included=False)
),
}
).extend(cv.COMPONENT_SCHEMA)

View File

@@ -5,7 +5,7 @@ Constants already defined in esphome.const are not duplicated here and must be i
"""
import logging
from typing import TYPE_CHECKING, Any
from typing import Any
from esphome import codegen as cg, config_validation as cv
from esphome.const import CONF_ITEMS
@@ -96,13 +96,9 @@ class LValidator:
return None
if isinstance(value, Lambda):
# Local import to avoid circular import
from .lvcode import CodeContext, LambdaContext
from .lvcode import get_lambda_context_args
if TYPE_CHECKING:
# CodeContext does not have get_automation_parameters
# so we need to assert the type here
assert isinstance(CodeContext.code_context, LambdaContext)
args = args or CodeContext.code_context.get_automation_parameters()
args = args or get_lambda_context_args()
return cg.RawExpression(
call_lambda(
await cg.process_lambda(value, args, return_type=self.rtype)

View File

@@ -1,5 +1,5 @@
import re
from typing import TYPE_CHECKING, Any
from typing import Any
import esphome.codegen as cg
from esphome.components import image
@@ -404,14 +404,9 @@ class TextValidator(LValidator):
self, value: Any, args: list[tuple[SafeExpType, str]] | None = None
) -> Expression:
# Local import to avoid circular import at module level
from .lvcode import get_lambda_context_args
from .lvcode import CodeContext, LambdaContext
if TYPE_CHECKING:
# CodeContext does not have get_automation_parameters
# so we need to assert the type here
assert isinstance(CodeContext.code_context, LambdaContext)
args = args or CodeContext.code_context.get_automation_parameters()
args = args or get_lambda_context_args()
if isinstance(value, dict):
if format_str := value.get(CONF_FORMAT):

View File

@@ -1,4 +1,5 @@
import abc
from typing import TYPE_CHECKING
from esphome import codegen as cg
from esphome.config import Config
@@ -200,6 +201,21 @@ class LvContext(LambdaContext):
return self.add(*args)
def get_lambda_context_args() -> list[tuple[SafeExpType, str]]:
"""Get automation parameters from the current lambda context if available.
When called from outside LVGL's context (e.g., from interval),
CodeContext.code_context will be None, so return empty args.
"""
if CodeContext.code_context is None:
return []
if TYPE_CHECKING:
# CodeContext base class doesn't define get_automation_parameters(),
# but LambdaContext and LvContext (the concrete implementations) do.
assert isinstance(CodeContext.code_context, LambdaContext)
return CodeContext.code_context.get_automation_parameters()
class LocalVariable(MockObj):
"""
Create a local variable and enclose the code using it within a block.

View File

@@ -38,7 +38,7 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(PCA9685Output),
cv.Optional(CONF_FREQUENCY): cv.All(
cv.frequency, cv.Range(min=23.84, max=1525.88)
cv.frequency, cv.float_range(min=23.84, max=1525.88)
),
cv.Optional(CONF_EXTERNAL_CLOCK_INPUT, default=False): cv.boolean,
cv.Optional(CONF_PHASE_BALANCER, default="linear"): cv.enum(

View File

@@ -7,10 +7,10 @@ from esphome.const import (
CONF_UPDATE_INTERVAL,
DEVICE_CLASS_PM25,
ICON_BLUR,
SCHEDULER_DONT_RUN,
STATE_CLASS_MEASUREMENT,
UNIT_MICROGRAMS_PER_CUBIC_METER,
)
from esphome.core import TimePeriodMilliseconds
CODEOWNERS = ["@habbie"]
DEPENDENCIES = ["uart"]
@@ -41,16 +41,12 @@ CONFIG_SCHEMA = cv.All(
def validate_interval_uart(config):
require_tx = False
interval = config.get(CONF_UPDATE_INTERVAL)
if isinstance(interval, TimePeriodMilliseconds):
# 'never' is encoded as a very large int, not as a TimePeriodMilliseconds objects
require_tx = True
uart.final_validate_device_schema(
"pm1006", baud_rate=9600, require_rx=True, require_tx=require_tx
"pm1006",
baud_rate=9600,
require_rx=True,
require_tx=interval.total_milliseconds != SCHEDULER_DONT_RUN,
)(config)

View File

@@ -11,7 +11,7 @@ DEPENDENCIES = ["rp2040"]
rp2040_pwm_ns = cg.esphome_ns.namespace("rp2040_pwm")
RP2040PWM = rp2040_pwm_ns.class_("RP2040PWM", output.FloatOutput, cg.Component)
SetFrequencyAction = rp2040_pwm_ns.class_("SetFrequencyAction", automation.Action)
validate_frequency = cv.All(cv.frequency, cv.Range(min=1.0e-6))
validate_frequency = cv.All(cv.frequency, cv.float_range(min=1.0e-6))
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{

View File

@@ -188,7 +188,7 @@ class LWIPRawImpl : public Socket {
errno = EINVAL;
return -1;
}
return this->ip2sockaddr_(&pcb_->local_ip, pcb_->local_port, name, addrlen);
return this->ip2sockaddr_(&pcb_->remote_ip, pcb_->remote_port, name, addrlen);
}
std::string getpeername() override {
if (pcb_ == nullptr) {

View File

@@ -199,9 +199,13 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_CRC_INITIAL, default=0x1D0F): cv.All(
cv.hex_int, cv.Range(min=0, max=0xFFFF)
),
cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000),
cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All(
cv.frequency, cv.float_range(min=0, max=100000)
),
cv.Required(CONF_DIO1_PIN): pins.gpio_input_pin_schema,
cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000),
cv.Required(CONF_FREQUENCY): cv.All(
cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6)
),
cv.Required(CONF_HW_VERSION): cv.one_of(
"sx1261", "sx1262", "sx1268", "llcc68", lower=True
),

View File

@@ -196,9 +196,13 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_BITSYNC): cv.boolean,
cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE),
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000),
cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All(
cv.frequency, cv.float_range(min=0, max=100000)
),
cv.Optional(CONF_DIO0_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000),
cv.Required(CONF_FREQUENCY): cv.All(
cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6)
),
cv.Required(CONF_MODULATION): cv.enum(MOD),
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
cv.Optional(CONF_PA_PIN, default="BOOST"): cv.enum(PA_PIN),

View File

@@ -34,7 +34,15 @@ void Syslog::log_(const int level, const char *tag, const char *message, size_t
severity = LOG_LEVEL_TO_SYSLOG_SEVERITY[level];
}
int pri = this->facility_ * 8 + severity;
auto timestamp = this->time_->now().strftime("%b %e %H:%M:%S");
auto now = this->time_->now();
std::string timestamp;
if (now.is_valid()) {
timestamp = now.strftime("%b %e %H:%M:%S");
} else {
// RFC 5424: A syslog application MUST use the NILVALUE as TIMESTAMP if the syslog application is incapable of
// obtaining system time.
timestamp = "-";
}
size_t len = message_len;
// remove color formatting
if (this->strip_ && message[0] == 0x1B && len > 11) {

View File

@@ -39,6 +39,7 @@ enum TemplateAlarmControlPanelRestoreMode {
ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED,
};
#ifdef USE_BINARY_SENSOR
struct SensorDataStore {
bool last_chime_state;
};
@@ -49,7 +50,6 @@ struct SensorInfo {
uint8_t store_index;
};
#ifdef USE_BINARY_SENSOR
struct AlarmSensor {
binary_sensor::BinarySensor *sensor;
SensorInfo info;
@@ -139,6 +139,9 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
FixedVector<AlarmSensor> sensors_;
// a list of automatically bypassed sensors
std::vector<uint8_t> bypassed_sensor_indicies_;
// Per sensor data store
std::vector<SensorDataStore> sensor_data_;
uint8_t next_store_index_ = 0;
#endif
TemplateAlarmControlPanelRestoreMode restore_mode_{};
@@ -154,14 +157,11 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
uint32_t trigger_time_;
// a list of codes
std::vector<std::string> codes_;
// Per sensor data store
std::vector<SensorDataStore> sensor_data_;
// requires a code to arm
bool requires_code_to_arm_ = false;
bool supports_arm_home_ = false;
bool supports_arm_night_ = false;
bool sensors_ready_ = false;
uint8_t next_store_index_ = 0;
// check if the code is valid
bool is_code_valid_(optional<std::string> code);

View File

@@ -9,6 +9,7 @@
#include "esphome/core/gpio.h"
#include "driver/gpio.h"
#include "soc/gpio_num.h"
#include "soc/uart_pins.h"
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
@@ -139,6 +140,22 @@ void IDFUARTComponent::load_settings(bool dump_config) {
return;
}
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1;
// Workaround for ESP-IDF issue: https://github.com/espressif/esp-idf/issues/17459
// Commit 9ed617fb17 removed gpio_func_sel() calls from uart_set_pin(), which breaks
// UART on default UART0 pins that may have residual state from boot console.
// Reset these pins before configuring UART to ensure they're in a clean state.
if (tx == U0TXD_GPIO_NUM || tx == U0RXD_GPIO_NUM) {
gpio_reset_pin(static_cast<gpio_num_t>(tx));
}
if (rx == U0TXD_GPIO_NUM || rx == U0RXD_GPIO_NUM) {
gpio_reset_pin(static_cast<gpio_num_t>(rx));
}
// Setup pins after reset to preserve open drain/pullup/pulldown flags
auto setup_pin_if_needed = [](InternalGPIOPin *pin) {
if (!pin) {
return;
@@ -154,10 +171,6 @@ void IDFUARTComponent::load_settings(bool dump_config) {
setup_pin_if_needed(this->tx_pin_);
}
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1;
uint32_t invert = 0;
if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) {
invert |= UART_SIGNAL_TXD_INV;

View File

@@ -528,6 +528,16 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
for (auto *listener : global_wifi_component->connect_state_listeners_) {
listener->on_wifi_connect_state(global_wifi_component->wifi_ssid(), global_wifi_component->wifi_bssid());
}
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#ifdef USE_WIFI_MANUAL_IP
if (const WiFiAP *config = global_wifi_component->get_selected_sta_();
config && config->get_manual_ip().has_value()) {
for (auto *listener : global_wifi_component->ip_state_listeners_) {
listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(),
global_wifi_component->get_dns_address(0), global_wifi_component->get_dns_address(1));
}
}
#endif
#endif
break;
}

View File

@@ -739,6 +739,14 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
for (auto *listener : this->connect_state_listeners_) {
listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid());
}
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#ifdef USE_WIFI_MANUAL_IP
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
for (auto *listener : this->ip_state_listeners_) {
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
}
}
#endif
#endif
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) {

View File

@@ -305,6 +305,14 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
for (auto *listener : this->connect_state_listeners_) {
listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid());
}
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#ifdef USE_WIFI_MANUAL_IP
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
for (auto *listener : this->ip_state_listeners_) {
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
}
}
#endif
#endif
break;
}

View File

@@ -259,6 +259,15 @@ void WiFiComponent::wifi_loop_() {
for (auto *listener : this->connect_state_listeners_) {
listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid());
}
// For static IP configurations, notify IP listeners immediately as the IP is already configured
#ifdef USE_WIFI_MANUAL_IP
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
s_sta_had_ip = true;
for (auto *listener : this->ip_state_listeners_) {
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
}
}
#endif
#endif
} else if (!is_connected && s_sta_was_connected) {
// Just disconnected

View File

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

View File

@@ -27,3 +27,7 @@ dependencies:
version: "1.7.6~1"
rules:
- if: "target in [esp32s2, esp32s3, esp32p4]"
esphome/esp-hub75:
version: 0.1.7
rules:
- if: "target in [esp32, esp32s2, esp32s3, esp32p4]"

View File

@@ -99,14 +99,11 @@ def storage_should_clean(old: StorageJSON | None, new: StorageJSON) -> bool:
def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> bool:
if (
# ESP32 uses CMake for both Arduino and ESP-IDF frameworks
return (
old.loaded_integrations != new.loaded_integrations
or old.loaded_platforms != new.loaded_platforms
) and new.core_platform == PLATFORM_ESP32:
from esphome.components.esp32 import FRAMEWORK_ESP_IDF
return new.framework == FRAMEWORK_ESP_IDF
return False
) and new.core_platform == PLATFORM_ESP32
def update_storage_json() -> None:

View File

@@ -156,7 +156,6 @@ lib_deps =
esphome/ESP32-audioI2S@2.3.0 ; i2s_audio
droscy/esp_wireguard@0.4.2 ; wireguard
esphome/esp-audio-libs@2.0.1 ; audio
esphome/esp-hub75@0.1.6 ; hub75
build_flags =
${common:arduino.build_flags}
@@ -180,7 +179,6 @@ lib_deps =
droscy/esp_wireguard@0.4.2 ; wireguard
kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word
esphome/esp-audio-libs@2.0.1 ; audio
esphome/esp-hub75@0.1.6 ; hub75
build_flags =
${common:idf.build_flags}
-Wno-nonnull-compare

View File

@@ -9,6 +9,13 @@ from unittest.mock import MagicMock, patch
import pytest
from esphome.const import (
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
PLATFORM_RTL87XX,
)
from esphome.core import EsphomeError
from esphome.storage_json import StorageJSON
from esphome.writer import (
@@ -21,6 +28,7 @@ from esphome.writer import (
clean_build,
clean_cmake_cache,
storage_should_clean,
storage_should_update_cmake_cache,
update_storage_json,
write_cpp,
write_gitignore,
@@ -164,6 +172,86 @@ def test_storage_edge_case_from_empty_integrations(
assert storage_should_clean(old, new) is False
# Tests for storage_should_update_cmake_cache
@pytest.mark.parametrize("framework", ["arduino", "esp-idf"])
def test_storage_should_update_cmake_cache_when_integration_added_esp32(
create_storage: Callable[..., StorageJSON],
framework: str,
) -> None:
"""Test cmake cache update triggered when integration added on ESP32."""
old = create_storage(
loaded_integrations=["api", "wifi"],
core_platform=PLATFORM_ESP32,
framework=framework,
)
new = create_storage(
loaded_integrations=["api", "wifi", "restart"],
core_platform=PLATFORM_ESP32,
framework=framework,
)
assert storage_should_update_cmake_cache(old, new) is True
def test_storage_should_update_cmake_cache_when_platform_changed_esp32(
create_storage: Callable[..., StorageJSON],
) -> None:
"""Test cmake cache update triggered when platforms change on ESP32."""
old = create_storage(
loaded_integrations=["api", "wifi"],
loaded_platforms={"sensor"},
core_platform=PLATFORM_ESP32,
framework="arduino",
)
new = create_storage(
loaded_integrations=["api", "wifi"],
loaded_platforms={"sensor", "binary_sensor"},
core_platform=PLATFORM_ESP32,
framework="arduino",
)
assert storage_should_update_cmake_cache(old, new) is True
def test_storage_should_not_update_cmake_cache_when_nothing_changes(
create_storage: Callable[..., StorageJSON],
) -> None:
"""Test cmake cache not updated when nothing changes."""
old = create_storage(
loaded_integrations=["api", "wifi"],
core_platform=PLATFORM_ESP32,
framework="arduino",
)
new = create_storage(
loaded_integrations=["api", "wifi"],
core_platform=PLATFORM_ESP32,
framework="arduino",
)
assert storage_should_update_cmake_cache(old, new) is False
@pytest.mark.parametrize(
"core_platform",
[PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_BK72XX, PLATFORM_RTL87XX],
)
def test_storage_should_not_update_cmake_cache_for_non_esp32(
create_storage: Callable[..., StorageJSON],
core_platform: str,
) -> None:
"""Test cmake cache not updated for non-ESP32 platforms."""
old = create_storage(
loaded_integrations=["api", "wifi"],
core_platform=core_platform,
framework="arduino",
)
new = create_storage(
loaded_integrations=["api", "wifi", "restart"],
core_platform=core_platform,
framework="arduino",
)
assert storage_should_update_cmake_cache(old, new) is False
@patch("esphome.writer.clean_build")
@patch("esphome.writer.StorageJSON")
@patch("esphome.writer.storage_path")