diff --git a/CODEOWNERS b/CODEOWNERS index 88f62c3194..5869925d7c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -457,6 +457,7 @@ esphome/components/sn74hc165/* @jesserockz esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sound_level/* @kahrendt +esphome/components/spa06_base/* @danielkent-net esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/speaker_source/* @kahrendt diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index 40676f8655..3137a59fad 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -1,22 +1,29 @@ #include "esphome/core/log.h" #include "absolute_humidity.h" -namespace esphome { -namespace absolute_humidity { +namespace esphome::absolute_humidity { -static const char *const TAG = "absolute_humidity.sensor"; +static const char *const TAG{"absolute_humidity.sensor"}; void AbsoluteHumidityComponent::setup() { + this->temperature_sensor_->add_on_state_callback([this](float state) { + this->temperature_ = state; + this->enable_loop(); + }); ESP_LOGD(TAG, " Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str()); - this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); }); + // Get initial value if (this->temperature_sensor_->has_state()) { - this->temperature_callback_(this->temperature_sensor_->get_state()); + this->temperature_ = this->temperature_sensor_->get_state(); } + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->humidity_ = state; + this->enable_loop(); + }); ESP_LOGD(TAG, " Added callback for relative humidity '%s'", this->humidity_sensor_->get_name().c_str()); - this->humidity_sensor_->add_on_state_callback([this](float state) { this->humidity_callback_(state); }); + // Get initial value if (this->humidity_sensor_->has_state()) { - this->humidity_callback_(this->humidity_sensor_->get_state()); + this->humidity_ = this->humidity_sensor_->get_state(); } } @@ -46,14 +53,12 @@ void AbsoluteHumidityComponent::dump_config() { } void AbsoluteHumidityComponent::loop() { - if (!this->next_update_) { - return; - } - this->next_update_ = false; + // Only run once + this->disable_loop(); // Ensure we have source data - const bool no_temperature = std::isnan(this->temperature_); - const bool no_humidity = std::isnan(this->humidity_); + const bool no_temperature{std::isnan(this->temperature_)}; + const bool no_humidity{std::isnan(this->humidity_)}; if (no_temperature || no_humidity) { if (no_temperature) { ESP_LOGW(TAG, "No valid state from temperature sensor!"); @@ -67,9 +72,9 @@ void AbsoluteHumidityComponent::loop() { } // Convert to desired units - const float temperature_c = this->temperature_; - const float temperature_k = temperature_c + 273.15; - const float hr = this->humidity_ / 100; + const float temperature_c{this->temperature_}; + const float temperature_k{temperature_c + 273.15f}; + const float hr{this->humidity_ / 100.0f}; // Calculate saturation vapor pressure float es; @@ -90,7 +95,7 @@ void AbsoluteHumidityComponent::loop() { } // Calculate absolute humidity - const float absolute_humidity = vapor_density(es, hr, temperature_k); + const float absolute_humidity{vapor_density(es, hr, temperature_k)}; ESP_LOGD(TAG, "Saturation vapor pressure %f kPa, absolute humidity %f g/m³", es, absolute_humidity); @@ -103,16 +108,16 @@ void AbsoluteHumidityComponent::loop() { // More accurate than Tetens in normal meteorologic conditions float AbsoluteHumidityComponent::es_buck(float temperature_c) { float a, b, c, d; - if (temperature_c >= 0) { - a = 0.61121; - b = 18.678; - c = 234.5; - d = 257.14; + if (temperature_c >= 0.0f) { + a = 0.61121f; + b = 18.678f; + c = 234.5f; + d = 257.14f; } else { - a = 0.61115; - b = 18.678; - c = 233.7; - d = 279.82; + a = 0.61115f; + b = 18.678f; + c = 233.7f; + d = 279.82f; } return a * expf((b - (temperature_c / c)) * (temperature_c / (d + temperature_c))); } @@ -120,14 +125,14 @@ float AbsoluteHumidityComponent::es_buck(float temperature_c) { // Tetens equation (https://en.wikipedia.org/wiki/Tetens_equation) float AbsoluteHumidityComponent::es_tetens(float temperature_c) { float a, b; - if (temperature_c >= 0) { - a = 17.27; - b = 237.3; + if (temperature_c >= 0.0f) { + a = 17.27f; + b = 237.3f; } else { - a = 21.875; - b = 265.5; + a = 21.875f; + b = 265.5f; } - return 0.61078 * expf((a * temperature_c) / (temperature_c + b)); + return 0.61078f * expf((a * temperature_c) / (temperature_c + b)); } // Wobus equation @@ -146,18 +151,18 @@ float AbsoluteHumidityComponent::es_wobus(float t) { // // Baker, Schlatter 17-MAY-1982 Original version. - const float c0 = +0.99999683e00; - const float c1 = -0.90826951e-02; - const float c2 = +0.78736169e-04; - const float c3 = -0.61117958e-06; - const float c4 = +0.43884187e-08; - const float c5 = -0.29883885e-10; - const float c6 = +0.21874425e-12; - const float c7 = -0.17892321e-14; - const float c8 = +0.11112018e-16; - const float c9 = -0.30994571e-19; - const float p = c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9))))))))); - return 0.61078 / pow(p, 8); + constexpr float c0{+0.99999683e+00f}; + constexpr float c1{-0.90826951e-02f}; + constexpr float c2{+0.78736169e-04f}; + constexpr float c3{-0.61117958e-06f}; + constexpr float c4{+0.43884187e-08f}; + constexpr float c5{-0.29883885e-10f}; + constexpr float c6{+0.21874425e-12f}; + constexpr float c7{-0.17892321e-14f}; + constexpr float c8{+0.11112018e-16f}; + constexpr float c9{-0.30994571e-19f}; + const float p{c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9)))))))))}; + return 0.61078f / powf(p, 8.0f); } // From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/ @@ -168,11 +173,10 @@ float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) { // hr = relative humidity [0-1] // ta = absolute temperature (K) - const float ea = hr * es * 1000; // vapor pressure of the air (Pa) - const float mw = 18.01528; // molar mass of water (g⋅mol⁻¹) - const float r = 8.31446261815324; // molar gas constant (J⋅K⁻¹) + const float ea{hr * es * 1000.0f}; // vapor pressure of the air (Pa) + const float mw{18.01528f}; // molar mass of water (g⋅mol⁻¹) + const float r{8.31446261815324f}; // molar gas constant (J⋅K⁻¹) return (ea * mw) / (r * ta); } -} // namespace absolute_humidity -} // namespace esphome +} // namespace esphome::absolute_humidity diff --git a/esphome/components/absolute_humidity/absolute_humidity.h b/esphome/components/absolute_humidity/absolute_humidity.h index 71feee2c42..be28d3dc50 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.h +++ b/esphome/components/absolute_humidity/absolute_humidity.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace absolute_humidity { +namespace esphome::absolute_humidity { /// Enum listing all implemented saturation vapor pressure equations. enum SaturationVaporPressureEquation { @@ -16,8 +15,6 @@ enum SaturationVaporPressureEquation { /// This class implements calculation of absolute humidity from temperature and relative humidity. class AbsoluteHumidityComponent : public sensor::Sensor, public Component { public: - AbsoluteHumidityComponent() = default; - void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } void set_equation(SaturationVaporPressureEquation equation) { this->equation_ = equation; } @@ -27,15 +24,6 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component { void loop() override; protected: - void temperature_callback_(float state) { - this->next_update_ = true; - this->temperature_ = state; - } - void humidity_callback_(float state) { - this->next_update_ = true; - this->humidity_ = state; - } - /** Buck equation for saturation vapor pressure in kPa. * * @param temperature_c Air temperature in °C. @@ -57,19 +45,15 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component { * @param es Saturation vapor pressure in kPa. * @param hr Relative humidity 0 to 1. * @param ta Absolute temperature in K. - * @param heater_duration The duration in ms that the heater should turn on for when measuring. */ static float vapor_density(float es, float hr, float ta); sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; - bool next_update_{false}; - float temperature_{NAN}; float humidity_{NAN}; SaturationVaporPressureEquation equation_; }; -} // namespace absolute_humidity -} // namespace esphome +} // namespace esphome::absolute_humidity diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index 2fd9cb0dd2..c2b0f14bcc 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -136,8 +136,9 @@ class CustomAPIDevice { template void subscribe_homeassistant_state(void (T::*callback)(StringRef), const std::string &entity_id, const std::string &attribute = "") { - auto f = std::bind(callback, (T *) this, std::placeholders::_1); - global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), std::move(f)); + auto *obj = static_cast(this); + global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), + [obj, callback](StringRef state) { (obj->*callback)(state); }); } /** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version). @@ -148,10 +149,12 @@ class CustomAPIDevice { ESPDEPRECATED("Use void callback(StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0") void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id, const std::string &attribute = "") { - auto f = std::bind(callback, (T *) this, std::placeholders::_1); + auto *obj = static_cast(this); // Explicit type to disambiguate overload resolution - global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), - std::function(f)); + global_api_server->subscribe_home_assistant_state( + entity_id, optional(attribute), + std::function( + [obj, callback](const std::string &state) { (obj->*callback)(state); })); } /** Subscribe to the state (or attribute state) of an entity from Home Assistant. @@ -176,8 +179,10 @@ class CustomAPIDevice { template void subscribe_homeassistant_state(void (T::*callback)(const std::string &, StringRef), const std::string &entity_id, const std::string &attribute = "") { - auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); - global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), std::move(f)); + auto *obj = static_cast(this); + global_api_server->subscribe_home_assistant_state( + entity_id, optional(attribute), + [obj, callback, entity_id](StringRef state) { (obj->*callback)(entity_id, state); }); } /** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version). @@ -188,10 +193,12 @@ class CustomAPIDevice { ESPDEPRECATED("Use void callback(const std::string &, StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0") void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id, const std::string &attribute = "") { - auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); + auto *obj = static_cast(this); // Explicit type to disambiguate overload resolution - global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), - std::function(f)); + global_api_server->subscribe_home_assistant_state( + entity_id, optional(attribute), + std::function( + [obj, callback, entity_id](const std::string &state) { (obj->*callback)(entity_id, state); })); } #else template diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index f8b130e08a..f30f9d3279 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -96,8 +96,7 @@ class MultiClickTrigger : public Trigger<>, public Component { void setup() override { this->last_state_ = this->parent_->get_state_default(false); - auto f = std::bind(&MultiClickTrigger::on_state_, this, std::placeholders::_1); - this->parent_->add_on_state_callback(f); + this->parent_->add_on_state_callback([this](bool state) { this->on_state_(state); }); } float get_setup_priority() const override { return setup_priority::HARDWARE; } diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index 0210d1e67d..ed2ec80896 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -279,7 +279,8 @@ void BME68xBSEC2Component::run_() { uint32_t meas_dur = 0; meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_); ESP_LOGV(TAG, "Queueing read in %uus", meas_dur); - this->set_timeout("read", meas_dur / 1000, [this, curr_time_ns]() { this->read_(curr_time_ns); }); + this->trigger_time_ns_ = curr_time_ns; + this->set_timeout("read", meas_dur / 1000, [this]() { this->read_(this->trigger_time_ns_); }); } else { ESP_LOGV(TAG, "Measurement not required"); this->read_(curr_time_ns); diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.h b/esphome/components/bme68x_bsec2/bme68x_bsec2.h index 8f4d8f61c2..1ed72eee03 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.h +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.h @@ -116,6 +116,8 @@ class BME68xBSEC2Component : public Component { int8_t bme68x_status_{BME68X_OK}; int64_t last_time_ms_{0}; + int64_t trigger_time_ns_{0}; // Stored for set_timeout lambda to help avoid heap allocation on supported 32-bit + // toolchains with small std::function SBO uint32_t millis_overflow_counter_{0}; std::queue> queue_; diff --git a/esphome/components/dps310/dps310.cpp b/esphome/components/dps310/dps310.cpp index aa0a77cdd8..b1366cd069 100644 --- a/esphome/components/dps310/dps310.cpp +++ b/esphome/components/dps310/dps310.cpp @@ -127,8 +127,7 @@ void DPS310Component::read_() { this->update_in_progress_ = false; this->status_clear_warning(); } else { - auto f = std::bind(&DPS310Component::read_, this); - this->set_timeout("dps310", 10, f); + this->set_timeout("dps310", 10, [this]() { this->read_(); }); } } diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 18178c83ff..64e5f44081 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -28,6 +28,7 @@ from esphome.const import ( CONF_PLATFORMIO_OPTIONS, CONF_REF, CONF_SAFE_MODE, + CONF_SIZE, CONF_SOURCE, CONF_TYPE, CONF_VARIANT, @@ -96,6 +97,7 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision" CONF_RELEASE = "release" +CONF_SUBTYPE = "subtype" ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32" ARDUINO_FRAMEWORK_PKG = f"pioarduino/{ARDUINO_FRAMEWORK_NAME}" @@ -1258,6 +1260,43 @@ def _set_default_framework(config): return config +RESERVED_PARTITION_NAMES = { + "nvs", + "app0", + "app1", + "otadata", + "eeprom", + "spiffs", + "phy_init", +} + +VALID_APP_SUBTYPES = {"factory", "test"} +VALID_DATA_SUBTYPES = { + "nvs", + "nvs_keys", + "spiffs", + "coredump", + "efuse", + "fat", + "undefined", + "littlefs", +} + + +def _validate_custom_partition(config: ConfigType) -> ConfigType: + """Voluptuous validator for custom partition schema.""" + try: + _validate_partition( + config[CONF_NAME], + config[CONF_TYPE], + config[CONF_SUBTYPE], + config[CONF_SIZE], + ) + except ValueError as e: + raise cv.Invalid(str(e)) from e + return config + + FLASH_SIZES = [ "2MB", "4MB", @@ -1280,7 +1319,28 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of( *FLASH_SIZES, upper=True ), - cv.Optional(CONF_PARTITIONS): cv.file_, + cv.Optional(CONF_PARTITIONS): cv.Any( + cv.file_, + cv.ensure_list( + cv.All( + cv.Schema( + { + cv.Required(CONF_NAME): cv.string_strict, + cv.Required(CONF_TYPE): cv.All( + cv.Any(cv.string_strict, cv.int_range(0x40, 0xFE)), + cv.int_to_hex_string, + ), + cv.Required(CONF_SUBTYPE): cv.All( + cv.Any(cv.string_strict, cv.int_range(0, 0xFE)), + cv.int_to_hex_string, + ), + cv.Required(CONF_SIZE): cv.int_range(min=0x1000), + } + ), + _validate_custom_partition, + ), + ), + ), cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA, } @@ -1749,9 +1809,18 @@ async def to_code(config): if use_platformio: cg.add_platformio_option("board_build.partitions", "partitions.csv") if CONF_PARTITIONS in config: - add_extra_build_file( - "partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS]) - ) + if isinstance(config[CONF_PARTITIONS], list): + for partition in config[CONF_PARTITIONS]: + add_partition( + partition[CONF_NAME], + partition[CONF_TYPE], + partition[CONF_SUBTYPE], + partition[CONF_SIZE], + ) + else: + add_extra_build_file( + "partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS]) + ) if assertion_level := advanced.get(CONF_ASSERTION_LEVEL): for key, flag in ASSERTION_LEVELS.items(): @@ -1885,45 +1954,175 @@ async def to_code(config): CORE.add_job(_write_arduino_libraries_sdkconfig) -APP_PARTITION_SIZES = { - "2MB": 0x0C0000, # 768 KB - "4MB": 0x1C0000, # 1792 KB - "8MB": 0x3C0000, # 3840 KB - "16MB": 0x7C0000, # 7936 KB - "32MB": 0xFC0000, # 16128 KB +KEY_CUSTOM_PARTITIONS = "custom_partitions" + + +@dataclass +class PartitionEntry: + name: str + type: str + subtype: str + size: int + + +# Partition sizes (offsets auto-placed by gen_esp32part.py). +# These constants are the single source of truth — used in both +# the CSV generation and the overhead calculation. +BOOTLOADER_SIZE = 0x8000 +PARTITION_TABLE_SIZE = 0x1000 +FIRST_PARTITION_OFFSET = BOOTLOADER_SIZE + PARTITION_TABLE_SIZE +OTADATA_SIZE = 0x2000 +PHY_INIT_SIZE = 0x1000 +EEPROM_SIZE = 0x1000 # Arduino only +SPIFFS_SIZE = 0xF000 # Arduino only +ARDUINO_NVS_SIZE = 0x60000 +IDF_NVS_SIZE = 0x70000 + + +def _get_partition_overhead() -> int: + """Total non-app partition budget (system partitions + nvs + padding). + + Custom partitions are appended at the end and steal from app. + """ + # otadata + phy_init are followed by app0 which requires 64KB alignment, + # so pad up to the next 64KB boundary. + overhead = ( + FIRST_PARTITION_OFFSET + OTADATA_SIZE + PHY_INIT_SIZE + 0xFFFF + ) & ~0xFFFF + if CORE.using_arduino: + overhead += EEPROM_SIZE + SPIFFS_SIZE + ARDUINO_NVS_SIZE + else: + overhead += IDF_NVS_SIZE + return overhead + + +VALID_SUBTYPES: dict[str, set[str]] = { + "app": VALID_APP_SUBTYPES, + "data": VALID_DATA_SUBTYPES, } -def get_arduino_partition_csv(flash_size: str): - app_partition_size = APP_PARTITION_SIZES[flash_size] - eeprom_partition_size = 0x1000 # 4 KB - spiffs_partition_size = 0xF000 # 60 KB - - app0_partition_start = 0x010000 # 64 KB - app1_partition_start = app0_partition_start + app_partition_size - eeprom_partition_start = app1_partition_start + app_partition_size - spiffs_partition_start = eeprom_partition_start + eeprom_partition_size - - return f"""\ -nvs, data, nvs, 0x9000, 0x5000, -otadata, data, ota, 0xE000, 0x2000, -app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X}, -app1, app, ota_1, 0x{app1_partition_start:X}, 0x{app_partition_size:X}, -eeprom, data, 0x99, 0x{eeprom_partition_start:X}, 0x{eeprom_partition_size:X}, -spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X} -""" +def _validate_partition( + name: str, p_type: str | int, subtype: str | int, size: int +) -> None: + """Validate partition parameters. Raises ValueError on invalid input.""" + if name in RESERVED_PARTITION_NAMES: + raise ValueError(f"Partition name '{name}' is reserved.") + if size % 0x1000 != 0: + raise ValueError("Partition size must be 4KB (0x1000) aligned.") + # Numeric or already-normalized hex types/subtypes skip string validation + if not isinstance(p_type, str) or p_type.startswith("0x"): + return + if p_type not in VALID_SUBTYPES: + raise ValueError( + f"Type '{p_type}' is invalid. Only 'app' and 'data' are allowed." + " Use numbers for custom types." + ) + if not isinstance(subtype, str) or subtype.startswith("0x"): + return + valid = VALID_SUBTYPES[p_type] + if subtype not in valid: + raise ValueError( + f"Subtype '{subtype}' is invalid for {p_type} type." + f" Only {', '.join(sorted(valid))} are allowed." + " Use numbers for custom subtypes." + ) -def get_idf_partition_csv(flash_size: str): - app_partition_size = APP_PARTITION_SIZES[flash_size] +def add_partition(name: str, p_type: str | int, subtype: str | int, size: int) -> None: + """Register a custom partition to be appended to the partition table. - return f"""\ -otadata, data, ota, , 0x2000, -phy_init, data, phy, , 0x1000, -app0, app, ota_0, , 0x{app_partition_size:X}, -app1, app, ota_1, , 0x{app_partition_size:X}, -nvs, data, nvs, , 0x6D000, -""" + Called from component to_code() to request additional flash partitions. + Size must be 4KB aligned. Integer types/subtypes are converted to hex strings. + """ + if name in CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}): + raise ValueError(f"Partition name '{name}' is already defined.") + _validate_partition(name, p_type, subtype, size) + p_type_str = f"0x{p_type:X}" if isinstance(p_type, int) else p_type + subtype_str = f"0x{subtype:X}" if isinstance(subtype, int) else subtype + custom_partitions = CORE.data[KEY_ESP32].setdefault(KEY_CUSTOM_PARTITIONS, {}) + custom_partitions[name] = PartitionEntry( + name=name, type=p_type_str, subtype=subtype_str, size=size + ) + + +def _flash_size_to_bytes(flash_size_mb: str) -> int: + """Convert flash size string (e.g. '4MB') to bytes.""" + return int(flash_size_mb.removesuffix("MB")) * 1024 * 1024 + + +def _get_custom_partitions_total_size() -> int: + """Total size of custom partitions including alignment padding.""" + size = 0 + for partition in CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}).values(): + if partition.type == "app": + size = (size + 0xFFFF) & ~0xFFFF # align to 64KB + size += partition.size + return size + + +def _get_app_partition_size(flash_size_mb: str) -> int: + flash_bytes = _flash_size_to_bytes(flash_size_mb) + custom_total = _get_custom_partitions_total_size() + # Align down to 64KB — app partitions require 64KB-aligned offsets, + # so the size must also be aligned to avoid unbudgeted padding. + raw_size = (flash_bytes - _get_partition_overhead() - custom_total) // 2 + app_size = raw_size & ~0xFFFF + wasted = (raw_size - app_size) * 2 + if wasted: + _LOGGER.info( + "Custom partitions cause %dKB of wasted flash due to 64KB app partition alignment.", + wasted // 1024, + ) + if app_size <= 0x10000: # 64 KB + raise ValueError( + "Custom partitions are too large to fit in the available flash size. " + "Reduce custom partition sizes." + ) + if app_size <= 0x80000: # 512 KB + _LOGGER.warning( + "App partition size is only %dKB. This may be too small for firmware with " + "many components. Consider reducing custom partition sizes or using a " + "larger flash chip.", + app_size // 1024, + ) + return app_size + + +def get_partition_csv(flash_size_mb: str) -> str: + app_size = _get_app_partition_size(flash_size_mb) + + partitions: list[PartitionEntry] = [ + PartitionEntry(name="otadata", type="data", subtype="ota", size=OTADATA_SIZE), + PartitionEntry(name="phy_init", type="data", subtype="phy", size=PHY_INIT_SIZE), + PartitionEntry(name="app0", type="app", subtype="ota_0", size=app_size), + PartitionEntry(name="app1", type="app", subtype="ota_1", size=app_size), + ] + if CORE.using_arduino: + partitions.append( + PartitionEntry(name="eeprom", type="data", subtype="0x99", size=EEPROM_SIZE) + ) + partitions.append( + PartitionEntry( + name="spiffs", type="data", subtype="spiffs", size=SPIFFS_SIZE + ) + ) + partitions.append( + PartitionEntry( + name="nvs", type="data", subtype="nvs", size=ARDUINO_NVS_SIZE + ) + ) + else: + partitions.append( + PartitionEntry(name="nvs", type="data", subtype="nvs", size=IDF_NVS_SIZE) + ) + partitions.extend(CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}).values()) + + csv = "".join( + f"{p.name}, {p.type}, {p.subtype}, , 0x{p.size:X},\n" for p in partitions + ) + _LOGGER.debug("Partition table:\n%s", csv) + return csv def _format_sdkconfig_val(value: SdkconfigValueType) -> str: @@ -2030,16 +2229,10 @@ def copy_files(): if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE] - if CORE.using_arduino: - write_file_if_changed( - CORE.relative_build_path("partitions.csv"), - get_arduino_partition_csv(flash_size), - ) - else: - write_file_if_changed( - CORE.relative_build_path("partitions.csv"), - get_idf_partition_csv(flash_size), - ) + write_file_if_changed( + CORE.relative_build_path("partitions.csv"), + get_partition_csv(flash_size), + ) # IDF build scripts look for version string to put in the build. # However, if the build path does not have an initialized git repo, # and no version.txt file exists, the CMake script fails for some setups. diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index e4ae49f235..c24b08b06f 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -350,8 +350,7 @@ void ESP32ImprovComponent::process_incoming_data_() { ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); - auto f = std::bind(&ESP32ImprovComponent::on_wifi_connect_timeout_, this); - this->set_timeout("wifi-connect-timeout", 30000, f); + this->set_timeout("wifi-connect-timeout", 30000, [this]() { this->on_wifi_connect_timeout_(); }); this->incoming_data_.clear(); break; } diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 35eaf36d32..4a06066d3c 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -242,7 +242,7 @@ void HaierClimateBase::setup() { this->last_request_timestamp_ = std::chrono::steady_clock::now(); this->set_phase(ProtocolPhases::SENDING_INIT_1); this->haier_protocol_.set_default_timeout_handler( - std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1)); + [this](haier_protocol::FrameType type) { return this->timeout_default_handler_(type); }); this->set_handlers(); this->initialization(); } diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index b7888f7976..92defe560e 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -301,32 +301,38 @@ void HonClimate::set_handlers() { // Set handlers this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::GET_DEVICE_VERSION, - std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->get_device_version_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::GET_DEVICE_ID, - std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->get_device_id_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::CONTROL, - std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, - std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->status_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION, - std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1, - std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->get_management_information_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::GET_ALARM_STATUS, - std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->get_alarm_status_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::REPORT_NETWORK_STATUS, - std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); - this->haier_protocol_.set_message_handler( - haier_protocol::FrameType::ALARM_STATUS, - std::bind(&HonClimate::alarm_status_message_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->report_network_status_answer_handler_(req, msg, data, size); + }); + this->haier_protocol_.set_message_handler(haier_protocol::FrameType::ALARM_STATUS, + [this](haier_protocol::FrameType type, const uint8_t *data, size_t size) { + return this->alarm_status_message_handler_(type, data, size); + }); } void HonClimate::dump_config() { diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index e91224e2d8..2be5d13050 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -106,18 +106,21 @@ void Smartair2Climate::set_handlers() { // Set handlers this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::GET_DEVICE_VERSION, - std::bind(&Smartair2Climate::get_device_version_answer_handler_, this, std::placeholders::_1, - std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->get_device_version_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::CONTROL, - std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->status_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::REPORT_NETWORK_STATUS, - std::bind(&Smartair2Climate::report_network_status_answer_handler_, this, std::placeholders::_1, - std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->report_network_status_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_default_timeout_handler( - std::bind(&Smartair2Climate::messages_timeout_handler_with_cycle_for_init_, this, std::placeholders::_1)); + [this](haier_protocol::FrameType type) { return this->messages_timeout_handler_with_cycle_for_init_(type); }); } void Smartair2Climate::dump_config() { diff --git a/esphome/components/homeassistant/number/homeassistant_number.cpp b/esphome/components/homeassistant/number/homeassistant_number.cpp index 00ea88ff16..da802b7fe9 100644 --- a/esphome/components/homeassistant/number/homeassistant_number.cpp +++ b/esphome/components/homeassistant/number/homeassistant_number.cpp @@ -55,15 +55,15 @@ void HomeassistantNumber::step_retrieved_(StringRef step) { } void HomeassistantNumber::setup() { - api::global_api_server->subscribe_home_assistant_state( - this->entity_id_, nullptr, std::bind(&HomeassistantNumber::state_changed_, this, std::placeholders::_1)); + api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullptr, + [this](StringRef state) { this->state_changed_(state); }); - api::global_api_server->get_home_assistant_state( - this->entity_id_, "min", std::bind(&HomeassistantNumber::min_retrieved_, this, std::placeholders::_1)); - api::global_api_server->get_home_assistant_state( - this->entity_id_, "max", std::bind(&HomeassistantNumber::max_retrieved_, this, std::placeholders::_1)); - api::global_api_server->get_home_assistant_state( - this->entity_id_, "step", std::bind(&HomeassistantNumber::step_retrieved_, this, std::placeholders::_1)); + api::global_api_server->get_home_assistant_state(this->entity_id_, "min", + [this](StringRef min) { this->min_retrieved_(min); }); + api::global_api_server->get_home_assistant_state(this->entity_id_, "max", + [this](StringRef max) { this->max_retrieved_(max); }); + api::global_api_server->get_home_assistant_state(this->entity_id_, "step", + [this](StringRef step) { this->step_retrieved_(step); }); } void HomeassistantNumber::dump_config() { diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 8bdea470b5..73dbda8694 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -487,12 +487,10 @@ template class HttpRequestSendAction : public Action { body = this->body_.value(x...); } if (!this->json_.empty()) { - auto f = std::bind(&HttpRequestSendAction::encode_json_, this, x..., std::placeholders::_1); - body = json::build_json(f); + body = json::build_json([this, x...](JsonObject root) { this->encode_json_(x..., root); }); } if (this->json_func_ != nullptr) { - auto f = std::bind(&HttpRequestSendAction::encode_json_func_, this, x..., std::placeholders::_1); - body = json::build_json(f); + body = json::build_json([this, x...](JsonObject root) { this->json_func_(x..., root); }); } std::vector
request_headers; request_headers.reserve(this->request_headers_.size()); @@ -561,7 +559,6 @@ template class HttpRequestSendAction : public Action { root[item.first] = val.value(x...); } } - void encode_json_func_(Ts... x, JsonObject root) { this->json_func_(x..., root); } HttpRequestComponent *parent_; FixedVector>> request_headers_{}; std::vector lower_case_collect_headers_{"content-type", "content-length"}; diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index edceb9a3b1..003328d535 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -245,8 +245,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); - auto f = std::bind(&ImprovSerialComponent::on_wifi_connect_timeout_, this); - this->set_timeout("wifi-connect-timeout", 30000, f); + this->set_timeout("wifi-connect-timeout", 30000, [this]() { this->on_wifi_connect_timeout_(); }); return true; } case improv::GET_CURRENT_STATE: diff --git a/esphome/components/max31855/max31855.cpp b/esphome/components/max31855/max31855.cpp index 99129880f4..8370977ce2 100644 --- a/esphome/components/max31855/max31855.cpp +++ b/esphome/components/max31855/max31855.cpp @@ -15,8 +15,7 @@ void MAX31855Sensor::update() { this->disable(); // Conversion time typ: 170ms, max: 220ms - auto f = std::bind(&MAX31855Sensor::read_data_, this); - this->set_timeout("value", 220, f); + this->set_timeout("value", 220, [this]() { this->read_data_(); }); } void MAX31855Sensor::setup() { this->spi_setup(); } diff --git a/esphome/components/max31856/max31856.cpp b/esphome/components/max31856/max31856.cpp index ff65c8c5c9..35e12309ba 100644 --- a/esphome/components/max31856/max31856.cpp +++ b/esphome/components/max31856/max31856.cpp @@ -41,8 +41,8 @@ void MAX31856Sensor::update() { this->one_shot_temperature_(); // Datasheet max conversion time for 1 shot is 155ms for 60Hz / 185ms for 50Hz - auto f = std::bind(&MAX31856Sensor::read_thermocouple_temperature_, this); - this->set_timeout("MAX31856Sensor::read_thermocouple_temperature_", filter_ == FILTER_60HZ ? 155 : 185, f); + this->set_timeout("MAX31856Sensor::read_thermocouple_temperature_", filter_ == FILTER_60HZ ? 155 : 185, + [this]() { this->read_thermocouple_temperature_(); }); } void MAX31856Sensor::read_thermocouple_temperature_() { diff --git a/esphome/components/max31865/max31865.cpp b/esphome/components/max31865/max31865.cpp index 09b8368d07..8b06a01166 100644 --- a/esphome/components/max31865/max31865.cpp +++ b/esphome/components/max31865/max31865.cpp @@ -60,8 +60,7 @@ void MAX31865Sensor::update() { this->write_config_(0b11100000, 0b10100000); // Datasheet max conversion time is 55ms for 60Hz / 66ms for 50Hz - auto f = std::bind(&MAX31865Sensor::read_data_, this); - this->set_timeout("value", filter_ == FILTER_60HZ ? 55 : 66, f); + this->set_timeout("value", filter_ == FILTER_60HZ ? 55 : 66, [this]() { this->read_data_(); }); } void MAX31865Sensor::setup() { diff --git a/esphome/components/max6675/max6675.cpp b/esphome/components/max6675/max6675.cpp index c329cdfd42..b8527c6b1d 100644 --- a/esphome/components/max6675/max6675.cpp +++ b/esphome/components/max6675/max6675.cpp @@ -13,8 +13,7 @@ void MAX6675Sensor::update() { this->disable(); // Conversion time typ: 170ms, max: 220ms - auto f = std::bind(&MAX6675Sensor::read_data_, this); - this->set_timeout("value", 250, f); + this->set_timeout("value", 250, [this]() { this->read_data_(); }); } void MAX6675Sensor::setup() { this->spi_setup(); } diff --git a/esphome/components/midea/appliance_base.h b/esphome/components/midea/appliance_base.h index 660d185b49..c7737ba7d6 100644 --- a/esphome/components/midea/appliance_base.h +++ b/esphome/components/midea/appliance_base.h @@ -59,7 +59,7 @@ template class ApplianceBase : public Component { public: ApplianceBase() { this->base_.setStream(&this->stream_); - this->base_.addOnStateCallback(std::bind(&ApplianceBase::on_status_change, this)); + this->base_.addOnStateCallback([this]() { this->on_status_change(); }); dudanov::midea::ApplianceBase::setLogger( [](int level, const char *tag, int line, const String &format, va_list args) { esp_log_vprintf_(level, tag, line, format.c_str(), args); diff --git a/esphome/components/mqtt/custom_mqtt_device.h b/esphome/components/mqtt/custom_mqtt_device.h index 09ed7bd6d1..a003379fe0 100644 --- a/esphome/components/mqtt/custom_mqtt_device.h +++ b/esphome/components/mqtt/custom_mqtt_device.h @@ -189,28 +189,33 @@ class CustomMQTTDevice { template void CustomMQTTDevice::subscribe(const std::string &topic, void (T::*callback)(const std::string &, const std::string &), uint8_t qos) { - auto f = std::bind(callback, (T *) this, std::placeholders::_1, std::placeholders::_2); - global_mqtt_client->subscribe(topic, f, qos); + auto *obj = static_cast(this); + global_mqtt_client->subscribe( + topic, [obj, callback](const std::string &t, const std::string &payload) { (obj->*callback)(t, payload); }, qos); } template void CustomMQTTDevice::subscribe(const std::string &topic, void (T::*callback)(const std::string &), uint8_t qos) { - auto f = std::bind(callback, (T *) this, std::placeholders::_2); - global_mqtt_client->subscribe(topic, f, qos); + auto *obj = static_cast(this); + global_mqtt_client->subscribe( + topic, [obj, callback](const std::string &, const std::string &payload) { (obj->*callback)(payload); }, qos); } template void CustomMQTTDevice::subscribe(const std::string &topic, void (T::*callback)(), uint8_t qos) { - auto f = std::bind(callback, (T *) this); - global_mqtt_client->subscribe(topic, f, qos); + auto *obj = static_cast(this); + global_mqtt_client->subscribe( + topic, [obj, callback](const std::string &, const std::string &) { (obj->*callback)(); }, qos); } template void CustomMQTTDevice::subscribe_json(const std::string &topic, void (T::*callback)(const std::string &, JsonObject), uint8_t qos) { - auto f = std::bind(callback, (T *) this, std::placeholders::_1, std::placeholders::_2); - global_mqtt_client->subscribe_json(topic, f, qos); + auto *obj = static_cast(this); + global_mqtt_client->subscribe_json( + topic, [obj, callback](const std::string &t, JsonObject root) { (obj->*callback)(t, root); }, qos); } template void CustomMQTTDevice::subscribe_json(const std::string &topic, void (T::*callback)(JsonObject), uint8_t qos) { - auto f = std::bind(callback, (T *) this, std::placeholders::_2); - global_mqtt_client->subscribe_json(topic, f, qos); + auto *obj = static_cast(this); + global_mqtt_client->subscribe_json( + topic, [obj, callback](const std::string &, JsonObject root) { (obj->*callback)(root); }, qos); } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 127e4073b0..0d52d98d2f 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -405,15 +405,14 @@ template class MQTTPublishJsonAction : public Action { void set_payload(std::function payload) { this->payload_ = payload; } void play(const Ts &...x) override { - auto f = std::bind(&MQTTPublishJsonAction::encode_, this, x..., std::placeholders::_1); auto topic = this->topic_.value(x...); auto qos = this->qos_.value(x...); auto retain = this->retain_.value(x...); - this->parent_->publish_json(topic, f, qos, retain); + this->parent_->publish_json( + topic, [this, x...](JsonObject root) { this->payload_(x..., root); }, qos, retain); } protected: - void encode_(Ts... x, JsonObject root) { this->payload_(x..., root); } std::function payload_; MQTTClientComponent *parent_; }; diff --git a/esphome/components/ms5611/ms5611.cpp b/esphome/components/ms5611/ms5611.cpp index 7ed73400c8..d47ca245b8 100644 --- a/esphome/components/ms5611/ms5611.cpp +++ b/esphome/components/ms5611/ms5611.cpp @@ -45,8 +45,7 @@ void MS5611Component::update() { return; } - auto f = std::bind(&MS5611Component::read_temperature_, this); - this->set_timeout("temperature", 10, f); + this->set_timeout("temperature", 10, [this]() { this->read_temperature_(); }); } void MS5611Component::read_temperature_() { uint8_t bytes[3]; @@ -62,8 +61,7 @@ void MS5611Component::read_temperature_() { return; } - auto f = std::bind(&MS5611Component::read_pressure_, this, raw_temperature); - this->set_timeout("pressure", 10, f); + this->set_timeout("pressure", 10, [this, raw_temperature]() { this->read_pressure_(raw_temperature); }); } void MS5611Component::read_pressure_(uint32_t raw_temperature) { uint8_t bytes[3]; diff --git a/esphome/components/ms8607/ms8607.cpp b/esphome/components/ms8607/ms8607.cpp index 88a3e6d7dc..d141dcb191 100644 --- a/esphome/components/ms8607/ms8607.cpp +++ b/esphome/components/ms8607/ms8607.cpp @@ -281,9 +281,8 @@ void MS8607Component::request_read_temperature_() { return; } - auto f = std::bind(&MS8607Component::read_temperature_, this); // datasheet says 17.2ms max conversion time at OSR 8192 - this->set_timeout("temperature", 20, f); + this->set_timeout("temperature", 20, [this]() { this->read_temperature_(); }); } void MS8607Component::read_temperature_() { @@ -303,9 +302,8 @@ void MS8607Component::request_read_pressure_(uint32_t d2_raw_temperature) { return; } - auto f = std::bind(&MS8607Component::read_pressure_, this, d2_raw_temperature); // datasheet says 17.2ms max conversion time at OSR 8192 - this->set_timeout("pressure", 20, f); + this->set_timeout("pressure", 20, [this, d2_raw_temperature]() { this->read_pressure_(d2_raw_temperature); }); } void MS8607Component::read_pressure_(uint32_t d2_raw_temperature) { @@ -325,9 +323,8 @@ void MS8607Component::request_read_humidity_(float temperature_float) { return; } - auto f = std::bind(&MS8607Component::read_humidity_, this, temperature_float); // datasheet says 15.89ms max conversion time at OSR 8192 - this->set_timeout("humidity", 20, f); + this->set_timeout("humidity", 20, [this, temperature_float]() { this->read_humidity_(temperature_float); }); } void MS8607Component::read_humidity_(float temperature_float) { diff --git a/esphome/components/nau7802/nau7802.cpp b/esphome/components/nau7802/nau7802.cpp index 937239b98d..66d36dd741 100644 --- a/esphome/components/nau7802/nau7802.cpp +++ b/esphome/components/nau7802/nau7802.cpp @@ -83,8 +83,7 @@ void NAU7802Sensor::setup() { // turn on AFE pu_ctrl |= PU_CTRL_POWERUP_ANALOG; - auto f = std::bind(&NAU7802Sensor::complete_setup_, this); - this->set_timeout(600, f); + this->set_timeout(600, [this]() { this->complete_setup_(); }); } void NAU7802Sensor::complete_setup_() { diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index bf17ac27b8..a0594d7f67 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -310,6 +310,50 @@ async def beo4_action(var, config, args): cg.add(var.set_repeats(template_)) +# Brennenstuhl +( + BrennenstuhlData, + BrennenstuhlBinarySensor, + BrennenstuhlTrigger, + BrennenstuhlAction, + BrennenstuhlDumper, +) = declare_protocol("Brennenstuhl") + +BRENNENSTUHL_SCHEMA = cv.Schema( + { + cv.Required(CONF_CODE): cv.hex_uint32_t, + } +) + + +@register_binary_sensor("brennenstuhl", BrennenstuhlBinarySensor, BRENNENSTUHL_SCHEMA) +def brennenstuhl_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + BrennenstuhlData, + ("code", config[CONF_CODE]), + ) + ) + ) + + +@register_trigger("brennenstuhl", BrennenstuhlTrigger, BrennenstuhlData) +def brennenstuhl_trigger(var, config): + pass + + +@register_dumper("brennenstuhl", BrennenstuhlDumper) +def brennenstuhl_dumper(var, config): + pass + + +@register_action("brennenstuhl", BrennenstuhlAction, BRENNENSTUHL_SCHEMA) +async def brennenstuhl_action(var, config, args): + template_ = await cg.templatable(config[CONF_CODE], args, cg.uint32) + cg.add(var.set_code(template_)) + + # ByronSX ( ByronSXData, diff --git a/esphome/components/remote_base/brennenstuhl_protocol.cpp b/esphome/components/remote_base/brennenstuhl_protocol.cpp new file mode 100644 index 0000000000..35261dc00a --- /dev/null +++ b/esphome/components/remote_base/brennenstuhl_protocol.cpp @@ -0,0 +1,144 @@ +#include "brennenstuhl_protocol.h" +#include "esphome/core/log.h" + +#include + +namespace esphome::remote_base { + +static const char *const TAG = "remote.brennenstuhl"; + +// receiver timing ranges [µs] +constexpr uint32_t START_PULSE_MIN = 200; +constexpr uint32_t START_PULSE_MAX = 500; +constexpr uint32_t START_SYMBOL_MIN = 2600; +constexpr uint32_t START_SYMBOL_MAX = 2700; +constexpr uint32_t DATA_SYMBOL_MIN = 1500; +constexpr uint32_t DATA_SYMBOL_MAX = 1600; + +// transmitter timings [µs] +constexpr uint32_t PW_SHORT_US = 390; +constexpr uint32_t PW_LONG_US = 1160; +constexpr uint32_t PW_START_US = 2300; + +// number of data bits +constexpr uint32_t N_BITS = 24; + +// number of required symbols = 2 x (start + N_BITS) = 50 +constexpr uint32_t N_SYMBOLS_REQ = 2u * (N_BITS + 1); + +// number of bs codes within received frame +constexpr int32_t N_FRAME_CODES = 4; + +// decoder finite-state-machine +enum class RxSt { START_PULSE, START_SYMBOL, PULSE, DATA_SYMBOL }; + +// The encode() member function reserves and fills a complete frame, to be send. The Brennenstuhl +// RC receivers demand a frame with a start-symbol followed by 4 repeated codes. +void BrennenstuhlProtocol::encode(RemoteTransmitData *dst, const BrennenstuhlData &data) { + uint32_t code = data.code; + dst->reserve((N_SYMBOLS_REQ * N_FRAME_CODES) + 1); + for (int32_t kc = 0; kc != N_FRAME_CODES; kc++) { + dst->item(PW_SHORT_US, PW_START_US); + for (int32_t ic = (N_BITS - 1); ic != -1; ic--) { + if ((code >> ic) & 1) { + dst->item(PW_LONG_US, PW_SHORT_US); + } else { + dst->item(PW_SHORT_US, PW_LONG_US); + } + } + } +} + +// The decode() member function extracts Brennenstuhl codes from the received frame. Instead +// of validating the pulse width of the carriers and pauses individually, it is more accurate +// to validate the symbols (symbol=carrier+pause) The symbol pulsewidth is around 1550µs, but +// the pulse with of the carrier and the pauses vary greatly. Once the symbol pulsewidth is +// valid, a code bit becomes "1" if the carrier is longer then the pause and "0" else. A total +// frame consists of a start symbol and up to four codes. The decoder decodes all codes and +// returns the best code (the one with the most identical codes) +optional BrennenstuhlProtocol::decode(RemoteReceiveData src) { + uint32_t n_received = static_cast(src.size()); + BrennenstuhlData data{ + .code = 0, + }; + // suppress noisy frames, at least a complete bs_code should be available + if (n_received > N_SYMBOLS_REQ) { + uint32_t bs_codes[4] = {0, 0, 0, 0}; // internal codes + int32_t bs_cnt = 0; // number of bs codes found within frame + int32_t bs_idx = -1; // index to best bs code + uint32_t bit_cnt = 0; // bit counter [0..23] + uint32_t pw_pre = 0; // pulsewidth of previous carrier (abs value) + RxSt fsm = RxSt::START_PULSE; + for (uint32_t ic = 0; (ic != n_received) && (bs_cnt != N_FRAME_CODES); ic++) { + uint32_t pw_cur = (uint32_t) (src[ic] < 0 ? -src[ic] : src[ic]); // current pulsewidth + uint32_t pw_sym = pw_cur + pw_pre; // symbol=pulse+pause + switch (fsm) { + case RxSt::START_PULSE: { // check if start pulse is valid + if ((src[ic] > 0) && (pw_cur >= START_PULSE_MIN) && (pw_cur <= START_PULSE_MAX)) { + bs_codes[bs_cnt] = 0; + bit_cnt = 0; + pw_pre = pw_cur; + fsm = RxSt::START_SYMBOL; + } + break; + } + case RxSt::START_SYMBOL: { // check if start symbol is valid + if ((src[ic] < 0) && (pw_sym >= START_SYMBOL_MIN) && (pw_sym <= START_SYMBOL_MAX)) { + fsm = RxSt::PULSE; + } else { + fsm = RxSt::START_PULSE; + } + break; + } + case RxSt::PULSE: { // just grab pulse, validation is done in DATA_SYMBOL state + if (src[ic] > 0) { + pw_pre = pw_cur; + fsm = RxSt::DATA_SYMBOL; + } else { + fsm = RxSt::START_PULSE; + } + break; + } + case RxSt::DATA_SYMBOL: { // check if data symbol is valid and append bit to data + if ((src[ic] < 0) && (pw_sym >= DATA_SYMBOL_MIN) && (pw_sym <= DATA_SYMBOL_MAX)) { + bs_codes[bs_cnt] <<= 1; + bs_codes[bs_cnt] += (pw_cur < pw_pre) ? 1 : 0; + if (++bit_cnt < N_BITS) { + fsm = RxSt::PULSE; + } else { + bs_cnt++; // complete code found + fsm = RxSt::START_PULSE; // start over for further codes in frame + } + } else { + fsm = RxSt::START_PULSE; // decoding failed, start over for further codes + } + break; + } + } + } + if (bs_cnt > 0) { // complete codes found, find best code in list now + int32_t identical_max = 0; + for (int32_t ic = 0; ic != bs_cnt; ic++) { + int32_t identical_cnt = 0; + for (int32_t jc = 0; jc != bs_cnt; jc++) { + identical_cnt += (bs_codes[ic] == bs_codes[jc]) ? 1 : 0; + } + if (identical_cnt > identical_max) { + identical_max = identical_cnt; + bs_idx = ic; // save index to best code + } + } + if (bs_idx > -1) { + data.code = bs_codes[bs_idx]; + return data; // return best bs code of list + } + } + } + return {}; +} + +void BrennenstuhlProtocol::dump(const BrennenstuhlData &data) { + ESP_LOGI(TAG, "Brennenstuhl: code=0x%06" PRIx32, data.code); +} + +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/brennenstuhl_protocol.h b/esphome/components/remote_base/brennenstuhl_protocol.h new file mode 100644 index 0000000000..1d5b621714 --- /dev/null +++ b/esphome/components/remote_base/brennenstuhl_protocol.h @@ -0,0 +1,34 @@ +#pragma once + +#include "remote_base.h" + +#include + +namespace esphome::remote_base { + +struct BrennenstuhlData { + uint32_t code; + bool operator==(const BrennenstuhlData &rhs) const { return code == rhs.code; } +}; + +class BrennenstuhlProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const BrennenstuhlData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const BrennenstuhlData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Brennenstuhl) + +template class BrennenstuhlAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint32_t, code) + + void encode(RemoteTransmitData *dst, Ts... x) override { + BrennenstuhlData data{}; + data.code = this->code_.value(x...); + BrennenstuhlProtocol().encode(dst, data); + } +}; + +} // namespace esphome::remote_base diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 42be326202..bf23e42e66 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -63,7 +63,7 @@ void SHT4XComponent::setup() { } ESP_LOGD(TAG, "Heater command: %x", this->heater_command_); - this->set_interval(heater_interval, std::bind(&SHT4XComponent::start_heater_, this)); + this->set_interval(heater_interval, [this]() { this->start_heater_(); }); } } diff --git a/esphome/components/spa06_base/__init__.py b/esphome/components/spa06_base/__init__.py new file mode 100644 index 0000000000..97d09aad81 --- /dev/null +++ b/esphome/components/spa06_base/__init__.py @@ -0,0 +1,201 @@ +import math + +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_OVERSAMPLING, + CONF_PRESSURE, + CONF_SAMPLE_RATE, + CONF_TEMPERATURE, + DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PASCAL, +) + +CODEOWNERS = ["@danielkent-net"] + +spa06_ns = cg.esphome_ns.namespace("spa06_base") + +SampleRate = spa06_ns.enum("SampleRate") +SAMPLE_RATE_OPTIONS = { + "1": SampleRate.SAMPLE_RATE_1, + "2": SampleRate.SAMPLE_RATE_2, + "4": SampleRate.SAMPLE_RATE_4, + "8": SampleRate.SAMPLE_RATE_8, + "16": SampleRate.SAMPLE_RATE_16, + "32": SampleRate.SAMPLE_RATE_32, + "64": SampleRate.SAMPLE_RATE_64, + "128": SampleRate.SAMPLE_RATE_128, + "25p16": SampleRate.SAMPLE_RATE_25P16, + "25p8": SampleRate.SAMPLE_RATE_25P8, + "25p4": SampleRate.SAMPLE_RATE_25P4, + "25p2": SampleRate.SAMPLE_RATE_25P2, + "25": SampleRate.SAMPLE_RATE_25, + "50": SampleRate.SAMPLE_RATE_50, + "100": SampleRate.SAMPLE_RATE_100, + "200": SampleRate.SAMPLE_RATE_200, +} + +Oversampling = spa06_ns.enum("Oversampling") +OVERSAMPLING_OPTIONS = { + "NONE": Oversampling.OVERSAMPLING_NONE, + "2X": Oversampling.OVERSAMPLING_X2, + "4X": Oversampling.OVERSAMPLING_X4, + "8X": Oversampling.OVERSAMPLING_X8, + "16X": Oversampling.OVERSAMPLING_X16, + "32X": Oversampling.OVERSAMPLING_X32, + "64X": Oversampling.OVERSAMPLING_X64, + "128X": Oversampling.OVERSAMPLING_X128, +} + +SPA06Component = spa06_ns.class_("SPA06Component", cg.PollingComponent) + + +def spa_oversample_time(oversample): + # Pressure oversampling conversion times are listed on datasheet Pg. 26 + # Datasheet does not have a table for temperature oversampling; + # assumption is that it is the same as pressure + OVERSAMPLING_CONVERSION_TIMES = { + "NONE": 3.6, + "2X": 5.2, + "4X": 8.4, + "8X": 14.8, + "16X": 27.6, + "32X": 53.2, + "64X": 104.4, + "128X": 206.8, + } + return OVERSAMPLING_CONVERSION_TIMES[oversample] + + +def spa_sample_rate(rate): + SAMPLE_RATE_OPTIONS_HZ = { + "1": 1.0, + "2": 2.0, + "4": 4.0, + "8": 8.0, + "16": 16.0, + "32": 32.0, + "64": 64.0, + "128": 128.0, + "25p16": 25.0 / 16.0, + "25p8": 25.0 / 8.0, + "25p4": 25.0 / 4.0, + "25p2": 25.0 / 2.0, + "25": 25.0, + "50": 50.0, + "100": 100.0, + "200": 200.0, + } + return SAMPLE_RATE_OPTIONS_HZ[rate] + + +def compute_measurement_conversion_time(config): + # - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet + # - returns a rounded up time in ms + + # No conversion time necessary without a pressure sensor + pressure_conversion_time = 0.0 + if pressure_config := config.get(CONF_PRESSURE): + pressure_conversion_time = spa_oversample_time( + pressure_config.get(CONF_OVERSAMPLING) + ) + # Temperature required in all cases, default to minimum sample time + temperature_conversion_time = 3.6 + if temperature_config := config.get(CONF_TEMPERATURE): + temperature_conversion_time = spa_oversample_time( + temperature_config.get(CONF_OVERSAMPLING) + ) + + # TODO: Read datasheet to find conversion time error + return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time)) + + +def measurement_timing_check(config): + + temp_time = 0.0 + if temperature_config := config.get(CONF_TEMPERATURE): + temp_oss = ( + spa_oversample_time(temperature_config.get(CONF_OVERSAMPLING)) / 1000.0 + ) + temp_hz = spa_sample_rate(temperature_config.get(CONF_SAMPLE_RATE)) + temp_time = temp_oss * temp_hz + + pres_time = 0.0 + if pressure_config := config.get(CONF_PRESSURE): + pres_oss = spa_oversample_time(pressure_config.get(CONF_OVERSAMPLING)) / 1000.0 + pres_hz = spa_sample_rate(pressure_config.get(CONF_SAMPLE_RATE)) + pres_time = pres_oss * pres_hz + + if temp_time + pres_time >= 1: + raise cv.Invalid( + "Combined sample_rate and oversampling for temperature and pressure is too high" + ) + return config + + +CONFIG_SCHEMA_BASE = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SPA06Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="NONE"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + cv.Optional(CONF_SAMPLE_RATE, default="1"): cv.enum( + SAMPLE_RATE_OPTIONS, lower=True + ), + } + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_PASCAL, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + cv.Optional(CONF_SAMPLE_RATE, default="1"): cv.enum( + SAMPLE_RATE_OPTIONS, lower=True + ), + } + ), + }, +).extend(cv.polling_component_schema("60s")) +CONFIG_SCHEMA_BASE.add_extra(measurement_timing_check) + + +async def to_code_base(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + cg.add( + var.set_temperature_oversampling_config( + temperature_config[CONF_OVERSAMPLING] + ) + ) + cg.add( + var.set_temperature_sample_rate_config(temperature_config[CONF_SAMPLE_RATE]) + ) + + if pressure_config := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure_config) + cg.add(var.set_pressure_sensor(sens)) + cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING])) + cg.add(var.set_pressure_sample_rate_config(pressure_config[CONF_SAMPLE_RATE])) + + cg.add(var.set_conversion_time(compute_measurement_conversion_time(config))) + return var diff --git a/esphome/components/spa06_base/spa06_base.cpp b/esphome/components/spa06_base/spa06_base.cpp new file mode 100644 index 0000000000..36268aa9a2 --- /dev/null +++ b/esphome/components/spa06_base/spa06_base.cpp @@ -0,0 +1,320 @@ +#include "spa06_base.h" + +#include "esphome/core/helpers.h" + +namespace esphome::spa06_base { + +static const char *const TAG = "spa06"; + +// Sign extension function for <=16 bit types +inline int16_t decode16(uint8_t msb, uint8_t lsb, size_t bits, size_t head = 0) { + return static_cast(encode_uint16(msb, lsb) << head) >> (16 - bits); +} + +// Sign extension function for <=32 bit types +inline int32_t decode32(uint8_t xmsb, uint8_t msb, uint8_t lsb, uint8_t xlsb, size_t bits, size_t head = 0) { + return static_cast(encode_uint32(xmsb, msb, lsb, xlsb) << head) >> (32 - bits); +} + +void SPA06Component::dump_config() { + ESP_LOGCONFIG(TAG, "SPA06:"); + LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, " Measurement conversion time: %ums", this->conversion_time_); + if (this->temperature_sensor_) { + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + ESP_LOGCONFIG(TAG, + " Oversampling: %s\n" + " Rate: %s", + LOG_STR_ARG(oversampling_to_str(this->temperature_oversampling_)), + LOG_STR_ARG(meas_rate_to_str(this->temperature_rate_))); + } + if (this->pressure_sensor_) { + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + ESP_LOGCONFIG(TAG, + " Oversampling: %s\n" + " Rate: %s", + LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_)), + LOG_STR_ARG(meas_rate_to_str(this->pressure_rate_))); + } +} + +void SPA06Component::setup() { + // Startup sequence for SPA06 (Pg. 16, Figure 4.6.4): + // 1. Perform a soft reset + // 2. Verify sensor chip ID matches + // 3. Verify coefficients are ready + // 4. Read coefficients + // 5. Configure temperature and pressure sensors + // 6. Write communication settings + // 7. Write measurement settings (background measurement mode) + + // 1. Soft reset + if (!this->soft_reset_()) { + this->mark_failed(LOG_STR("Reset failed")); + return; + } + + // soft_reset_() internally delays by 3ms to make sure that + // the sensor is in a ready state and coefficients are ready. + + // 2. Read chip ID + // TODO: check ID for consistency? + if (!spa_read_byte(SPA06_ID, &this->prod_id_.reg)) { + this->mark_failed(LOG_STR("Chip ID read failure")); + return; + } + ESP_LOGV(TAG, + "Product Info:\n" + " Prod ID: %u\n" + " Rev ID: %u", + this->prod_id_.bit.prod_id, this->prod_id_.bit.rev_id); + + // 3. Read chip readiness from MEAS_CFG + // First check if the sensor reports ready + if (!spa_read_byte(SPA06_MEAS_CFG, &this->meas_.reg)) { + this->mark_failed(LOG_STR("Sensor status read failure")); + return; + } + // Check if the sensor reports coefficients are ready + if (!meas_.bit.coef_ready) { + this->mark_failed(LOG_STR("Coefficients not ready")); + return; + } + + // 4. Read coefficients + if (!this->read_coefficients_()) { + this->mark_failed(LOG_STR("Coefficients read error")); + return; + } + + // 5. Configure temperature and pressure sensors + // Default to measuring both temperature and pressure + + // Temperature must be read regardless of configuration to compute pressure + // If temperature is not configured in config: + // - No oversampling is used + // - Lowest possible rate is configured + if (!this->temperature_sensor_) { + this->temperature_rate_ = SAMPLE_RATE_1; + this->temperature_oversampling_ = OVERSAMPLING_NONE; + this->kt_ = oversampling_to_scale_factor(OVERSAMPLING_NONE); + } + + // If pressure is not configured in config + // - No oversampling is used + // - Lowest possible rate is configured + if (!this->pressure_sensor_) { + this->pressure_rate_ = SAMPLE_RATE_1; + this->pressure_oversampling_ = OVERSAMPLING_NONE; + this->kp_ = oversampling_to_scale_factor(OVERSAMPLING_NONE); + } + + // Write temperature settings + if (!write_temperature_settings_(this->temperature_oversampling_, this->temperature_rate_)) { + this->mark_failed(LOG_STR("Temperature settings write fail")); + return; + } + + // Write pressure settings + if (!write_pressure_settings_(this->pressure_oversampling_, this->pressure_rate_)) { + this->mark_failed(LOG_STR("Pressure settings write fail")); + return; + } + // 6. Write communication settings + // This call sets the bit shifts for pressure and temperature if + // their respective oversampling config is > X8 + // This call also disables interrupts, FIFO, and specifies SPI 4-wire + if (!write_communication_settings_(this->pressure_oversampling_ > OVERSAMPLING_X8, + this->temperature_oversampling_ > OVERSAMPLING_X8)) { + this->mark_failed(LOG_STR("Comm settings write fail")); + return; + } + + // 7. Write measurement settings + // This function sets background measurement mode without FIFO + if (!write_measurement_settings_(this->pressure_sensor_ ? MeasCrtl::MEASCRTL_BG_BOTH : MeasCrtl::MEASCRTL_BG_TEMP)) { + this->mark_failed(LOG_STR("Measurement settings write fail")); + return; + } +} + +bool SPA06Component::write_temperature_settings_(Oversampling oversampling, SampleRate rate) { + return this->write_sensor_settings_(oversampling, rate, SPA06_TMP_CFG); +} + +bool SPA06Component::write_pressure_settings_(Oversampling oversampling, SampleRate rate) { + return this->write_sensor_settings_(oversampling, rate, SPA06_PSR_CFG); +} + +bool SPA06Component::write_sensor_settings_(Oversampling oversampling, SampleRate rate, uint8_t reg) { + if (reg != SPA06_PSR_CFG && reg != SPA06_TMP_CFG) { + return false; + } + this->pt_meas_cfg_.bit.rate = rate; + this->pt_meas_cfg_.bit.prc = oversampling; + ESP_LOGD(TAG, "Config write: %02x", this->pt_meas_cfg_.reg); + return spa_write_byte(reg, this->pt_meas_cfg_.reg); +} + +bool SPA06Component::write_measurement_settings_(MeasCrtl crtl) { + this->meas_.bit.meas_crtl = crtl; + return spa_write_byte(SPA06_MEAS_CFG, this->meas_.reg); +} + +bool SPA06Component::write_communication_settings_(bool pressure_shift, bool temperature_shift, bool interrupt_hl, + bool interrupt_fifo, bool interrupt_tmp, bool interrupt_prs, + bool enable_fifo, bool spi_3wire) { + this->cfg_.bit.p_shift = pressure_shift; + this->cfg_.bit.t_shift = temperature_shift; + this->cfg_.bit.int_hl = interrupt_hl; + this->cfg_.bit.int_fifo = interrupt_fifo; + this->cfg_.bit.int_tmp = interrupt_tmp; + this->cfg_.bit.int_prs = interrupt_prs; + this->cfg_.bit.fifo_en = enable_fifo; + this->cfg_.bit.spi_3wire = spi_3wire; + return spa_write_byte(SPA06_CFG_REG, this->cfg_.reg); +} + +bool SPA06Component::read_coefficients_() { + uint8_t coef[SPA06_COEF_LEN]; + if (!spa_read_bytes(SPA06_COEF, coef, SPA06_COEF_LEN)) { + return false; + } + this->c0_ = decode16(coef[0], coef[1], 12); + this->c1_ = decode16(coef[1], coef[2], 12, 4); + this->c00_ = decode32(coef[3], coef[4], coef[5], 0, 20); + this->c10_ = decode32(coef[5], coef[6], coef[7], 0, 20, 4); + this->c01_ = decode16(coef[8], coef[9], 16); + this->c11_ = decode16(coef[10], coef[11], 16); + this->c20_ = decode16(coef[12], coef[13], 16); + this->c21_ = decode16(coef[14], coef[15], 16); + this->c30_ = decode16(coef[16], coef[17], 16); + this->c31_ = decode16(coef[18], coef[19], 12); + this->c40_ = decode16(coef[19], coef[20], 12, 4); + + ESP_LOGV(TAG, + "Coefficients:\n" + " c0: %i, c1: %i,\n" + " c00: %i, c10: %i, c20: %i, c30: %i, c40: %i,\n" + " c01: %i, c11: %i, c21: %i, c31: %i", + this->c0_, this->c1_, this->c00_, this->c10_, this->c20_, this->c30_, this->c40_, this->c01_, this->c11_, + this->c21_, this->c31_); + return true; +} + +bool SPA06Component::soft_reset_() { + // Setup steps for SPA06: + // 1. Perform a protocol reset (required to write command for SPI code, noop for I2C) + this->protocol_reset(); + + // 2. Perform the actual reset + this->reset_.bit.fifo_flush = true; + this->reset_.bit.soft_rst = SPA06_SOFT_RESET; + if (!this->spa_write_byte(SPA06_RESET, this->reset_.reg)) { + return false; + } + + // 3. Wait for chip to become ready. Datasheet specifies 2 ms; wait 3 + delay(3); + // 4. Perform another protocol reset (required for SPI code, noop for I2C) + this->protocol_reset(); + return true; +} + +// Temperature conversion formula. See datasheet pg. 14 +float SPA06Component::convert_temperature_(const float &t_raw_sc) { return this->c0_ * 0.5 + this->c1_ * t_raw_sc; } +// Pressure conversion formula. See datasheet pg. 14 +float SPA06Component::convert_pressure_(const float &p_raw_sc, const float &t_raw_sc) { + float p2_raw_sc = p_raw_sc * p_raw_sc; + float p3_raw_sc = p2_raw_sc * p_raw_sc; + float p4_raw_sc = p3_raw_sc * p_raw_sc; + return this->c00_ + (float) this->c10_ * p_raw_sc + (float) this->c20_ * p2_raw_sc + (float) this->c30_ * p3_raw_sc + + (float) this->c40_ * p4_raw_sc + + t_raw_sc * ((float) this->c01_ + (float) this->c11_ * p_raw_sc + (float) this->c21_ * p2_raw_sc + + (float) this->c31_ * p3_raw_sc); +} + +void SPA06Component::update() { + // Verify either a temperature or pressure sensor is defined before proceeding + if ((!this->temperature_sensor_) && (!this->pressure_sensor_)) { + return; + } + + // Queue a background task for retrieving the measurement + this->set_timeout("measurement", this->conversion_time_, [this]() { + float raw_temperature; + float temperature = 0.0; + float pressure = 0.0; + + // Check measurement register for readiness + if (!this->spa_read_byte(SPA06_MEAS_CFG, &this->meas_.reg)) { + ESP_LOGW(TAG, "Cannot read meas config"); + this->status_set_warning(); + return; + } + if (this->pressure_sensor_) { + if (!this->meas_.bit.prs_ready || !this->meas_.bit.tmp_ready) { + ESP_LOGW(TAG, "Temperature and pressure not ready"); + this->status_set_warning(); + return; + } + if (!this->read_temperature_and_pressure_(temperature, pressure, raw_temperature)) { + ESP_LOGW(TAG, "Temperature and pressure read failure"); + this->status_set_warning(); + return; + } + } else { + if (!this->meas_.bit.tmp_ready) { + ESP_LOGW(TAG, "Temperature not ready"); + this->status_set_warning(); + return; + } + if (!this->read_temperature_(temperature, raw_temperature)) { + ESP_LOGW(TAG, "Temperature read fail"); + this->status_set_warning(); + return; + } + } + if (this->temperature_sensor_) { + this->temperature_sensor_->publish_state(temperature); + } else { + ESP_LOGV(TAG, "No temperature sensor configured"); + } + if (this->pressure_sensor_) { + this->pressure_sensor_->publish_state(pressure); + } else { + ESP_LOGV(TAG, "No pressure sensor configured"); + } + this->status_clear_warning(); + }); +} + +bool SPA06Component::read_temperature_and_pressure_(float &temperature, float &pressure, float &t_raw_sc) { + // Temperature read and decode + if (!this->read_temperature_(temperature, t_raw_sc)) { + return false; + } + // Read raw pressure from device + uint8_t buf[3]; + if (!this->spa_read_bytes(SPA06_PSR, buf, 3)) { + return false; + } + // Calculate raw scaled pressure value + float p_raw_sc = (float) decode32(buf[0], buf[1], buf[2], 0, 24) / (float) this->kp_; + + // Calculate full pressure values + pressure = this->convert_pressure_(p_raw_sc, t_raw_sc); + return true; +} + +bool SPA06Component::read_temperature_(float &temperature, float &t_raw_sc) { + uint8_t buf[3]; + if (!this->spa_read_bytes(SPA06_TMP, buf, 3)) { + return false; + } + + t_raw_sc = (float) decode32(buf[0], buf[1], buf[2], 0, 24) / (float) this->kt_; + temperature = this->convert_temperature_(t_raw_sc); + return true; +} +} // namespace esphome::spa06_base diff --git a/esphome/components/spa06_base/spa06_base.h b/esphome/components/spa06_base/spa06_base.h new file mode 100644 index 0000000000..5239e80ec5 --- /dev/null +++ b/esphome/components/spa06_base/spa06_base.h @@ -0,0 +1,257 @@ +// SPA06 interface code for ESPHome +// All datasheet page references refer to Goermicro SPA06-003 datasheet version 2.0 + +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/progmem.h" + +namespace esphome::spa06_base { + +// Read sizes. All other registers are size 1 +constexpr size_t SPA06_MEAS_LEN = 3; +constexpr size_t SPA06_COEF_LEN = 21; + +// Soft reset command (0b1001, 0x9) +constexpr uint8_t SPA06_SOFT_RESET = 0x9; + +// SPA06 Register Addresses +enum Register : uint8_t { + SPA06_PSR = 0x00, // Pressure Reading MSB (or all 3) + SPA06_PSR_B1 = 0x01, // Pressure Reading LSB + SPA06_PSR_B0 = 0x02, // Pressure Reading XLSB (LSB: Pressure flag in FIFO) + SPA06_TMP = 0x03, // Temperature Reading MSB (or all 3) + SPA06_TMP_B1 = 0x04, // Temperature Reading LSB + SPA06_TMP_B0 = 0x05, // Temperature Reading XLSB + SPA06_PSR_CFG = 0x06, // Pressure Configuration + SPA06_TMP_CFG = 0x07, // Temperature Configuration + SPA06_MEAS_CFG = 0x08, // Measurement Configuration (includes readiness) + SPA06_CFG_REG = 0x09, // Configuration Register + SPA06_INT_STS = 0x0A, // Interrupt Status + SPA06_FIFO_STS = 0x0B, // FIFO Status + SPA06_RESET = 0x0C, // Reset + FIFO Flush + SPA06_ID = 0x0D, // Product ID and revision + SPA06_COEF = 0x10, // Coefficients (0x10-0x24) + SPA06_INVALID_CMD = 0x25, // End of enum command +}; + +// Oversampling config. +enum Oversampling : uint8_t { + OVERSAMPLING_NONE = 0x0, + OVERSAMPLING_X2 = 0x1, + OVERSAMPLING_X4 = 0x2, + OVERSAMPLING_X8 = 0x3, + OVERSAMPLING_X16 = 0x4, + OVERSAMPLING_X32 = 0x5, + OVERSAMPLING_X64 = 0x6, + OVERSAMPLING_X128 = 0x7, + OVERSAMPLING_COUNT = 0x8, +}; + +// Measuring rate config +enum SampleRate : uint8_t { + SAMPLE_RATE_1 = 0x0, + SAMPLE_RATE_2 = 0x1, + SAMPLE_RATE_4 = 0x2, + SAMPLE_RATE_8 = 0x3, + SAMPLE_RATE_16 = 0x4, + SAMPLE_RATE_32 = 0x5, + SAMPLE_RATE_64 = 0x6, + SAMPLE_RATE_128 = 0x7, + SAMPLE_RATE_25P16 = 0x8, + SAMPLE_RATE_25P8 = 0x9, + SAMPLE_RATE_25P4 = 0xA, + SAMPLE_RATE_25P2 = 0xB, + SAMPLE_RATE_25 = 0xC, + SAMPLE_RATE_50 = 0xD, + SAMPLE_RATE_100 = 0xE, + SAMPLE_RATE_200 = 0xF, +}; + +// Measuring control config, set in MEAS_CFG register. +// See datasheet pages 28-29 +enum MeasCrtl : uint8_t { + MEASCRTL_IDLE = 0x0, + MEASCRTL_PRES = 0x1, + MEASCRTL_TEMP = 0x2, + MEASCRTL_BG_PRES = 0x5, + MEASCRTL_BG_TEMP = 0x6, + MEASCRTL_BG_BOTH = 0x7, +}; + +// Oversampling scale factors. See datasheet page 15. +constexpr uint32_t OVERSAMPLING_K_LUT[8] = {524288, 1572864, 3670016, 7864320, 253952, 516096, 1040384, 2088960}; +PROGMEM_STRING_TABLE(MeasRateStrings, "1Hz", "2Hz", "4Hz", "8Hz", "16Hz", "32Hz", "64Hz", "128Hz", "1.5625Hz", + "3.125Hz", "6.25Hz", "12.5Hz", "25Hz", "50Hz", "100Hz", "200Hz"); +PROGMEM_STRING_TABLE(OversamplingStrings, "X1", "X2", "X4", "X8", "X16", "X32", "X64", "X128"); + +inline static const LogString *oversampling_to_str(const Oversampling oversampling) { + return OversamplingStrings::get_log_str(static_cast(oversampling), OversamplingStrings::LAST_INDEX); +} +inline static const LogString *meas_rate_to_str(SampleRate rate) { + return MeasRateStrings::get_log_str(static_cast(rate), MeasRateStrings::LAST_INDEX); +} +inline uint32_t oversampling_to_scale_factor(const Oversampling oversampling) { + return OVERSAMPLING_K_LUT[static_cast(oversampling)]; +}; + +class SPA06Component : public PollingComponent { + public: + //// Standard ESPHome component class functions + void setup() override; + void update() override; + void dump_config() override; + + //// ESPHome-side settings + void set_conversion_time(uint16_t conversion_time) { this->conversion_time_ = conversion_time; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; } + void set_temperature_oversampling_config(Oversampling temperature_oversampling) { + this->temperature_oversampling_ = temperature_oversampling; + this->kt_ = oversampling_to_scale_factor(temperature_oversampling); + } + void set_pressure_oversampling_config(Oversampling pressure_oversampling) { + this->pressure_oversampling_ = pressure_oversampling; + this->kp_ = oversampling_to_scale_factor(pressure_oversampling); + } + void set_pressure_sample_rate_config(SampleRate rate) { this->pressure_rate_ = rate; } + void set_temperature_sample_rate_config(SampleRate rate) { this->temperature_rate_ = rate; } + + protected: + // Virtual read functions. Implemented in SPI/I2C components + virtual bool spa_read_byte(uint8_t reg, uint8_t *data) = 0; + virtual bool spa_write_byte(uint8_t reg, uint8_t data) = 0; + virtual bool spa_read_bytes(uint8_t reg, uint8_t *data, size_t len) = 0; + virtual bool spa_write_bytes(uint8_t reg, uint8_t *data, size_t len) = 0; + + //// Protocol-specific read functions + // Soft reset + bool soft_reset_(); + // Protocol-specific reset (used for SPI only, implemented as noop for I2C) + virtual void protocol_reset() {} + // Read temperature and calculate Celsius and scaled raw temperatures + bool read_temperature_(float &temperature, float &t_raw_sc); + // No pressure only read! Pressure calculation depends on scaled temperature value + // Read temperature and calculate Celsius temperature, Pascal pressure, and scaled raw temperature + bool read_temperature_and_pressure_(float &temperature, float &pressure, float &t_raw_sc); + // Read coefficients. Stores in class variables. + bool read_coefficients_(); + + //// Protocol-specific write functions + // Write temperature settings to TMP_CFG register + bool write_temperature_settings_(Oversampling oversampling, SampleRate rate); + // Write pressure settings to PRS_CFG register + bool write_pressure_settings_(Oversampling oversampling, SampleRate rate); + // Write measurement settings to MEAS_CRTL register + bool write_measurement_settings_(MeasCrtl crtl); + + // Write communication settings to CFG_REG register + // Set pressure_shift to true if pressure oversampling >X8 + // Set temperature_shift to true if temperature oversampling >X8 + bool write_communication_settings_(bool pressure_shift, bool temperature_shift, bool interrupt_hl = false, + bool interrupt_fifo = false, bool interrupt_tmp = false, + bool interrupt_prs = false, bool enable_fifo = false, bool spi_3wire = false); + + //// Protocol helper functions + // Write function for both temperature and pressure (deduplicates code) + bool write_sensor_settings_(Oversampling oversampling, SampleRate rate, uint8_t reg); + // Convert raw temperature reading into Celsius + float convert_temperature_(const float &t_raw_sc); + // Convert raw pressure and scaled raw temperature into Pascals + float convert_pressure_(const float &p_raw_sc, const float &t_raw_sc); + + //// Protocol-related variables + // Oversampling scale factors. Defaults are for X16 (pressure) and X1 (temp) + uint32_t kp_{253952}, kt_{524288}; + // Coefficients for calculating pressure and temperature from raw values + // Obtained from IC during setup + int32_t c00_{0}, c10_{0}; + int16_t c0_{0}, c1_{0}, c01_{0}, c11_{0}, c20_{0}, c21_{0}, c30_{0}, c31_{0}, c40_{0}; + + //// ESPHome class objects and configuration + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + Oversampling temperature_oversampling_{Oversampling::OVERSAMPLING_NONE}; + Oversampling pressure_oversampling_{Oversampling::OVERSAMPLING_X16}; + SampleRate temperature_rate_{SampleRate::SAMPLE_RATE_1}; + SampleRate pressure_rate_{SampleRate::SAMPLE_RATE_1}; + // Default conversion time: 27.6ms (16x pres) + 3.6ms (1x temp) ~ 32ms + uint16_t conversion_time_{32}; + + union { + struct { + Oversampling prc : 4; + SampleRate rate : 4; + } bit; + uint8_t reg; + } pt_meas_cfg_ = {.reg = 0}; // PRS_CFG and TMP_CFG + + union { + struct { + uint8_t meas_crtl : 3; + bool tmp_ext : 1; + bool prs_ready : 1; + bool tmp_ready : 1; + bool sensor_ready : 1; + bool coef_ready : 1; + } bit; + uint8_t reg; + } meas_ = {.reg = 0}; // MEAS_REG + + union { + struct { + uint8_t _reserved : 5; + bool int_prs : 1; + bool int_tmp : 1; + bool int_fifo_full : 1; + } bit; + uint8_t reg; + } int_status_ = {.reg = 0}; // INT_STS + + union { + struct { + bool spi_3wire : 1; + bool fifo_en : 1; + bool p_shift : 1; + bool t_shift : 1; + bool int_prs : 1; + bool int_tmp : 1; + bool int_fifo : 1; + bool int_hl : 1; + } bit; + uint8_t reg; + } cfg_ = {.reg = 0}; // CFG_REG + + union { + struct { + bool fifo_empty : 1; + bool fifo_full : 1; + uint8_t _reserved : 6; + } bit; + uint8_t reg; + } fifo_sts_ = {.reg = 0}; // FIFO_STS + + union { + struct { + // Set to true to flush FIFO + bool fifo_flush : 1; + // Reserved bits + uint8_t _reserved : 3; + // Soft reset. Set to 1001 (0x9) to perform reset. + uint8_t soft_rst : 4; + } bit; + uint8_t reg = 0; + } reset_ = {.reg = 0}; // RESET + + union { + struct { + uint8_t prod_id : 4; + uint8_t rev_id : 4; + } bit; + uint8_t reg = 0; + } prod_id_ = {.reg = 0}; // ID + +}; // class SPA06Component +} // namespace esphome::spa06_base diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index d1f7452054..0802cdec8e 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -335,8 +335,8 @@ Sprinkler::Sprinkler(const char *name) : name_(name) { // The `name` is stored for dump_config logging this->timer_.init(2); // Timer names only need to be unique within this component instance - this->timer_.push_back({"sm", false, 0, 0, std::bind(&Sprinkler::sm_timer_callback_, this)}); - this->timer_.push_back({"vs", false, 0, 0, std::bind(&Sprinkler::valve_selection_callback_, this)}); + this->timer_.push_back({"sm", false, 0, 0, [this]() { this->sm_timer_callback_(); }}); + this->timer_.push_back({"vs", false, 0, 0, [this]() { this->valve_selection_callback_(); }}); } void Sprinkler::setup() { diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 7ffa408db9..9821046a73 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -59,15 +59,20 @@ _DST_RULE_TYPE_MAP = { def _load_tzdata(iana_key: str) -> bytes | None: # From https://tzdata.readthedocs.io/en/latest/#examples + if not iana_key: + return None try: package_loc, resource = iana_key.rsplit("/", 1) except ValueError: - return None - package = "tzdata.zoneinfo." + package_loc.replace("/", ".") + # Handle top-level timezone entries like "UTC", "GMT" + package = "tzdata.zoneinfo" + resource = iana_key + else: + package = "tzdata.zoneinfo." + package_loc.replace("/", ".") try: return (resources.files(package) / resource).read_bytes() - except (FileNotFoundError, ModuleNotFoundError): + except (FileNotFoundError, ModuleNotFoundError, IsADirectoryError): return None diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 4c4150e44d..b049a0413c 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -629,8 +629,8 @@ void WiFiComponent::wifi_pre_setup_() { return; } - auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2); - WiFi.onEvent(f); + WiFi.onEvent( + [this](arduino_event_id_t event, arduino_event_info_t info) { this->wifi_event_callback_(event, info); }); // Make sure WiFi is in clean state before anything starts this->wifi_mode_(false, false); } diff --git a/esphome/components/wireguard/wireguard.cpp b/esphome/components/wireguard/wireguard.cpp index 2022e25b6c..b4641894db 100644 --- a/esphome/components/wireguard/wireguard.cpp +++ b/esphome/components/wireguard/wireguard.cpp @@ -2,7 +2,6 @@ #ifdef USE_WIREGUARD #include #include -#include #include "esphome/core/application.h" #include "esphome/core/log.h" @@ -48,8 +47,8 @@ void Wireguard::setup() { if (this->wg_initialized_ == ESP_OK) { ESP_LOGI(TAG, "Initialized"); this->wg_peer_offline_time_ = millis(); - this->srctime_->add_on_time_sync_callback(std::bind(&Wireguard::start_connection_, this)); - this->defer(std::bind(&Wireguard::start_connection_, this)); // defer to avoid blocking setup + this->srctime_->add_on_time_sync_callback([this]() { this->start_connection_(); }); + this->defer([this]() { this->start_connection_(); }); // defer to avoid blocking setup #ifdef USE_TEXT_SENSOR if (this->address_sensor_ != nullptr) { @@ -206,7 +205,7 @@ void Wireguard::enable() { void Wireguard::disable() { this->enabled_ = false; - this->defer(std::bind(&Wireguard::stop_connection_, this)); // defer to avoid blocking running loop + this->defer([this]() { this->stop_connection_(); }); // defer to avoid blocking running loop ESP_LOGI(TAG, "Disabled"); this->publish_enabled_state(); } diff --git a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp index ee3ad316e1..0f27f09c87 100644 --- a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp +++ b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp @@ -58,15 +58,13 @@ bool XiaomiRTCGQ02LM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) #ifdef USE_BINARY_SENSOR if (res->has_motion.has_value() && this->motion_ != nullptr) { this->motion_->publish_state(*res->has_motion); - this->set_timeout("motion_timeout", this->motion_timeout_, - [this, res]() { this->motion_->publish_state(false); }); + this->set_timeout("motion_timeout", this->motion_timeout_, [this]() { this->motion_->publish_state(false); }); } if (res->is_light.has_value() && this->light_ != nullptr) this->light_->publish_state(*res->is_light); if (res->button_press.has_value() && this->button_ != nullptr) { this->button_->publish_state(*res->button_press); - this->set_timeout("button_timeout", this->button_timeout_, - [this, res]() { this->button_->publish_state(false); }); + this->set_timeout("button_timeout", this->button_timeout_, [this]() { this->button_->publish_state(false); }); } #endif #ifdef USE_SENSOR diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 32689dab27..56f255a076 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -494,6 +494,13 @@ def hex_int(value): return HexInt(int_(value)) +def int_to_hex_string(value: int | str) -> str: + """Convert an integer to a hex string (e.g. 64 -> '0x40'). Pass-through strings.""" + if isinstance(value, int): + return f"0x{value:X}" + return value + + def int_(value): """Validate that the config option is an integer. diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 67e1755cc9..985f26e711 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -188,7 +188,7 @@ template class DelayAction : public Action, public Compon // Issue #10264: This is a workaround for parallel script delays interfering with each other. // Optimization: For no-argument delays (most common case), use direct lambda - // instead of std::bind to avoid bind overhead (~16 bytes heap + faster execution) + // to avoid overhead from capturing arguments by value if constexpr (sizeof...(Ts) == 0) { App.scheduler.set_timer_common_( this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, nullptr, @@ -196,9 +196,9 @@ template class DelayAction : public Action, public Compon [this]() { this->play_next_(); }, /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); } else { - // For delays with arguments, use std::bind to preserve argument values + // For delays with arguments, capture by value to preserve argument values // Arguments must be copied because original references may be invalid after delay - auto f = std::bind(&DelayAction::play_next_, this, x...); + auto f = [this, x...]() { this->play_next_(x...); }; App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast(InternalSchedulerID::DELAY_ACTION), this->delay_.value(x...), std::move(f), diff --git a/requirements.txt b/requirements.txt index f8f60f1932..9e2f4efe3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.6.0 +aioesphomeapi==44.6.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import diff --git a/tests/components/esp32/test.esp32-s3-idf.yaml b/tests/components/esp32/test.esp32-s3-idf.yaml index 7a3bbe55b3..b9a3b804a8 100644 --- a/tests/components/esp32/test.esp32-s3-idf.yaml +++ b/tests/components/esp32/test.esp32-s3-idf.yaml @@ -1,5 +1,10 @@ esp32: variant: esp32s3 + partitions: + - name: my_data + type: data + subtype: spiffs + size: 0x1000 framework: type: esp-idf advanced: diff --git a/tests/components/remote_receiver/common-actions.yaml b/tests/components/remote_receiver/common-actions.yaml index de01fa3602..30b99eeb70 100644 --- a/tests/components/remote_receiver/common-actions.yaml +++ b/tests/components/remote_receiver/common-actions.yaml @@ -8,6 +8,11 @@ on_beo4: - logger.log: format: "on_beo4: %u %u" args: ["x.source", "x.command"] +on_brennenstuhl: + then: + - logger.log: + format: "on_brennenstuhl: %u" + args: ["x.code"] on_aeha: then: - logger.log: diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml index d48d36bd54..c6c7049605 100644 --- a/tests/components/remote_transmitter/common-buttons.yaml +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -14,6 +14,12 @@ button: remote_transmitter.transmit_beo4: source: 0x01 command: 0x0C + - platform: template + name: brennenstuhl + id: button_a_on + on_press: + remote_transmitter.transmit_brennenstuhl: + code: 0xBD2E2C - platform: template name: Dyson fan up id: dyson_fan_up