mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'integration' into memory_api
This commit is contained in:
		| @@ -1 +1 @@ | |||||||
| 32b0db73b3ae01ba18c9cbb1dabbd8156bc14dded500471919bd0a3dc33916e0 | f84518ea4140c194b21cc516aae05aaa0cf876ab866f89e22e91842df46333ed | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| from esphome import pins | from esphome import pins | ||||||
| import esphome.codegen as cg | 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 ( | from esphome.components.esp32.const import ( | ||||||
|     VARIANT_ESP32, |     VARIANT_ESP32, | ||||||
|     VARIANT_ESP32C2, |     VARIANT_ESP32C2, | ||||||
| @@ -140,6 +140,16 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { | |||||||
|         9: adc_channel_t.ADC_CHANNEL_8, |         9: adc_channel_t.ADC_CHANNEL_8, | ||||||
|         10: adc_channel_t.ADC_CHANNEL_9, |         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 | # pin to adc2 channel mapping | ||||||
| @@ -198,6 +208,14 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { | |||||||
|         19: adc_channel_t.ADC_CHANNEL_8, |         19: adc_channel_t.ADC_CHANNEL_8, | ||||||
|         20: adc_channel_t.ADC_CHANNEL_9, |         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, | ||||||
|  |     }, | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -136,8 +136,8 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage | |||||||
|   adc_oneshot_unit_handle_t adc_handle_{nullptr}; |   adc_oneshot_unit_handle_t adc_handle_{nullptr}; | ||||||
|   adc_cali_handle_t calibration_handle_{nullptr}; |   adc_cali_handle_t calibration_handle_{nullptr}; | ||||||
|   adc_atten_t attenuation_{ADC_ATTEN_DB_0}; |   adc_atten_t attenuation_{ADC_ATTEN_DB_0}; | ||||||
|   adc_channel_t channel_; |   adc_channel_t channel_{}; | ||||||
|   adc_unit_t adc_unit_; |   adc_unit_t adc_unit_{}; | ||||||
|   struct SetupFlags { |   struct SetupFlags { | ||||||
|     uint8_t init_complete : 1; |     uint8_t init_complete : 1; | ||||||
|     uint8_t config_complete : 1; |     uint8_t config_complete : 1; | ||||||
|   | |||||||
| @@ -72,10 +72,9 @@ void ADCSensor::setup() { | |||||||
|   // Initialize ADC calibration |   // Initialize ADC calibration | ||||||
|   if (this->calibration_handle_ == nullptr) { |   if (this->calibration_handle_ == nullptr) { | ||||||
|     adc_cali_handle_t 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 || \ | #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 |     // RISC-V variants and S3 use curve fitting calibration | ||||||
|     adc_cali_curve_fitting_config_t cali_config = {};  // Zero initialize first |     adc_cali_curve_fitting_config_t cali_config = {};  // Zero initialize first | ||||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) | #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); |       ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err); | ||||||
|       if (this->calibration_handle_ != nullptr) { |       if (this->calibration_handle_ != nullptr) { | ||||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ | #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_); |         adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); | ||||||
| #else   // Other ESP32 variants use line fitting calibration | #else   // Other ESP32 variants use line fitting calibration | ||||||
|         adc_cali_delete_scheme_line_fitting(this->calibration_handle_); |         adc_cali_delete_scheme_line_fitting(this->calibration_handle_); | ||||||
| @@ -220,7 +219,7 @@ float ADCSensor::sample_autorange_() { | |||||||
|     if (this->calibration_handle_ != nullptr) { |     if (this->calibration_handle_ != nullptr) { | ||||||
|       // Delete old calibration handle |       // Delete old calibration handle | ||||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ | #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_); |       adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); | ||||||
| #else | #else | ||||||
|       adc_cali_delete_scheme_line_fitting(this->calibration_handle_); |       adc_cali_delete_scheme_line_fitting(this->calibration_handle_); | ||||||
| @@ -232,7 +231,7 @@ float ADCSensor::sample_autorange_() { | |||||||
|     adc_cali_handle_t handle = nullptr; |     adc_cali_handle_t handle = nullptr; | ||||||
|  |  | ||||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ | #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 = {}; |     adc_cali_curve_fitting_config_t cali_config = {}; | ||||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) | #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) | ||||||
|     cali_config.chan = this->channel_; |     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); |       ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); | ||||||
|       if (handle != nullptr) { |       if (handle != nullptr) { | ||||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ | #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); |         adc_cali_delete_scheme_curve_fitting(handle); | ||||||
| #else | #else | ||||||
|         adc_cali_delete_scheme_line_fitting(handle); |         adc_cali_delete_scheme_line_fitting(handle); | ||||||
| @@ -281,7 +280,7 @@ float ADCSensor::sample_autorange_() { | |||||||
|       } |       } | ||||||
|       // Clean up calibration handle |       // Clean up calibration handle | ||||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ | #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); |       adc_cali_delete_scheme_curve_fitting(handle); | ||||||
| #else | #else | ||||||
|       adc_cali_delete_scheme_line_fitting(handle); |       adc_cali_delete_scheme_line_fitting(handle); | ||||||
|   | |||||||
| @@ -300,7 +300,7 @@ class APIConnection : public APIServerConnection { | |||||||
|  |  | ||||||
| #ifdef USE_API_HOMEASSISTANT_STATES | #ifdef USE_API_HOMEASSISTANT_STATES | ||||||
|   void process_state_subscriptions_(); |   void process_state_subscriptions_(); | ||||||
| #endif  // USE_API_HOMEASSISTANT_STATES | #endif | ||||||
|  |  | ||||||
|   // Non-template helper to encode any ProtoMessage |   // Non-template helper to encode any ProtoMessage | ||||||
|   static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, |   static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, | ||||||
|   | |||||||
| @@ -516,6 +516,7 @@ def binary_sensor_schema( | |||||||
|     icon: str = cv.UNDEFINED, |     icon: str = cv.UNDEFINED, | ||||||
|     entity_category: str = cv.UNDEFINED, |     entity_category: str = cv.UNDEFINED, | ||||||
|     device_class: str = cv.UNDEFINED, |     device_class: str = cv.UNDEFINED, | ||||||
|  |     filters: list = cv.UNDEFINED, | ||||||
| ) -> cv.Schema: | ) -> cv.Schema: | ||||||
|     schema = {} |     schema = {} | ||||||
|  |  | ||||||
| @@ -527,6 +528,7 @@ def binary_sensor_schema( | |||||||
|         (CONF_ICON, icon, cv.icon), |         (CONF_ICON, icon, cv.icon), | ||||||
|         (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), |         (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), | ||||||
|         (CONF_DEVICE_CLASS, device_class, validate_device_class), |         (CONF_DEVICE_CLASS, device_class, validate_device_class), | ||||||
|  |         (CONF_FILTERS, filters, validate_filters), | ||||||
|     ]: |     ]: | ||||||
|         if default is not cv.UNDEFINED: |         if default is not cv.UNDEFINED: | ||||||
|             schema[cv.Optional(key, default=default)] = validator |             schema[cv.Optional(key, default=default)] = validator | ||||||
|   | |||||||
| @@ -313,7 +313,7 @@ def _format_framework_espidf_version( | |||||||
| RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1) | RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1) | ||||||
| # The platform-espressif32 version to use for arduino frameworks | # The platform-espressif32 version to use for arduino frameworks | ||||||
| #  - https://github.com/pioarduino/platform-espressif32/releases | #  - 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 | # The default/recommended esp-idf framework version | ||||||
| #  - https://github.com/espressif/esp-idf/releases | #  - 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 | # The platformio/espressif32 version to use for esp-idf frameworks | ||||||
| #  - https://github.com/platformio/platform-espressif32/releases | #  - https://github.com/platformio/platform-espressif32/releases | ||||||
| #  - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 | #  - 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 | # List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions | ||||||
| SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ | SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ | ||||||
| @@ -468,10 +468,10 @@ def _parse_platform_version(value): | |||||||
|     try: |     try: | ||||||
|         ver = cv.Version.parse(cv.version_number(value)) |         ver = cv.Version.parse(cv.version_number(value)) | ||||||
|         if ver.major >= 50:  # a pioarduino version |         if ver.major >= 50:  # a pioarduino version | ||||||
|             if "-" in value: |             release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" | ||||||
|                 # maybe a release candidate?...definitely not our default, just use it as-is... |             if ver.extra: | ||||||
|                 return f"https://github.com/pioarduino/platform-espressif32/releases/download/{value}/platform-espressif32.zip" |                 release += f"-{ver.extra}" | ||||||
|             return f"https://github.com/pioarduino/platform-espressif32/releases/download/{ver.major}.{ver.minor:02d}.{ver.patch:02d}/platform-espressif32.zip" |             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 |         # if platform version is a valid version constraint, prefix the default package | ||||||
|         cv.platformio_version_constraint(value) |         cv.platformio_version_constraint(value) | ||||||
|         return f"platformio/espressif32@{value}" |         return f"platformio/espressif32@{value}" | ||||||
|   | |||||||
| @@ -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): | def merge_factory_bin(source, target, env): | ||||||
|     """ |     """ | ||||||
| @@ -25,7 +26,9 @@ def merge_factory_bin(source, target, env): | |||||||
|         try: |         try: | ||||||
|             with flasher_args_path.open() as f: |             with flasher_args_path.open() as f: | ||||||
|                 flash_data = json.load(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) |                 file_path = pathlib.Path(fname) | ||||||
|                 if file_path.exists(): |                 if file_path.exists(): | ||||||
|                     sections.append((addr, str(file_path))) |                     sections.append((addr, str(file_path))) | ||||||
| @@ -40,20 +43,27 @@ def merge_factory_bin(source, target, env): | |||||||
|         if flash_images: |         if flash_images: | ||||||
|             print("Using FLASH_EXTRA_IMAGES from PlatformIO environment") |             print("Using FLASH_EXTRA_IMAGES from PlatformIO environment") | ||||||
|             # flatten any nested lists |             # flatten any nested lists | ||||||
|             flat = list(itertools.chain.from_iterable( |             flat = list( | ||||||
|                 x if isinstance(x, (list, tuple)) else [x] for x in flash_images |                 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] |             entries = [env.subst(x) for x in flat] | ||||||
|             for i in range(0, len(entries) - 1, 2): |             for i in range(0, len(entries) - 1, 2): | ||||||
|                 addr, fname = entries[i], entries[i + 1] |                 addr, fname = entries[i], entries[i + 1] | ||||||
|                 if isinstance(fname, (list, tuple)): |                 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 |                     continue | ||||||
|                 file_path = pathlib.Path(str(fname)) |                 file_path = pathlib.Path(str(fname)) | ||||||
|                 if file_path.exists(): |                 if file_path.exists(): | ||||||
|                     sections.append((addr, str(file_path))) |                     sections.append((addr, file_path)) | ||||||
|                 else: |                 else: | ||||||
|                     print(f"Info: {file_path.name} not found — skipping") |                     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 |     # 3. Final fallback: guess standard image locations | ||||||
|     if not sections: |     if not sections: | ||||||
| @@ -62,11 +72,11 @@ def merge_factory_bin(source, target, env): | |||||||
|             ("0x0", build_dir / "bootloader" / "bootloader.bin"), |             ("0x0", build_dir / "bootloader" / "bootloader.bin"), | ||||||
|             ("0x8000", build_dir / "partition_table" / "partition-table.bin"), |             ("0x8000", build_dir / "partition_table" / "partition-table.bin"), | ||||||
|             ("0xe000", build_dir / "ota_data_initial.bin"), |             ("0xe000", build_dir / "ota_data_initial.bin"), | ||||||
|             ("0x10000", firmware_path) |             ("0x10000", firmware_path), | ||||||
|         ] |         ] | ||||||
|         for addr, file_path in guesses: |         for addr, file_path in guesses: | ||||||
|             if file_path.exists(): |             if file_path.exists(): | ||||||
|                 sections.append((addr, str(file_path))) |                 sections.append((addr, file_path)) | ||||||
|             else: |             else: | ||||||
|                 print(f"Info: {file_path.name} not found — skipping") |                 print(f"Info: {file_path.name} not found — skipping") | ||||||
|  |  | ||||||
| @@ -76,21 +86,25 @@ def merge_factory_bin(source, target, env): | |||||||
|         return |         return | ||||||
|  |  | ||||||
|     output_path = firmware_path.with_suffix(".factory.bin") |     output_path = firmware_path.with_suffix(".factory.bin") | ||||||
|  |     python_exe = f'"{env.subst("$PYTHONEXE")}"' | ||||||
|     cmd = [ |     cmd = [ | ||||||
|         "--chip", chip, |         python_exe, | ||||||
|  |         "-m", | ||||||
|  |         "esptool", | ||||||
|  |         "--chip", | ||||||
|  |         chip, | ||||||
|         "merge_bin", |         "merge_bin", | ||||||
|         "--flash_size", flash_size, |         "--flash_size", | ||||||
|         "--output", str(output_path) |         flash_size, | ||||||
|  |         "--output", | ||||||
|  |         str(output_path), | ||||||
|     ] |     ] | ||||||
|     for addr, file_path in sections: |     for addr, file_path in sections: | ||||||
|         cmd += [addr, file_path] |         cmd += [addr, str(file_path)] | ||||||
|  |  | ||||||
|     print(f"Merging binaries into {output_path}") |     print(f"Merging binaries into {output_path}") | ||||||
|     result = env.Execute( |     result = env.Execute( | ||||||
|         env.VerboseAction( |         env.VerboseAction(" ".join(cmd), "Merging binaries with esptool") | ||||||
|             f"{env.subst('$PYTHONEXE')} -m esptool " + " ".join(cmd), |  | ||||||
|             "Merging binaries with esptool" |  | ||||||
|         ) |  | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     if result == 0: |     if result == 0: | ||||||
| @@ -98,6 +112,7 @@ def merge_factory_bin(source, target, env): | |||||||
|     else: |     else: | ||||||
|         print(f"Error: esptool merge_bin failed with code {result}") |         print(f"Error: esptool merge_bin failed with code {result}") | ||||||
|  |  | ||||||
|  |  | ||||||
| def esp32_copy_ota_bin(source, target, env): | def esp32_copy_ota_bin(source, target, env): | ||||||
|     """ |     """ | ||||||
|     Copy the main firmware to a .ota.bin file for compatibility with ESPHome OTA tools. |     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) |     shutil.copyfile(firmware_name, new_file_name) | ||||||
|     print(f"Copied firmware to {new_file_name}") |     print(f"Copied firmware to {new_file_name}") | ||||||
|  |  | ||||||
|  |  | ||||||
| # Run merge first, then ota copy second | # Run merge first, then ota copy second | ||||||
| env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) | env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin)  # noqa: F821 | ||||||
| env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) | env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin)  # noqa: F821 | ||||||
|   | |||||||
| @@ -201,15 +201,13 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { | |||||||
|     touch_pad_t pad = child->get_touch_pad(); |     touch_pad_t pad = child->get_touch_pad(); | ||||||
|  |  | ||||||
|     // Read current value using ISR-safe API |     // Read current value using ISR-safe API | ||||||
|     uint32_t value; |     // IMPORTANT: ESP-IDF v5.4 regression - touch_pad_read_filtered() is no longer ISR-safe | ||||||
|     if (component->iir_filter_enabled_()) { |     // In v5.3 and earlier it was ISR-safe, but v5.4 added mutex protection that causes: | ||||||
|       uint16_t temp_value = 0; |     // "assert failed: xQueueSemaphoreTake queue.c:1718" | ||||||
|       touch_pad_read_filtered(pad, &temp_value); |     // We must use raw values even when filter is enabled as a workaround. | ||||||
|       value = temp_value; |     // Users should adjust thresholds to compensate for the lack of IIR filtering. | ||||||
|     } else { |     // See: https://github.com/espressif/esp-idf/issues/17045 | ||||||
|       // Use low-level HAL function when filter is not enabled |     uint32_t value = touch_ll_read_raw_data(pad); | ||||||
|       value = touch_ll_read_raw_data(pad); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Skip pads that aren’t in the trigger mask |     // Skip pads that aren’t in the trigger mask | ||||||
|     if (((mask >> pad) & 1) == 0) { |     if (((mask >> pad) & 1) == 0) { | ||||||
|   | |||||||
| @@ -52,7 +52,7 @@ void GPS::update() { | |||||||
| void GPS::loop() { | void GPS::loop() { | ||||||
|   while (this->available() > 0 && !this->has_time_) { |   while (this->available() > 0 && !this->has_time_) { | ||||||
|     if (!this->tiny_gps_.encode(this->read())) { |     if (!this->tiny_gps_.encode(this->read())) { | ||||||
|       return; |       continue; | ||||||
|     } |     } | ||||||
|     if (this->tiny_gps_.location.isUpdated()) { |     if (this->tiny_gps_.location.isUpdated()) { | ||||||
|       this->latitude_ = this->tiny_gps_.location.lat(); |       this->latitude_ = this->tiny_gps_.location.lat(); | ||||||
|   | |||||||
| @@ -126,6 +126,6 @@ async def to_code(config): | |||||||
|     cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) |     cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) | ||||||
|     cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) |     cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) | ||||||
|  |  | ||||||
|     cg.add_library("tonia/HeatpumpIR", "1.0.35") |     cg.add_library("tonia/HeatpumpIR", "1.0.37") | ||||||
|     if CORE.is_libretiny: |     if CORE.is_libretiny or CORE.is_esp32: | ||||||
|         CORE.add_platformio_option("lib_ignore", "IRremoteESP8266") |         CORE.add_platformio_option("lib_ignore", "IRremoteESP8266") | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ from ..defines import ( | |||||||
|     TILE_DIRECTIONS, |     TILE_DIRECTIONS, | ||||||
|     literal, |     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 ..lvcode import lv, lv_assign, lv_expr, lv_obj, lv_Pvariable | ||||||
| from ..schemas import container_schema | from ..schemas import container_schema | ||||||
| from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr | from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr | ||||||
| @@ -41,8 +41,8 @@ TILEVIEW_SCHEMA = cv.Schema( | |||||||
|             container_schema( |             container_schema( | ||||||
|                 obj_spec, |                 obj_spec, | ||||||
|                 { |                 { | ||||||
|                     cv.Required(CONF_ROW): lv_int, |                     cv.Required(CONF_ROW): cv.positive_int, | ||||||
|                     cv.Required(CONF_COLUMN): lv_int, |                     cv.Required(CONF_COLUMN): cv.positive_int, | ||||||
|                     cv.GenerateID(): cv.declare_id(lv_tile_t), |                     cv.GenerateID(): cv.declare_id(lv_tile_t), | ||||||
|                     cv.Optional(CONF_DIR, default="ALL"): TILE_DIRECTIONS.several_of, |                     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): |     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] |             w_id = tile_conf[CONF_ID] | ||||||
|             tile_obj = lv_Pvariable(lv_obj_t, w_id) |             tile_obj = lv_Pvariable(lv_obj_t, w_id) | ||||||
|             tile = Widget.create(w_id, tile_obj, tile_spec, tile_conf) |             tile = Widget.create(w_id, tile_obj, tile_spec, tile_conf) | ||||||
|             dirs = tile_conf[CONF_DIR] |             dirs = tile_conf[CONF_DIR] | ||||||
|             if isinstance(dirs, list): |             if isinstance(dirs, list): | ||||||
|                 dirs = "|".join(dirs) |                 dirs = "|".join(dirs) | ||||||
|  |             row_pos = tile_conf[CONF_ROW] | ||||||
|  |             col_pos = tile_conf[CONF_COLUMN] | ||||||
|             lv_assign( |             lv_assign( | ||||||
|                 tile_obj, |                 tile_obj, | ||||||
|                 lv_expr.tileview_add_tile( |                 lv_expr.tileview_add_tile(w.obj, col_pos, row_pos, literal(dirs)), | ||||||
|                     w.obj, tile_conf[CONF_COLUMN], tile_conf[CONF_ROW], 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 set_obj_properties(tile, tile_conf) | ||||||
|             await add_widgets(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() | tileview_spec = TileviewType() | ||||||
|   | |||||||
| @@ -1,6 +1,9 @@ | |||||||
| #include "esphome/core/defines.h" | #include "esphome/core/defines.h" | ||||||
| #ifdef USE_OPENTHREAD | #ifdef USE_OPENTHREAD | ||||||
| #include "openthread.h" | #include "openthread.h" | ||||||
|  | #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) | ||||||
|  | #include "esp_openthread.h" | ||||||
|  | #endif | ||||||
|  |  | ||||||
| #include <freertos/portmacro.h> | #include <freertos/portmacro.h> | ||||||
|  |  | ||||||
| @@ -28,18 +31,6 @@ OpenThreadComponent *global_openthread_component =  // NOLINT(cppcoreguidelines- | |||||||
|  |  | ||||||
| OpenThreadComponent::OpenThreadComponent() { global_openthread_component = this; } | 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() { | bool OpenThreadComponent::is_connected() { | ||||||
|   auto lock = InstanceLock::try_acquire(100); |   auto lock = InstanceLock::try_acquire(100); | ||||||
|   if (!lock) { |   if (!lock) { | ||||||
| @@ -199,6 +190,33 @@ void *OpenThreadSrpComponent::pool_alloc_(size_t size) { | |||||||
|  |  | ||||||
| void OpenThreadSrpComponent::set_mdns(esphome::mdns::MDNSComponent *mdns) { this->mdns_ = mdns; } | 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 openthread | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|  |  | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ class OpenThreadComponent : public Component { | |||||||
|   OpenThreadComponent(); |   OpenThreadComponent(); | ||||||
|   ~OpenThreadComponent(); |   ~OpenThreadComponent(); | ||||||
|   void setup() override; |   void setup() override; | ||||||
|  |   bool teardown() override; | ||||||
|   float get_setup_priority() const override { return setup_priority::WIFI; } |   float get_setup_priority() const override { return setup_priority::WIFI; } | ||||||
|  |  | ||||||
|   bool is_connected(); |   bool is_connected(); | ||||||
| @@ -30,6 +31,8 @@ class OpenThreadComponent : public Component { | |||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   std::optional<otIp6Address> get_omr_address_(InstanceLock &lock); |   std::optional<otIp6Address> 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) | extern OpenThreadComponent *global_openthread_component;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||||
|   | |||||||
| @@ -143,10 +143,13 @@ void OpenThreadComponent::ot_main() { | |||||||
|   esp_openthread_launch_mainloop(); |   esp_openthread_launch_mainloop(); | ||||||
|  |  | ||||||
|   // Clean up |   // Clean up | ||||||
|  |   esp_openthread_deinit(); | ||||||
|   esp_openthread_netif_glue_deinit(); |   esp_openthread_netif_glue_deinit(); | ||||||
|   esp_netif_destroy(openthread_netif); |   esp_netif_destroy(openthread_netif); | ||||||
|  |  | ||||||
|   esp_vfs_eventfd_unregister(); |   esp_vfs_eventfd_unregister(); | ||||||
|  |   this->teardown_complete_ = true; | ||||||
|  |   vTaskDelete(NULL); | ||||||
| } | } | ||||||
|  |  | ||||||
| network::IPAddresses OpenThreadComponent::get_ip_addresses() { | network::IPAddresses OpenThreadComponent::get_ip_addresses() { | ||||||
|   | |||||||
| @@ -43,6 +43,8 @@ FloatOutputPtr = FloatOutput.operator("ptr") | |||||||
| TurnOffAction = output_ns.class_("TurnOffAction", automation.Action) | TurnOffAction = output_ns.class_("TurnOffAction", automation.Action) | ||||||
| TurnOnAction = output_ns.class_("TurnOnAction", automation.Action) | TurnOnAction = output_ns.class_("TurnOnAction", automation.Action) | ||||||
| SetLevelAction = output_ns.class_("SetLevelAction", 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): | 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 |     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): | async def to_code(config): | ||||||
|     cg.add_define("USE_OUTPUT") |     cg.add_define("USE_OUTPUT") | ||||||
|     cg.add_global(output_ns.using) |     cg.add_global(output_ns.using) | ||||||
|   | |||||||
| @@ -40,5 +40,29 @@ template<typename... Ts> class SetLevelAction : public Action<Ts...> { | |||||||
|   FloatOutput *output_; |   FloatOutput *output_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class SetMinPowerAction : public Action<Ts...> { | ||||||
|  |  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<typename... Ts> class SetMaxPowerAction : public Action<Ts...> { | ||||||
|  |  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 output | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -332,6 +332,7 @@ def sensor_schema( | |||||||
|     device_class: str = cv.UNDEFINED, |     device_class: str = cv.UNDEFINED, | ||||||
|     state_class: str = cv.UNDEFINED, |     state_class: str = cv.UNDEFINED, | ||||||
|     entity_category: str = cv.UNDEFINED, |     entity_category: str = cv.UNDEFINED, | ||||||
|  |     filters: list = cv.UNDEFINED, | ||||||
| ) -> cv.Schema: | ) -> cv.Schema: | ||||||
|     schema = {} |     schema = {} | ||||||
|  |  | ||||||
| @@ -346,6 +347,7 @@ def sensor_schema( | |||||||
|         (CONF_DEVICE_CLASS, device_class, validate_device_class), |         (CONF_DEVICE_CLASS, device_class, validate_device_class), | ||||||
|         (CONF_STATE_CLASS, state_class, validate_state_class), |         (CONF_STATE_CLASS, state_class, validate_state_class), | ||||||
|         (CONF_ENTITY_CATEGORY, entity_category, sensor_entity_category), |         (CONF_ENTITY_CATEGORY, entity_category, sensor_entity_category), | ||||||
|  |         (CONF_FILTERS, filters, validate_filters), | ||||||
|     ]: |     ]: | ||||||
|         if default is not cv.UNDEFINED: |         if default is not cv.UNDEFINED: | ||||||
|             schema[cv.Optional(key, default=default)] = validator |             schema[cv.Optional(key, default=default)] = validator | ||||||
|   | |||||||
| @@ -162,6 +162,7 @@ def text_sensor_schema( | |||||||
|     device_class: str = cv.UNDEFINED, |     device_class: str = cv.UNDEFINED, | ||||||
|     entity_category: str = cv.UNDEFINED, |     entity_category: str = cv.UNDEFINED, | ||||||
|     icon: str = cv.UNDEFINED, |     icon: str = cv.UNDEFINED, | ||||||
|  |     filters: list = cv.UNDEFINED, | ||||||
| ) -> cv.Schema: | ) -> cv.Schema: | ||||||
|     schema = {} |     schema = {} | ||||||
|  |  | ||||||
| @@ -172,6 +173,7 @@ def text_sensor_schema( | |||||||
|         (CONF_ICON, icon, cv.icon), |         (CONF_ICON, icon, cv.icon), | ||||||
|         (CONF_DEVICE_CLASS, device_class, validate_device_class), |         (CONF_DEVICE_CLASS, device_class, validate_device_class), | ||||||
|         (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), |         (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), | ||||||
|  |         (CONF_FILTERS, filters, validate_filters), | ||||||
|     ]: |     ]: | ||||||
|         if default is not cv.UNDEFINED: |         if default is not cv.UNDEFINED: | ||||||
|             schema[cv.Optional(key, default=default)] = validator |             schema[cv.Optional(key, default=default)] = validator | ||||||
|   | |||||||
| @@ -291,6 +291,8 @@ class Version: | |||||||
|     extra: str = "" |     extra: str = "" | ||||||
|  |  | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|  |         if self.extra: | ||||||
|  |             return f"{self.major}.{self.minor}.{self.patch}-{self.extra}" | ||||||
|         return f"{self.major}.{self.minor}.{self.patch}" |         return f"{self.major}.{self.minor}.{self.patch}" | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|   | |||||||
| @@ -225,9 +225,10 @@ class _Schema(vol.Schema): | |||||||
|             return ret |             return ret | ||||||
|  |  | ||||||
|         schema = schemas[0] |         schema = schemas[0] | ||||||
|  |         extra_schemas = self._extra_schemas.copy() | ||||||
|  |         if isinstance(schema, _Schema): | ||||||
|  |             extra_schemas.extend(schema._extra_schemas) | ||||||
|         if isinstance(schema, vol.Schema): |         if isinstance(schema, vol.Schema): | ||||||
|             schema = schema.schema |             schema = schema.schema | ||||||
|         ret = super().extend(schema, extra=extra) |         ret = super().extend(schema, extra=extra) | ||||||
|         return _Schema( |         return _Schema(ret.schema, extra=ret.extra, extra_schemas=extra_schemas) | ||||||
|             ret.schema, extra=ret.extra, extra_schemas=self._extra_schemas.copy() |  | ||||||
|         ) |  | ||||||
|   | |||||||
| @@ -78,7 +78,7 @@ lib_deps = | |||||||
|     glmnet/Dsmr@0.7                                       ; dsmr |     glmnet/Dsmr@0.7                                       ; dsmr | ||||||
|     rweather/Crypto@0.4.0                                 ; dsmr |     rweather/Crypto@0.4.0                                 ; dsmr | ||||||
|     dudanov/MideaUART@1.1.9                               ; midea |     dudanov/MideaUART@1.1.9                               ; midea | ||||||
|     tonia/HeatpumpIR@1.0.35                               ; heatpumpir |     tonia/HeatpumpIR@1.0.37                               ; heatpumpir | ||||||
| build_flags = | build_flags = | ||||||
|     ${common.build_flags} |     ${common.build_flags} | ||||||
|     -DUSE_ARDUINO |     -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. | ; This are common settings for the ESP32 (all variants) using Arduino. | ||||||
| [common:esp32-arduino] | [common:esp32-arduino] | ||||||
| extends = common: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 = | platform_packages = | ||||||
|     pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.2.1/esp32-3.2.1.zip |     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. | ; This are common settings for the ESP32 (all variants) using IDF. | ||||||
| [common:esp32-idf] | [common:esp32-idf] | ||||||
| extends = common: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 = | platform_packages = | ||||||
|     pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.4.2/esp-idf-v5.4.2.zip |     pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.4.2/esp-idf-v5.4.2.zip | ||||||
|  |  | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ platformio==6.1.18  # When updating platformio, also update /docker/Dockerfile | |||||||
| esptool==4.9.0 | esptool==4.9.0 | ||||||
| click==8.1.7 | click==8.1.7 | ||||||
| esphome-dashboard==20250514.0 | esphome-dashboard==20250514.0 | ||||||
| aioesphomeapi==37.1.2 | aioesphomeapi==37.1.3 | ||||||
| zeroconf==0.147.0 | zeroconf==0.147.0 | ||||||
| puremagic==1.30 | puremagic==1.30 | ||||||
| ruamel.yaml==0.18.14 # dashboard_import | ruamel.yaml==0.18.14 # dashboard_import | ||||||
|   | |||||||
							
								
								
									
										51
									
								
								tests/component_tests/config_validation/test_config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								tests/component_tests/config_validation/test_config.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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" | ||||||
							
								
								
									
										6
									
								
								tests/components/adc/test.esp32-p4-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								tests/components/adc/test.esp32-p4-idf.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | packages: | ||||||
|  |   base: !include common.yaml | ||||||
|  |  | ||||||
|  | sensor: | ||||||
|  |   - id: !extend my_sensor | ||||||
|  |     pin: GPIO50 | ||||||
| @@ -738,7 +738,7 @@ lvgl: | |||||||
|                     id: bar_id |                     id: bar_id | ||||||
|                     value: !lambda return (int)((float)rand() / RAND_MAX * 100); |                     value: !lambda return (int)((float)rand() / RAND_MAX * 100); | ||||||
|                     start_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: |                 - logger.log: | ||||||
|                     format: "bar value %f" |                     format: "bar value %f" | ||||||
|                     args: [x] |                     args: [x] | ||||||
|   | |||||||
| @@ -6,6 +6,12 @@ esphome: | |||||||
|       - output.set_level: |       - output.set_level: | ||||||
|           id: light_output_1 |           id: light_output_1 | ||||||
|           level: 50% |           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: | output: | ||||||
|   - platform: ${output_platform} |   - platform: ${output_platform} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user