Merge branch 'disable_touch_pad_read_filtered' into integration

This commit is contained in:
J. Nick Koston 2025-07-28 22:10:19 -10:00
commit b324978c9d
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
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,
},
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

@ -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 <freertos/portmacro.h>
@ -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

View File

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

View File

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

View File

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

View File

@ -40,5 +40,29 @@ template<typename... Ts> class SetLevelAction : public Action<Ts...> {
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 esphome

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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