Merge branch 'integration' into memory_api

This commit is contained in:
J. Nick Koston 2025-07-28 22:10:32 -10:00
commit 8b52a9a02e
No known key found for this signature in database
27 changed files with 278 additions and 81 deletions

View File

@ -1 +1 @@
32b0db73b3ae01ba18c9cbb1dabbd8156bc14dded500471919bd0a3dc33916e0 f84518ea4140c194b21cc516aae05aaa0cf876ab866f89e22e91842df46333ed

View File

@ -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,
},
} }

View File

@ -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;

View File

@ -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);

View File

@ -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,

View File

@ -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

View File

@ -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}"

View File

@ -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

View File

@ -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 arent in the trigger mask // Skip pads that arent in the trigger mask
if (((mask >> pad) & 1) == 0) { if (((mask >> pad) & 1) == 0) {

View File

@ -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();

View File

@ -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")

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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() {

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()
)

View File

@ -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

View File

@ -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

View 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"

View File

@ -0,0 +1,6 @@
packages:
base: !include common.yaml
sensor:
- id: !extend my_sensor
pin: GPIO50

View File

@ -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]

View File

@ -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}