diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 316f43e706..5dd779effb 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -32b0db73b3ae01ba18c9cbb1dabbd8156bc14dded500471919bd0a3dc33916e0 +f84518ea4140c194b21cc516aae05aaa0cf876ab866f89e22e91842df46333ed diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index efe3b190af..1232d9677f 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -1,6 +1,6 @@ from esphome import pins import esphome.codegen as cg -from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C2, @@ -140,6 +140,16 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { 9: adc_channel_t.ADC_CHANNEL_8, 10: adc_channel_t.ADC_CHANNEL_9, }, + VARIANT_ESP32P4: { + 16: adc_channel_t.ADC_CHANNEL_0, + 17: adc_channel_t.ADC_CHANNEL_1, + 18: adc_channel_t.ADC_CHANNEL_2, + 19: adc_channel_t.ADC_CHANNEL_3, + 20: adc_channel_t.ADC_CHANNEL_4, + 21: adc_channel_t.ADC_CHANNEL_5, + 22: adc_channel_t.ADC_CHANNEL_6, + 23: adc_channel_t.ADC_CHANNEL_7, + }, } # pin to adc2 channel mapping @@ -198,6 +208,14 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { 19: adc_channel_t.ADC_CHANNEL_8, 20: adc_channel_t.ADC_CHANNEL_9, }, + VARIANT_ESP32P4: { + 49: adc_channel_t.ADC_CHANNEL_0, + 50: adc_channel_t.ADC_CHANNEL_1, + 51: adc_channel_t.ADC_CHANNEL_2, + 52: adc_channel_t.ADC_CHANNEL_3, + 53: adc_channel_t.ADC_CHANNEL_4, + 54: adc_channel_t.ADC_CHANNEL_5, + }, } diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index a60272a1f7..00a703191e 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -136,8 +136,8 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage adc_oneshot_unit_handle_t adc_handle_{nullptr}; adc_cali_handle_t calibration_handle_{nullptr}; adc_atten_t attenuation_{ADC_ATTEN_DB_0}; - adc_channel_t channel_; - adc_unit_t adc_unit_; + adc_channel_t channel_{}; + adc_unit_t adc_unit_{}; struct SetupFlags { uint8_t init_complete : 1; uint8_t config_complete : 1; diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index 4f0ffbdc38..9905475b1e 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -72,10 +72,9 @@ void ADCSensor::setup() { // Initialize ADC calibration if (this->calibration_handle_ == nullptr) { adc_cali_handle_t handle = nullptr; - esp_err_t err; #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 + USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 // RISC-V variants and S3 use curve fitting calibration adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) @@ -187,7 +186,7 @@ float ADCSensor::sample_fixed_attenuation_() { ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err); if (this->calibration_handle_ != nullptr) { #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 + USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); #else // Other ESP32 variants use line fitting calibration adc_cali_delete_scheme_line_fitting(this->calibration_handle_); @@ -220,7 +219,7 @@ float ADCSensor::sample_autorange_() { if (this->calibration_handle_ != nullptr) { // Delete old calibration handle #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 + USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); #else adc_cali_delete_scheme_line_fitting(this->calibration_handle_); @@ -232,7 +231,7 @@ float ADCSensor::sample_autorange_() { adc_cali_handle_t handle = nullptr; #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 + USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 adc_cali_curve_fitting_config_t cali_config = {}; #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) cali_config.chan = this->channel_; @@ -261,7 +260,7 @@ float ADCSensor::sample_autorange_() { ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); if (handle != nullptr) { #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 + USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 adc_cali_delete_scheme_curve_fitting(handle); #else adc_cali_delete_scheme_line_fitting(handle); @@ -281,7 +280,7 @@ float ADCSensor::sample_autorange_() { } // Clean up calibration handle #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 + USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 adc_cali_delete_scheme_curve_fitting(handle); #else adc_cali_delete_scheme_line_fitting(handle); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index d8b2a4db2c..f57d37f5a5 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -300,7 +300,7 @@ class APIConnection : public APIServerConnection { #ifdef USE_API_HOMEASSISTANT_STATES void process_state_subscriptions_(); -#endif // USE_API_HOMEASSISTANT_STATES +#endif // Non-template helper to encode any ProtoMessage static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index e3931e3946..376a399637 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -516,6 +516,7 @@ def binary_sensor_schema( icon: str = cv.UNDEFINED, entity_category: str = cv.UNDEFINED, device_class: str = cv.UNDEFINED, + filters: list = cv.UNDEFINED, ) -> cv.Schema: schema = {} @@ -527,6 +528,7 @@ def binary_sensor_schema( (CONF_ICON, icon, cv.icon), (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), (CONF_DEVICE_CLASS, device_class, validate_device_class), + (CONF_FILTERS, filters, validate_filters), ]: if default is not cv.UNDEFINED: schema[cv.Optional(key, default=default)] = validator diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 78bafcb790..4ab85a55cd 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -313,7 +313,7 @@ def _format_framework_espidf_version( RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1) # The platform-espressif32 version to use for arduino frameworks # - https://github.com/pioarduino/platform-espressif32/releases -ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21) +ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21, "1") # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases @@ -322,7 +322,7 @@ RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 4, 2) # The platformio/espressif32 version to use for esp-idf frameworks # - https://github.com/platformio/platform-espressif32/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 -ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21) +ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21, "1") # List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ @@ -468,10 +468,10 @@ def _parse_platform_version(value): try: ver = cv.Version.parse(cv.version_number(value)) if ver.major >= 50: # a pioarduino version - if "-" in value: - # maybe a release candidate?...definitely not our default, just use it as-is... - return f"https://github.com/pioarduino/platform-espressif32/releases/download/{value}/platform-espressif32.zip" - return f"https://github.com/pioarduino/platform-espressif32/releases/download/{ver.major}.{ver.minor:02d}.{ver.patch:02d}/platform-espressif32.zip" + release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" + if ver.extra: + release += f"-{ver.extra}" + return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip" # if platform version is a valid version constraint, prefix the default package cv.platformio_version_constraint(value) return f"platformio/espressif32@{value}" diff --git a/esphome/components/esp32/post_build.py.script b/esphome/components/esp32/post_build.py.script index 6e0e439011..586f12e00b 100644 --- a/esphome/components/esp32/post_build.py.script +++ b/esphome/components/esp32/post_build.py.script @@ -1,10 +1,11 @@ -Import("env") +Import("env") # noqa: F821 + +import itertools # noqa: E402 +import json # noqa: E402 +import os # noqa: E402 +import pathlib # noqa: E402 +import shutil # noqa: E402 -import os -import json -import shutil -import pathlib -import itertools def merge_factory_bin(source, target, env): """ @@ -25,7 +26,9 @@ def merge_factory_bin(source, target, env): try: with flasher_args_path.open() as f: flash_data = json.load(f) - for addr, fname in sorted(flash_data["flash_files"].items(), key=lambda kv: int(kv[0], 16)): + for addr, fname in sorted( + flash_data["flash_files"].items(), key=lambda kv: int(kv[0], 16) + ): file_path = pathlib.Path(fname) if file_path.exists(): sections.append((addr, str(file_path))) @@ -40,20 +43,27 @@ def merge_factory_bin(source, target, env): if flash_images: print("Using FLASH_EXTRA_IMAGES from PlatformIO environment") # flatten any nested lists - flat = list(itertools.chain.from_iterable( - x if isinstance(x, (list, tuple)) else [x] for x in flash_images - )) + flat = list( + itertools.chain.from_iterable( + x if isinstance(x, (list, tuple)) else [x] for x in flash_images + ) + ) entries = [env.subst(x) for x in flat] for i in range(0, len(entries) - 1, 2): addr, fname = entries[i], entries[i + 1] if isinstance(fname, (list, tuple)): - print(f"Warning: Skipping malformed FLASH_EXTRA_IMAGES entry: {fname}") + print( + f"Warning: Skipping malformed FLASH_EXTRA_IMAGES entry: {fname}" + ) continue file_path = pathlib.Path(str(fname)) if file_path.exists(): - sections.append((addr, str(file_path))) + sections.append((addr, file_path)) else: print(f"Info: {file_path.name} not found — skipping") + if sections: + # Append main firmware to sections + sections.append(("0x10000", firmware_path)) # 3. Final fallback: guess standard image locations if not sections: @@ -62,11 +72,11 @@ def merge_factory_bin(source, target, env): ("0x0", build_dir / "bootloader" / "bootloader.bin"), ("0x8000", build_dir / "partition_table" / "partition-table.bin"), ("0xe000", build_dir / "ota_data_initial.bin"), - ("0x10000", firmware_path) + ("0x10000", firmware_path), ] for addr, file_path in guesses: if file_path.exists(): - sections.append((addr, str(file_path))) + sections.append((addr, file_path)) else: print(f"Info: {file_path.name} not found — skipping") @@ -76,21 +86,25 @@ def merge_factory_bin(source, target, env): return output_path = firmware_path.with_suffix(".factory.bin") + python_exe = f'"{env.subst("$PYTHONEXE")}"' cmd = [ - "--chip", chip, + python_exe, + "-m", + "esptool", + "--chip", + chip, "merge_bin", - "--flash_size", flash_size, - "--output", str(output_path) + "--flash_size", + flash_size, + "--output", + str(output_path), ] for addr, file_path in sections: - cmd += [addr, file_path] + cmd += [addr, str(file_path)] print(f"Merging binaries into {output_path}") result = env.Execute( - env.VerboseAction( - f"{env.subst('$PYTHONEXE')} -m esptool " + " ".join(cmd), - "Merging binaries with esptool" - ) + env.VerboseAction(" ".join(cmd), "Merging binaries with esptool") ) if result == 0: @@ -98,6 +112,7 @@ def merge_factory_bin(source, target, env): else: print(f"Error: esptool merge_bin failed with code {result}") + def esp32_copy_ota_bin(source, target, env): """ Copy the main firmware to a .ota.bin file for compatibility with ESPHome OTA tools. @@ -107,6 +122,7 @@ def esp32_copy_ota_bin(source, target, env): shutil.copyfile(firmware_name, new_file_name) print(f"Copied firmware to {new_file_name}") + # Run merge first, then ota copy second -env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) -env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821 +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa: F821 diff --git a/esphome/components/esp32_touch/esp32_touch_v1.cpp b/esphome/components/esp32_touch/esp32_touch_v1.cpp index 629dc8e793..d41f1615c8 100644 --- a/esphome/components/esp32_touch/esp32_touch_v1.cpp +++ b/esphome/components/esp32_touch/esp32_touch_v1.cpp @@ -201,15 +201,13 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { touch_pad_t pad = child->get_touch_pad(); // Read current value using ISR-safe API - uint32_t value; - if (component->iir_filter_enabled_()) { - uint16_t temp_value = 0; - touch_pad_read_filtered(pad, &temp_value); - value = temp_value; - } else { - // Use low-level HAL function when filter is not enabled - value = touch_ll_read_raw_data(pad); - } + // IMPORTANT: ESP-IDF v5.4 regression - touch_pad_read_filtered() is no longer ISR-safe + // In v5.3 and earlier it was ISR-safe, but v5.4 added mutex protection that causes: + // "assert failed: xQueueSemaphoreTake queue.c:1718" + // We must use raw values even when filter is enabled as a workaround. + // Users should adjust thresholds to compensate for the lack of IIR filtering. + // See: https://github.com/espressif/esp-idf/issues/17045 + uint32_t value = touch_ll_read_raw_data(pad); // Skip pads that aren’t in the trigger mask if (((mask >> pad) & 1) == 0) { diff --git a/esphome/components/gps/gps.cpp b/esphome/components/gps/gps.cpp index cbbd36887b..65cddcd984 100644 --- a/esphome/components/gps/gps.cpp +++ b/esphome/components/gps/gps.cpp @@ -52,7 +52,7 @@ void GPS::update() { void GPS::loop() { while (this->available() > 0 && !this->has_time_) { if (!this->tiny_gps_.encode(this->read())) { - return; + continue; } if (this->tiny_gps_.location.isUpdated()) { this->latitude_ = this->tiny_gps_.location.lat(); diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 0f9f146ae9..4f83bf2435 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -126,6 +126,6 @@ async def to_code(config): cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) - cg.add_library("tonia/HeatpumpIR", "1.0.35") - if CORE.is_libretiny: + cg.add_library("tonia/HeatpumpIR", "1.0.37") + if CORE.is_libretiny or CORE.is_esp32: CORE.add_platformio_option("lib_ignore", "IRremoteESP8266") diff --git a/esphome/components/lvgl/widgets/tileview.py b/esphome/components/lvgl/widgets/tileview.py index 3865d404e2..5e3a95f017 100644 --- a/esphome/components/lvgl/widgets/tileview.py +++ b/esphome/components/lvgl/widgets/tileview.py @@ -15,7 +15,7 @@ from ..defines import ( TILE_DIRECTIONS, literal, ) -from ..lv_validation import animated, lv_int +from ..lv_validation import animated, lv_int, lv_pct from ..lvcode import lv, lv_assign, lv_expr, lv_obj, lv_Pvariable from ..schemas import container_schema from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr @@ -41,8 +41,8 @@ TILEVIEW_SCHEMA = cv.Schema( container_schema( obj_spec, { - cv.Required(CONF_ROW): lv_int, - cv.Required(CONF_COLUMN): lv_int, + cv.Required(CONF_ROW): cv.positive_int, + cv.Required(CONF_COLUMN): cv.positive_int, cv.GenerateID(): cv.declare_id(lv_tile_t), cv.Optional(CONF_DIR, default="ALL"): TILE_DIRECTIONS.several_of, }, @@ -63,21 +63,29 @@ class TileviewType(WidgetType): ) async def to_code(self, w: Widget, config: dict): - for tile_conf in config.get(CONF_TILES, ()): + tiles = config[CONF_TILES] + for tile_conf in tiles: w_id = tile_conf[CONF_ID] tile_obj = lv_Pvariable(lv_obj_t, w_id) tile = Widget.create(w_id, tile_obj, tile_spec, tile_conf) dirs = tile_conf[CONF_DIR] if isinstance(dirs, list): dirs = "|".join(dirs) + row_pos = tile_conf[CONF_ROW] + col_pos = tile_conf[CONF_COLUMN] lv_assign( tile_obj, - lv_expr.tileview_add_tile( - w.obj, tile_conf[CONF_COLUMN], tile_conf[CONF_ROW], literal(dirs) - ), + lv_expr.tileview_add_tile(w.obj, col_pos, row_pos, literal(dirs)), ) + # Bugfix for LVGL 8.x + lv_obj.set_pos(tile_obj, lv_pct(col_pos * 100), lv_pct(row_pos * 100)) await set_obj_properties(tile, tile_conf) await add_widgets(tile, tile_conf) + if tiles: + # Set the first tile as active + lv_obj.set_tile_id( + w.obj, tiles[0][CONF_COLUMN], tiles[0][CONF_ROW], literal("LV_ANIM_OFF") + ) tileview_spec = TileviewType() diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 800128745c..322ff43238 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -1,6 +1,9 @@ #include "esphome/core/defines.h" #ifdef USE_OPENTHREAD #include "openthread.h" +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) +#include "esp_openthread.h" +#endif #include @@ -28,18 +31,6 @@ OpenThreadComponent *global_openthread_component = // NOLINT(cppcoreguidelines- OpenThreadComponent::OpenThreadComponent() { global_openthread_component = this; } -OpenThreadComponent::~OpenThreadComponent() { - auto lock = InstanceLock::try_acquire(100); - if (!lock) { - ESP_LOGW(TAG, "Failed to acquire OpenThread lock in destructor, leaking memory"); - return; - } - otInstance *instance = lock->get_instance(); - otSrpClientClearHostAndServices(instance); - otSrpClientBuffersFreeAllServices(instance); - global_openthread_component = nullptr; -} - bool OpenThreadComponent::is_connected() { auto lock = InstanceLock::try_acquire(100); if (!lock) { @@ -199,6 +190,33 @@ void *OpenThreadSrpComponent::pool_alloc_(size_t size) { void OpenThreadSrpComponent::set_mdns(esphome::mdns::MDNSComponent *mdns) { this->mdns_ = mdns; } +bool OpenThreadComponent::teardown() { + if (!this->teardown_started_) { + this->teardown_started_ = true; + ESP_LOGD(TAG, "Clear Srp"); + auto lock = InstanceLock::try_acquire(100); + if (!lock) { + ESP_LOGW(TAG, "Failed to acquire OpenThread lock during teardown, leaking memory"); + return true; + } + otInstance *instance = lock->get_instance(); + otSrpClientClearHostAndServices(instance); + otSrpClientBuffersFreeAllServices(instance); + global_openthread_component = nullptr; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + ESP_LOGD(TAG, "Exit main loop "); + int error = esp_openthread_mainloop_exit(); + if (error != ESP_OK) { + ESP_LOGW(TAG, "Failed attempt to stop main loop %d", error); + this->teardown_complete_ = true; + } +#else + this->teardown_complete_ = true; +#endif + } + return this->teardown_complete_; +} + } // namespace openthread } // namespace esphome diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 77fd58851a..a0ea1b3f3a 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -21,6 +21,7 @@ class OpenThreadComponent : public Component { OpenThreadComponent(); ~OpenThreadComponent(); void setup() override; + bool teardown() override; float get_setup_priority() const override { return setup_priority::WIFI; } bool is_connected(); @@ -30,6 +31,8 @@ class OpenThreadComponent : public Component { protected: std::optional get_omr_address_(InstanceLock &lock); + bool teardown_started_{false}; + bool teardown_complete_{false}; }; extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index f495027172..b11b7ad34a 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -143,10 +143,13 @@ void OpenThreadComponent::ot_main() { esp_openthread_launch_mainloop(); // Clean up + esp_openthread_deinit(); esp_openthread_netif_glue_deinit(); esp_netif_destroy(openthread_netif); esp_vfs_eventfd_unregister(); + this->teardown_complete_ = true; + vTaskDelete(NULL); } network::IPAddresses OpenThreadComponent::get_ip_addresses() { diff --git a/esphome/components/output/__init__.py b/esphome/components/output/__init__.py index 78bfa045e1..bde106b085 100644 --- a/esphome/components/output/__init__.py +++ b/esphome/components/output/__init__.py @@ -43,6 +43,8 @@ FloatOutputPtr = FloatOutput.operator("ptr") TurnOffAction = output_ns.class_("TurnOffAction", automation.Action) TurnOnAction = output_ns.class_("TurnOnAction", automation.Action) SetLevelAction = output_ns.class_("SetLevelAction", automation.Action) +SetMinPowerAction = output_ns.class_("SetMinPowerAction", automation.Action) +SetMaxPowerAction = output_ns.class_("SetMaxPowerAction", automation.Action) async def setup_output_platform_(obj, config): @@ -104,6 +106,42 @@ async def output_set_level_to_code(config, action_id, template_arg, args): return var +@automation.register_action( + "output.set_min_power", + SetMinPowerAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(FloatOutput), + cv.Required(CONF_MIN_POWER): cv.templatable(cv.percentage), + } + ), +) +async def output_set_min_power_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_MIN_POWER], args, float) + cg.add(var.set_min_power(template_)) + return var + + +@automation.register_action( + "output.set_max_power", + SetMaxPowerAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(FloatOutput), + cv.Required(CONF_MAX_POWER): cv.templatable(cv.percentage), + } + ), +) +async def output_set_max_power_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_MAX_POWER], args, float) + cg.add(var.set_max_power(template_)) + return var + + async def to_code(config): cg.add_define("USE_OUTPUT") cg.add_global(output_ns.using) diff --git a/esphome/components/output/automation.h b/esphome/components/output/automation.h index 51c2849702..de84bb91ca 100644 --- a/esphome/components/output/automation.h +++ b/esphome/components/output/automation.h @@ -40,5 +40,29 @@ template class SetLevelAction : public Action { FloatOutput *output_; }; +template class SetMinPowerAction : public Action { + public: + SetMinPowerAction(FloatOutput *output) : output_(output) {} + + TEMPLATABLE_VALUE(float, min_power) + + void play(Ts... x) override { this->output_->set_min_power(this->min_power_.value(x...)); } + + protected: + FloatOutput *output_; +}; + +template class SetMaxPowerAction : public Action { + public: + SetMaxPowerAction(FloatOutput *output) : output_(output) {} + + TEMPLATABLE_VALUE(float, max_power) + + void play(Ts... x) override { this->output_->set_max_power(this->max_power_.value(x...)); } + + protected: + FloatOutput *output_; +}; + } // namespace output } // namespace esphome diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index bcde623df2..20c6911d28 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -332,6 +332,7 @@ def sensor_schema( device_class: str = cv.UNDEFINED, state_class: str = cv.UNDEFINED, entity_category: str = cv.UNDEFINED, + filters: list = cv.UNDEFINED, ) -> cv.Schema: schema = {} @@ -346,6 +347,7 @@ def sensor_schema( (CONF_DEVICE_CLASS, device_class, validate_device_class), (CONF_STATE_CLASS, state_class, validate_state_class), (CONF_ENTITY_CATEGORY, entity_category, sensor_entity_category), + (CONF_FILTERS, filters, validate_filters), ]: if default is not cv.UNDEFINED: schema[cv.Optional(key, default=default)] = validator diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index abb2dcae6c..0341ab2f71 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -162,6 +162,7 @@ def text_sensor_schema( device_class: str = cv.UNDEFINED, entity_category: str = cv.UNDEFINED, icon: str = cv.UNDEFINED, + filters: list = cv.UNDEFINED, ) -> cv.Schema: schema = {} @@ -172,6 +173,7 @@ def text_sensor_schema( (CONF_ICON, icon, cv.icon), (CONF_DEVICE_CLASS, device_class, validate_device_class), (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + (CONF_FILTERS, filters, validate_filters), ]: if default is not cv.UNDEFINED: schema[cv.Optional(key, default=default)] = validator diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 1a4976e235..a79f8cd17c 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -291,6 +291,8 @@ class Version: extra: str = "" def __str__(self): + if self.extra: + return f"{self.major}.{self.minor}.{self.patch}-{self.extra}" return f"{self.major}.{self.minor}.{self.patch}" @classmethod diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index 8fb966e3b2..7220fb307f 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -225,9 +225,10 @@ class _Schema(vol.Schema): return ret schema = schemas[0] + extra_schemas = self._extra_schemas.copy() + if isinstance(schema, _Schema): + extra_schemas.extend(schema._extra_schemas) if isinstance(schema, vol.Schema): schema = schema.schema ret = super().extend(schema, extra=extra) - return _Schema( - ret.schema, extra=ret.extra, extra_schemas=self._extra_schemas.copy() - ) + return _Schema(ret.schema, extra=ret.extra, extra_schemas=extra_schemas) diff --git a/platformio.ini b/platformio.ini index bf0754ead3..ab0774b29f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -78,7 +78,7 @@ lib_deps = glmnet/Dsmr@0.7 ; dsmr rweather/Crypto@0.4.0 ; dsmr dudanov/MideaUART@1.1.9 ; midea - tonia/HeatpumpIR@1.0.35 ; heatpumpir + tonia/HeatpumpIR@1.0.37 ; heatpumpir build_flags = ${common.build_flags} -DUSE_ARDUINO @@ -125,7 +125,7 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script ; This are common settings for the ESP32 (all variants) using Arduino. [common:esp32-arduino] extends = common:arduino -platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21-1/platform-espressif32.zip platform_packages = pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.2.1/esp32-3.2.1.zip @@ -161,7 +161,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script ; This are common settings for the ESP32 (all variants) using IDF. [common:esp32-idf] extends = common:idf -platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21-1/platform-espressif32.zip platform_packages = pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.4.2/esp-idf-v5.4.2.zip diff --git a/requirements.txt b/requirements.txt index 80bd470f02..9889e83910 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.9.0 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==37.1.2 +aioesphomeapi==37.1.3 zeroconf==0.147.0 puremagic==1.30 ruamel.yaml==0.18.14 # dashboard_import diff --git a/tests/component_tests/config_validation/test_config.py b/tests/component_tests/config_validation/test_config.py new file mode 100644 index 0000000000..1a9b9bc1f3 --- /dev/null +++ b/tests/component_tests/config_validation/test_config.py @@ -0,0 +1,51 @@ +""" +Test schema.extend functionality in esphome.config_validation. +""" + +from typing import Any + +import esphome.config_validation as cv + + +def test_config_extend() -> None: + """Test that schema.extend correctly merges schemas with extras.""" + + def func1(data: dict[str, Any]) -> dict[str, Any]: + data["extra_1"] = "value1" + return data + + def func2(data: dict[str, Any]) -> dict[str, Any]: + data["extra_2"] = "value2" + return data + + schema1 = cv.Schema( + { + cv.Required("key1"): cv.string, + } + ) + schema1.add_extra(func1) + schema2 = cv.Schema( + { + cv.Required("key2"): cv.string, + } + ) + schema2.add_extra(func2) + extended_schema = schema1.extend(schema2) + config = { + "key1": "initial_value1", + "key2": "initial_value2", + } + validated = extended_schema(config) + assert validated["key1"] == "initial_value1" + assert validated["key2"] == "initial_value2" + assert validated["extra_1"] == "value1" + assert validated["extra_2"] == "value2" + + # Check the opposite order of extension + extended_schema = schema2.extend(schema1) + + validated = extended_schema(config) + assert validated["key1"] == "initial_value1" + assert validated["key2"] == "initial_value2" + assert validated["extra_1"] == "value1" + assert validated["extra_2"] == "value2" diff --git a/tests/components/adc/test.esp32-p4-idf.yaml b/tests/components/adc/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..97844cf398 --- /dev/null +++ b/tests/components/adc/test.esp32-p4-idf.yaml @@ -0,0 +1,6 @@ +packages: + base: !include common.yaml + +sensor: + - id: !extend my_sensor + pin: GPIO50 diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 46341c266d..853466c9cc 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -738,7 +738,7 @@ lvgl: id: bar_id value: !lambda return (int)((float)rand() / RAND_MAX * 100); start_value: !lambda return (int)((float)rand() / RAND_MAX * 100); - mode: symmetrical + mode: range - logger.log: format: "bar value %f" args: [x] diff --git a/tests/components/output/common.yaml b/tests/components/output/common.yaml index 5f31ae50a8..81d802e9bf 100644 --- a/tests/components/output/common.yaml +++ b/tests/components/output/common.yaml @@ -6,6 +6,12 @@ esphome: - output.set_level: id: light_output_1 level: 50% + - output.set_min_power: + id: light_output_1 + min_power: 20% + - output.set_max_power: + id: light_output_1 + max_power: 80% output: - platform: ${output_platform}