diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f043cc5ca6..15edd8421a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 + uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 + uses: github/codeql-action/analyze@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11 with: category: "/language:${{matrix.language}}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 06f9bf2a5b..b068673ecf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.13 + rev: v0.14.14 hooks: # Run the linter. - id: ruff diff --git a/CODEOWNERS b/CODEOWNERS index 8a37aeb29f..8537d451db 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -88,7 +88,8 @@ esphome/components/bmp3xx/* @latonita esphome/components/bmp3xx_base/* @latonita @martgras esphome/components/bmp3xx_i2c/* @latonita esphome/components/bmp3xx_spi/* @latonita -esphome/components/bmp581/* @kahrendt +esphome/components/bmp581_base/* @danielkent-net @kahrendt +esphome/components/bmp581_i2c/* @danielkent-net @kahrendt esphome/components/bp1658cj/* @Cossid esphome/components/bp5758d/* @Cossid esphome/components/bthome_mithermometer/* @nagyrobi diff --git a/esphome/components/bl0940/number/calibration_number.cpp b/esphome/components/bl0940/number/calibration_number.cpp index e83c3add1f..5e775004bd 100644 --- a/esphome/components/bl0940/number/calibration_number.cpp +++ b/esphome/components/bl0940/number/calibration_number.cpp @@ -9,7 +9,7 @@ static const char *const TAG = "bl0940.number"; void CalibrationNumber::setup() { float value = 0.0f; if (this->restore_value_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (!this->pref_.load(&value)) { value = 0.0f; } diff --git a/esphome/components/bmp581/sensor.py b/esphome/components/bmp581/sensor.py index e2790f83b9..0dd06bfd36 100644 --- a/esphome/components/bmp581/sensor.py +++ b/esphome/components/bmp581/sensor.py @@ -1,164 +1,5 @@ -import math - -import esphome.codegen as cg -from esphome.components import i2c, sensor import esphome.config_validation as cv -from esphome.const import ( - CONF_ID, - CONF_IIR_FILTER, - CONF_OVERSAMPLING, - CONF_PRESSURE, - CONF_TEMPERATURE, - DEVICE_CLASS_ATMOSPHERIC_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, - UNIT_CELSIUS, - UNIT_PASCAL, + +CONFIG_SCHEMA = cv.invalid( + "The bmp581 sensor component has been renamed to bmp581_i2c." ) - -CODEOWNERS = ["@kahrendt"] -DEPENDENCIES = ["i2c"] - -bmp581_ns = cg.esphome_ns.namespace("bmp581") - -Oversampling = bmp581_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, -} - -IIRFilter = bmp581_ns.enum("IIRFilter") -IIR_FILTER_OPTIONS = { - "OFF": IIRFilter.IIR_FILTER_OFF, - "2X": IIRFilter.IIR_FILTER_2, - "4X": IIRFilter.IIR_FILTER_4, - "8X": IIRFilter.IIR_FILTER_8, - "16X": IIRFilter.IIR_FILTER_16, - "32X": IIRFilter.IIR_FILTER_32, - "64X": IIRFilter.IIR_FILTER_64, - "128X": IIRFilter.IIR_FILTER_128, -} - -BMP581Component = bmp581_ns.class_( - "BMP581Component", cg.PollingComponent, i2c.I2CDevice -) - - -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 - - # Page 12 of datasheet - PRESSURE_OVERSAMPLING_CONVERSION_TIMES = { - "NONE": 1.0, - "2X": 1.7, - "4X": 2.9, - "8X": 5.4, - "16X": 10.4, - "32X": 20.4, - "64X": 40.4, - "128X": 80.4, - } - - # Page 12 of datasheet - TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES = { - "NONE": 1.0, - "2X": 1.1, - "4X": 1.5, - "8X": 2.1, - "16X": 3.3, - "32X": 5.8, - "64X": 10.8, - "128X": 20.8, - } - - pressure_conversion_time = ( - 0.0 # No conversion time necessary without a pressure sensor - ) - if pressure_config := config.get(CONF_PRESSURE): - pressure_conversion_time = PRESSURE_OVERSAMPLING_CONVERSION_TIMES[ - pressure_config.get(CONF_OVERSAMPLING) - ] - - temperature_conversion_time = ( - 1.0 # BMP581 always samples the temperature even if only reading pressure - ) - if temperature_config := config.get(CONF_TEMPERATURE): - temperature_conversion_time = TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES[ - temperature_config.get(CONF_OVERSAMPLING) - ] - - # Datasheet indicates a 5% possible error in each conversion time listed - return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time)) - - -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(BMP581Component), - 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_IIR_FILTER, default="OFF"): cv.enum( - IIR_FILTER_OPTIONS, upper=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_IIR_FILTER, default="OFF"): cv.enum( - IIR_FILTER_OPTIONS, upper=True - ), - } - ), - } - ) - .extend(cv.polling_component_schema("60s")) - .extend(i2c.i2c_device_schema(0x46)) -) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await i2c.register_i2c_device(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_iir_filter_config(temperature_config[CONF_IIR_FILTER]) - ) - - 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_iir_filter_config(pressure_config[CONF_IIR_FILTER])) - - cg.add(var.set_conversion_time(compute_measurement_conversion_time(config))) diff --git a/esphome/components/bmp581_base/__init__.py b/esphome/components/bmp581_base/__init__.py new file mode 100644 index 0000000000..6a7cf45089 --- /dev/null +++ b/esphome/components/bmp581_base/__init__.py @@ -0,0 +1,157 @@ +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_IIR_FILTER, + CONF_OVERSAMPLING, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PASCAL, +) + +CODEOWNERS = ["@kahrendt", "@danielkent-net"] + +bmp581_ns = cg.esphome_ns.namespace("bmp581_base") + +Oversampling = bmp581_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, +} + +IIRFilter = bmp581_ns.enum("IIRFilter") +IIR_FILTER_OPTIONS = { + "OFF": IIRFilter.IIR_FILTER_OFF, + "2X": IIRFilter.IIR_FILTER_2, + "4X": IIRFilter.IIR_FILTER_4, + "8X": IIRFilter.IIR_FILTER_8, + "16X": IIRFilter.IIR_FILTER_16, + "32X": IIRFilter.IIR_FILTER_32, + "64X": IIRFilter.IIR_FILTER_64, + "128X": IIRFilter.IIR_FILTER_128, +} + +BMP581Component = bmp581_ns.class_("BMP581Component", cg.PollingComponent) + + +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 + + # Page 12 of datasheet + PRESSURE_OVERSAMPLING_CONVERSION_TIMES = { + "NONE": 1.0, + "2X": 1.7, + "4X": 2.9, + "8X": 5.4, + "16X": 10.4, + "32X": 20.4, + "64X": 40.4, + "128X": 80.4, + } + + # Page 12 of datasheet + TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES = { + "NONE": 1.0, + "2X": 1.1, + "4X": 1.5, + "8X": 2.1, + "16X": 3.3, + "32X": 5.8, + "64X": 10.8, + "128X": 20.8, + } + + pressure_conversion_time = ( + 0.0 # No conversion time necessary without a pressure sensor + ) + if pressure_config := config.get(CONF_PRESSURE): + pressure_conversion_time = PRESSURE_OVERSAMPLING_CONVERSION_TIMES[ + pressure_config.get(CONF_OVERSAMPLING) + ] + + temperature_conversion_time = ( + 1.0 # BMP581 always samples the temperature even if only reading pressure + ) + if temperature_config := config.get(CONF_TEMPERATURE): + temperature_conversion_time = TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES[ + temperature_config.get(CONF_OVERSAMPLING) + ] + + # Datasheet indicates a 5% possible error in each conversion time listed + return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time)) + + +CONFIG_SCHEMA_BASE = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BMP581Component), + 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_IIR_FILTER, default="OFF"): cv.enum( + IIR_FILTER_OPTIONS, upper=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_IIR_FILTER, default="OFF"): cv.enum( + IIR_FILTER_OPTIONS, upper=True + ), + } + ), + } +).extend(cv.polling_component_schema("60s")) + + +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_iir_filter_config(temperature_config[CONF_IIR_FILTER]) + ) + + 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_iir_filter_config(pressure_config[CONF_IIR_FILTER])) + + cg.add(var.set_conversion_time(compute_measurement_conversion_time(config))) + return var diff --git a/esphome/components/bmp581/bmp581.cpp b/esphome/components/bmp581_base/bmp581_base.cpp similarity index 95% rename from esphome/components/bmp581/bmp581.cpp rename to esphome/components/bmp581_base/bmp581_base.cpp index 301fc31df0..67c6771862 100644 --- a/esphome/components/bmp581/bmp581.cpp +++ b/esphome/components/bmp581_base/bmp581_base.cpp @@ -10,12 +10,11 @@ * - All datasheet page references refer to Bosch Document Number BST-BMP581-DS004-04 (revision number 1.4) */ -#include "bmp581.h" +#include "bmp581_base.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace bmp581 { +namespace esphome::bmp581_base { static const char *const TAG = "bmp581"; @@ -91,7 +90,6 @@ void BMP581Component::dump_config() { break; } - LOG_I2C_DEVICE(this); LOG_UPDATE_INTERVAL(this); ESP_LOGCONFIG(TAG, " Measurement conversion time: %ums", this->conversion_time_); @@ -149,7 +147,7 @@ void BMP581Component::setup() { uint8_t chip_id; // read chip id from sensor - if (!this->read_byte(BMP581_CHIP_ID, &chip_id)) { + if (!this->bmp_read_byte(BMP581_CHIP_ID, &chip_id)) { ESP_LOGE(TAG, "Read chip ID failed"); this->error_code_ = ERROR_COMMUNICATION_FAILED; @@ -172,7 +170,7 @@ void BMP581Component::setup() { // 3) Verify sensor status (check if NVM is okay) // //////////////////////////////////////////////////// - if (!this->read_byte(BMP581_STATUS, &this->status_.reg)) { + if (!this->bmp_read_byte(BMP581_STATUS, &this->status_.reg)) { ESP_LOGE(TAG, "Failed to read status register"); this->error_code_ = ERROR_COMMUNICATION_FAILED; @@ -359,7 +357,7 @@ bool BMP581Component::check_data_readiness_() { uint8_t status; - if (!this->read_byte(BMP581_INT_STATUS, &status)) { + if (!this->bmp_read_byte(BMP581_INT_STATUS, &status)) { ESP_LOGE(TAG, "Failed to read interrupt status register"); return false; } @@ -400,7 +398,7 @@ bool BMP581Component::prime_iir_filter_() { // flush the IIR filter with forced measurements (we will only flush once) this->dsp_config_.bit.iir_flush_forced_en = true; - if (!this->write_byte(BMP581_DSP, this->dsp_config_.reg)) { + if (!this->bmp_write_byte(BMP581_DSP, this->dsp_config_.reg)) { ESP_LOGE(TAG, "Failed to write IIR source register"); return false; @@ -430,7 +428,7 @@ bool BMP581Component::prime_iir_filter_() { // disable IIR filter flushings on future forced measurements this->dsp_config_.bit.iir_flush_forced_en = false; - if (!this->write_byte(BMP581_DSP, this->dsp_config_.reg)) { + if (!this->bmp_write_byte(BMP581_DSP, this->dsp_config_.reg)) { ESP_LOGE(TAG, "Failed to write IIR source register"); return false; @@ -454,7 +452,7 @@ bool BMP581Component::read_temperature_(float &temperature) { } uint8_t data[3]; - if (!this->read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 3)) { + if (!this->bmp_read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 3)) { ESP_LOGW(TAG, "Failed to read measurement"); this->status_set_warning(); @@ -483,7 +481,7 @@ bool BMP581Component::read_temperature_and_pressure_(float &temperature, float & } uint8_t data[6]; - if (!this->read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 6)) { + if (!this->bmp_read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 6)) { ESP_LOGW(TAG, "Failed to read measurement"); this->status_set_warning(); @@ -507,7 +505,7 @@ bool BMP581Component::reset_() { // - returns the Power-On-Reboot interrupt status, which is asserted if successful // writes reset command to BMP's command register - if (!this->write_byte(BMP581_COMMAND, RESET_COMMAND)) { + if (!this->bmp_write_byte(BMP581_COMMAND, RESET_COMMAND)) { ESP_LOGE(TAG, "Failed to write reset command"); return false; @@ -518,7 +516,7 @@ bool BMP581Component::reset_() { delay(3); // read interrupt status register - if (!this->read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) { + if (!this->bmp_read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) { ESP_LOGE(TAG, "Failed to read interrupt status register"); return false; @@ -562,7 +560,7 @@ bool BMP581Component::write_iir_settings_(IIRFilter temperature_iir, IIRFilter p // BMP581_DSP register and BMP581_DSP_IIR registers are successive // - allows us to write the IIR configuration with one command to both registers uint8_t register_data[2] = {this->dsp_config_.reg, this->iir_config_.reg}; - return this->write_bytes(BMP581_DSP, register_data, sizeof(register_data)); + return this->bmp_write_bytes(BMP581_DSP, register_data, sizeof(register_data)); } bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) { @@ -572,7 +570,7 @@ bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) { this->int_source_.bit.drdy_data_reg_en = data_ready_enable; // write interrupt source register - return this->write_byte(BMP581_INT_SOURCE, this->int_source_.reg); + return this->bmp_write_byte(BMP581_INT_SOURCE, this->int_source_.reg); } bool BMP581Component::write_oversampling_settings_(Oversampling temperature_oversampling, @@ -583,7 +581,7 @@ bool BMP581Component::write_oversampling_settings_(Oversampling temperature_over this->osr_config_.bit.osr_t = temperature_oversampling; this->osr_config_.bit.osr_p = pressure_oversampling; - return this->write_byte(BMP581_OSR, this->osr_config_.reg); + return this->bmp_write_byte(BMP581_OSR, this->osr_config_.reg); } bool BMP581Component::write_power_mode_(OperationMode mode) { @@ -593,8 +591,7 @@ bool BMP581Component::write_power_mode_(OperationMode mode) { this->odr_config_.bit.pwr_mode = mode; // write odr register - return this->write_byte(BMP581_ODR, this->odr_config_.reg); + return this->bmp_write_byte(BMP581_ODR, this->odr_config_.reg); } -} // namespace bmp581 -} // namespace esphome +} // namespace esphome::bmp581_base diff --git a/esphome/components/bmp581/bmp581.h b/esphome/components/bmp581_base/bmp581_base.h similarity index 95% rename from esphome/components/bmp581/bmp581.h rename to esphome/components/bmp581_base/bmp581_base.h index 1d7e932fa1..d99c420272 100644 --- a/esphome/components/bmp581/bmp581.h +++ b/esphome/components/bmp581_base/bmp581_base.h @@ -3,11 +3,9 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/components/i2c/i2c.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace bmp581 { +namespace esphome::bmp581_base { static const uint8_t BMP581_ASIC_ID = 0x50; // BMP581's ASIC chip ID (page 51 of datasheet) static const uint8_t RESET_COMMAND = 0xB6; // Soft reset command @@ -59,7 +57,7 @@ enum IIRFilter { IIR_FILTER_128 = 0x7 }; -class BMP581Component : public PollingComponent, public i2c::I2CDevice { +class BMP581Component : public PollingComponent { public: void dump_config() override; @@ -84,6 +82,11 @@ class BMP581Component : public PollingComponent, public i2c::I2CDevice { void set_conversion_time(uint8_t conversion_time) { this->conversion_time_ = conversion_time; } protected: + virtual bool bmp_read_byte(uint8_t a_register, uint8_t *data) = 0; + virtual bool bmp_write_byte(uint8_t a_register, uint8_t data) = 0; + virtual bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; + virtual bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; + sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *pressure_sensor_{nullptr}; @@ -216,5 +219,4 @@ class BMP581Component : public PollingComponent, public i2c::I2CDevice { } odr_config_ = {.reg = 0}; }; -} // namespace bmp581 -} // namespace esphome +} // namespace esphome::bmp581_base diff --git a/esphome/components/bmp581_i2c/__init__.py b/esphome/components/bmp581_i2c/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/bmp581_i2c/bmp581_i2c.cpp b/esphome/components/bmp581_i2c/bmp581_i2c.cpp new file mode 100644 index 0000000000..8df4610e0b --- /dev/null +++ b/esphome/components/bmp581_i2c/bmp581_i2c.cpp @@ -0,0 +1,12 @@ +#include "bmp581_i2c.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome::bmp581_i2c { + +void BMP581I2CComponent::dump_config() { + LOG_I2C_DEVICE(this); + BMP581Component::dump_config(); +} + +} // namespace esphome::bmp581_i2c diff --git a/esphome/components/bmp581_i2c/bmp581_i2c.h b/esphome/components/bmp581_i2c/bmp581_i2c.h new file mode 100644 index 0000000000..a4e43daf64 --- /dev/null +++ b/esphome/components/bmp581_i2c/bmp581_i2c.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/components/bmp581_base/bmp581_base.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome::bmp581_i2c { + +static const char *const TAG = "bmp581_i2c.sensor"; + +/// This class implements support for the BMP581 Temperature+Pressure i2c sensor. +class BMP581I2CComponent : public esphome::bmp581_base::BMP581Component, public i2c::I2CDevice { + public: + bool bmp_read_byte(uint8_t a_register, uint8_t *data) override { return read_byte(a_register, data); } + bool bmp_write_byte(uint8_t a_register, uint8_t data) override { return write_byte(a_register, data); } + bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override { + return read_bytes(a_register, data, len); + } + bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) override { + return write_bytes(a_register, data, len); + } + void dump_config() override; +}; + +} // namespace esphome::bmp581_i2c diff --git a/esphome/components/bmp581_i2c/sensor.py b/esphome/components/bmp581_i2c/sensor.py new file mode 100644 index 0000000000..42645022a6 --- /dev/null +++ b/esphome/components/bmp581_i2c/sensor.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv + +from ..bmp581_base import CONFIG_SCHEMA_BASE, to_code_base + +AUTO_LOAD = ["bmp581_base"] +CODEOWNERS = ["@kahrendt", "@danielkent-net"] +DEPENDENCIES = ["i2c"] + +bmp581_ns = cg.esphome_ns.namespace("bmp581_i2c") +BMP581I2CComponent = bmp581_ns.class_( + "BMP581I2CComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend( + i2c.i2c_device_schema(default_address=0x46) +).extend({cv.GenerateID(): cv.declare_id(BMP581I2CComponent)}) + + +async def to_code(config): + var = await to_code_base(config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 816bd5dfcb..ba6de4ff61 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -360,8 +360,7 @@ void Climate::add_on_control_callback(std::function &&callb static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL; optional Climate::restore_state_() { - this->rtc_ = global_preferences->make_preference(this->get_preference_hash() ^ - RESTORE_STATE_VERSION); + this->rtc_ = this->make_entity_preference(RESTORE_STATE_VERSION); ClimateDeviceRestoreState recovered{}; if (!this->rtc_.load(&recovered)) return {}; diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 97b8c2213e..88cca077dd 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -187,7 +187,7 @@ void Cover::publish_state(bool save) { } } optional Cover::restore_state_() { - this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); + this->rtc_ = this->make_entity_preference(); CoverRestoreState recovered{}; if (!this->rtc_.load(&recovered)) return {}; diff --git a/esphome/components/duty_time/duty_time_sensor.cpp b/esphome/components/duty_time/duty_time_sensor.cpp index f77f1fcf53..561040623d 100644 --- a/esphome/components/duty_time/duty_time_sensor.cpp +++ b/esphome/components/duty_time/duty_time_sensor.cpp @@ -41,7 +41,7 @@ void DutyTimeSensor::setup() { uint32_t seconds = 0; if (this->restore_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); this->pref_.load(&seconds); } diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 799e9ba233..ebec7a196b 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -227,8 +227,7 @@ void Fan::publish_state() { constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA; optional Fan::restore_state_() { FanRestoreState recovered{}; - this->rtc_ = - global_preferences->make_preference(this->get_preference_hash() ^ RESTORE_STATE_VERSION); + this->rtc_ = this->make_entity_preference(RESTORE_STATE_VERSION); bool restored = this->rtc_.load(&recovered); switch (this->restore_mode_) { diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index cd2673a272..1882aa439e 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -350,8 +350,7 @@ ClimateTraits HaierClimateBase::traits() { return traits_; } void HaierClimateBase::initialization() { constexpr uint32_t restore_settings_version = 0xA77D21EF; - this->base_rtc_ = - global_preferences->make_preference(this->get_preference_hash() ^ restore_settings_version); + this->base_rtc_ = this->make_entity_preference(restore_settings_version); HaierBaseSettings recovered; if (!this->base_rtc_.load(&recovered)) { recovered = {false, true}; diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 23d28bfd47..d98d273957 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -515,8 +515,7 @@ haier_protocol::HaierMessage HonClimate::get_power_message(bool state) { void HonClimate::initialization() { HaierClimateBase::initialization(); constexpr uint32_t restore_settings_version = 0x57EB59DDUL; - this->hon_rtc_ = - global_preferences->make_preference(this->get_preference_hash() ^ restore_settings_version); + this->hon_rtc_ = this->make_entity_preference(restore_settings_version); HonSettings recovered; if (this->hon_rtc_.load(&recovered)) { this->settings_ = recovered; diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index da4e3215b3..bc19ee18aa 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -185,7 +185,7 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s } jobs[num_jobs++].command = I2C_MASTER_CMD_STOP; ESP_LOGV(TAG, "Sending %zu jobs", num_jobs); - esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 20); + esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 100); if (err == ESP_ERR_INVALID_STATE) { ESP_LOGV(TAG, "TX to %02X failed: not acked", address); return ERROR_NOT_ACKNOWLEDGED; diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp index 80c718dc8d..b084801d3b 100644 --- a/esphome/components/integration/integration_sensor.cpp +++ b/esphome/components/integration/integration_sensor.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "integration"; void IntegrationSensor::setup() { if (this->restore_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); float preference_value = 0; this->pref_.load(&preference_value); this->result_ = preference_value; diff --git a/esphome/components/ir_rf_proxy/ir_rf_proxy.h b/esphome/components/ir_rf_proxy/ir_rf_proxy.h index d7c8919def..f067a6e17a 100644 --- a/esphome/components/ir_rf_proxy/ir_rf_proxy.h +++ b/esphome/components/ir_rf_proxy/ir_rf_proxy.h @@ -5,8 +5,6 @@ // Once the API is considered stable, this warning will be removed. #include "esphome/components/infrared/infrared.h" -#include "esphome/components/remote_transmitter/remote_transmitter.h" -#include "esphome/components/remote_receiver/remote_receiver.h" namespace esphome::ir_rf_proxy { diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 58d469b2a7..07809023cd 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -184,7 +184,7 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui void LD2450Component::setup() { #ifdef USE_NUMBER if (this->presence_timeout_number_ != nullptr) { - this->pref_ = global_preferences->make_preference(this->presence_timeout_number_->get_preference_hash()); + this->pref_ = this->presence_timeout_number_->make_entity_preference(); this->set_presence_timeout(); } #endif diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 234d641f0d..6d42dd1513 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -391,7 +391,10 @@ void LightCall::transform_parameters_() { min_mireds > 0.0f && max_mireds > 0.0f) { ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values", this->parent_->get_name().c_str()); - if (this->has_color_temperature()) { + // Only compute cold_white/warm_white from color_temperature if they're not already explicitly set. + // This is important for state restoration, where both color_temperature and cold_white/warm_white + // are restored from flash - we want to preserve the saved cold_white/warm_white values. + if (this->has_color_temperature() && !this->has_cold_white() && !this->has_warm_white()) { const float color_temp = clamp(this->color_temperature_, min_mireds, max_mireds); const float range = max_mireds - min_mireds; const float ww_fraction = (color_temp - min_mireds) / range; diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 91bb2e2f1f..ed86bf58da 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -44,7 +44,7 @@ void LightState::setup() { case LIGHT_RESTORE_DEFAULT_ON: case LIGHT_RESTORE_INVERTED_DEFAULT_OFF: case LIGHT_RESTORE_INVERTED_DEFAULT_ON: - this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); + this->rtc_ = this->make_entity_preference(); // Attempt to load from preferences, else fall back to default values if (!this->rtc_.load(&recovered)) { recovered.state = (this->restore_mode_ == LIGHT_RESTORE_DEFAULT_ON || @@ -57,7 +57,7 @@ void LightState::setup() { break; case LIGHT_RESTORE_AND_OFF: case LIGHT_RESTORE_AND_ON: - this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); + this->rtc_ = this->make_entity_preference(); this->rtc_.load(&recovered); recovered.state = (this->restore_mode_ == LIGHT_RESTORE_AND_ON); break; diff --git a/esphome/components/lvgl/number/lvgl_number.h b/esphome/components/lvgl/number/lvgl_number.h index d9885bc7fb..44409a0ad5 100644 --- a/esphome/components/lvgl/number/lvgl_number.h +++ b/esphome/components/lvgl/number/lvgl_number.h @@ -21,7 +21,7 @@ class LVGLNumber : public number::Number, public Component { void setup() override { float value = this->value_lambda_(); if (this->restore_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (this->pref_.load(&value)) { this->control_lambda_(value); } diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h index 70bb3e7bcb..ba03920a88 100644 --- a/esphome/components/lvgl/select/lvgl_select.h +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -20,7 +20,7 @@ class LVGLSelect : public select::Select, public Component { this->set_options_(); if (this->restore_) { size_t index; - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (this->pref_.load(&index)) this->widget_->set_selected_index(index, LV_ANIM_OFF); } diff --git a/esphome/components/lvgl/widgets/label.py b/esphome/components/lvgl/widgets/label.py index 3a3a997737..8afd8d610f 100644 --- a/esphome/components/lvgl/widgets/label.py +++ b/esphome/components/lvgl/widgets/label.py @@ -32,7 +32,7 @@ class LabelType(WidgetType): async def to_code(self, w: Widget, config): """For a text object, create and set text""" - if value := config.get(CONF_TEXT): + if (value := config.get(CONF_TEXT)) is not None: await w.set_property(CONF_TEXT, await lv_text.process(value)) await w.set_property(CONF_LONG_MODE, config) await w.set_property(CONF_RECOLOR, config) diff --git a/esphome/components/mipi_rgb/models/st7701s.py b/esphome/components/mipi_rgb/models/st7701s.py index 3c66380d04..990a1ca4f3 100644 --- a/esphome/components/mipi_rgb/models/st7701s.py +++ b/esphome/components/mipi_rgb/models/st7701s.py @@ -55,6 +55,7 @@ st7701s = ST7701S( pclk_frequency="16MHz", pclk_inverted=True, initsequence=( + (0x01,), # Software Reset (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), # Page 0 (0xC0, 0x3B, 0x00), (0xC1, 0x0D, 0x02), (0xC2, 0x31, 0x05), (0xB0, 0x00, 0x11, 0x18, 0x0E, 0x11, 0x06, 0x07, 0x08, 0x07, 0x22, 0x04, 0x12, 0x0F, 0xAA, 0x31, 0x18,), diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index 1c23783ce3..c45c338bb3 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -279,7 +279,7 @@ def modbus_calc_properties(config): if isinstance(value, str): value = value.encode() config[CONF_ADDRESS] = binascii.crc_hqx(value, 0) - config[CONF_REGISTER_TYPE] = ModbusRegisterType.CUSTOM + config[CONF_REGISTER_TYPE] = cv.enum(MODBUS_REGISTER_TYPE)("custom") config[CONF_FORCE_NEW_RANGE] = True return byte_offset, reg_count diff --git a/esphome/components/opentherm/number/opentherm_number.cpp b/esphome/components/opentherm/number/opentherm_number.cpp index f0c69144c8..bdb02a605c 100644 --- a/esphome/components/opentherm/number/opentherm_number.cpp +++ b/esphome/components/opentherm/number/opentherm_number.cpp @@ -17,7 +17,7 @@ void OpenthermNumber::setup() { if (!this->restore_value_) { value = this->initial_value_; } else { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (!this->pref_.load(&value)) { if (!std::isnan(this->initial_value_)) { value = this->initial_value_; diff --git a/esphome/components/rd03d/rd03d.cpp b/esphome/components/rd03d/rd03d.cpp index d9b0b59fe9..ba05abe8e0 100644 --- a/esphome/components/rd03d/rd03d.cpp +++ b/esphome/components/rd03d/rd03d.cpp @@ -133,14 +133,17 @@ void RD03DComponent::process_frame_() { uint8_t offset = FRAME_HEADER_SIZE + (i * TARGET_DATA_SIZE); // Extract raw bytes for this target + // Note: Despite datasheet Table 5-2 showing order as X, Y, Speed, Resolution, + // actual radar output has Resolution before Speed (verified empirically - + // stationary targets were showing non-zero speed with original field order) uint8_t x_low = this->buffer_[offset + 0]; uint8_t x_high = this->buffer_[offset + 1]; uint8_t y_low = this->buffer_[offset + 2]; uint8_t y_high = this->buffer_[offset + 3]; - uint8_t speed_low = this->buffer_[offset + 4]; - uint8_t speed_high = this->buffer_[offset + 5]; - uint8_t res_low = this->buffer_[offset + 6]; - uint8_t res_high = this->buffer_[offset + 7]; + uint8_t res_low = this->buffer_[offset + 4]; + uint8_t res_high = this->buffer_[offset + 5]; + uint8_t speed_low = this->buffer_[offset + 6]; + uint8_t speed_high = this->buffer_[offset + 7]; // Decode values per RD-03D format int16_t x = decode_value(x_low, x_high); diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index 26e20664f2..c652944120 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -132,7 +132,7 @@ void RotaryEncoderSensor::setup() { int32_t initial_value = 0; switch (this->restore_mode_) { case ROTARY_ENCODER_RESTORE_DEFAULT_ZERO: - this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); + this->rtc_ = this->make_entity_preference(); if (!this->rtc_.load(&initial_value)) { initial_value = 0; } diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index d5c9dfa3ae..ea08a5ee77 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -124,8 +124,8 @@ void SEN5XComponent::setup() { sen5x_type = SEN55; } } - ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str()); } + ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str()); if (this->humidity_sensor_ && sen5x_type == SEN50) { ESP_LOGE(TAG, "Relative humidity requires a SEN54 or SEN55"); this->humidity_sensor_ = nullptr; // mark as not used @@ -159,37 +159,23 @@ void SEN5XComponent::setup() { // This ensures the baseline storage is cleared after OTA // Serial numbers are unique to each sensor, so multiple sensors can be used without conflict uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), combined_serial); - this->pref_ = global_preferences->make_preference(hash, true); - - if (this->pref_.load(&this->voc_baselines_storage_)) { - ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, - this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); - } - - // Initialize storage timestamp - this->seconds_since_last_store_ = 0; - - if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) { - ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, - this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); - uint16_t states[4]; - - states[0] = this->voc_baselines_storage_.state0 >> 16; - states[1] = this->voc_baselines_storage_.state0 & 0xFFFF; - states[2] = this->voc_baselines_storage_.state1 >> 16; - states[3] = this->voc_baselines_storage_.state1 & 0xFFFF; - - if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, states, 4)) { - ESP_LOGE(TAG, "Failed to set VOC baseline from saved state"); + this->pref_ = global_preferences->make_preference(hash, true); + this->voc_baseline_time_ = App.get_loop_component_start_time(); + if (this->pref_.load(&this->voc_baseline_state_)) { + if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, this->voc_baseline_state_, 4)) { + ESP_LOGE(TAG, "VOC Baseline State write to sensor failed"); + } else { + ESP_LOGV(TAG, "VOC Baseline State loaded"); + delay(20); } } } bool result; if (this->auto_cleaning_interval_.has_value()) { // override default value - result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL, this->auto_cleaning_interval_.value()); + result = this->write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL, this->auto_cleaning_interval_.value()); } else { - result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL); + result = this->write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL); } if (result) { delay(20); @@ -288,6 +274,14 @@ void SEN5XComponent::dump_config() { ESP_LOGCONFIG(TAG, " RH/T acceleration mode: %s", LOG_STR_ARG(rht_accel_mode_to_string(this->acceleration_mode_.value()))); } + if (this->voc_sensor_) { + char hex_buf[5 * 4]; + format_hex_pretty_to(hex_buf, this->voc_baseline_state_, 4, 0); + ESP_LOGCONFIG(TAG, + " Store Baseline: %s\n" + " State: %s\n", + TRUEFALSE(this->store_baseline_), hex_buf); + } LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_); LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_); @@ -304,36 +298,6 @@ void SEN5XComponent::update() { return; } - // Store baselines after defined interval or if the difference between current and stored baseline becomes too - // much - if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) { - if (this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) { - // run it a bit later to avoid adding a delay here - this->set_timeout(550, [this]() { - uint16_t states[4]; - if (this->read_data(states, 4)) { - uint32_t state0 = states[0] << 16 | states[1]; - uint32_t state1 = states[2] << 16 | states[3]; - if ((uint32_t) std::abs(static_cast(this->voc_baselines_storage_.state0 - state0)) > - MAXIMUM_STORAGE_DIFF || - (uint32_t) std::abs(static_cast(this->voc_baselines_storage_.state1 - state1)) > - MAXIMUM_STORAGE_DIFF) { - this->seconds_since_last_store_ = 0; - this->voc_baselines_storage_.state0 = state0; - this->voc_baselines_storage_.state1 = state1; - - if (this->pref_.save(&this->voc_baselines_storage_)) { - ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, - this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); - } else { - ESP_LOGW(TAG, "Could not store VOC baselines"); - } - } - } - }); - } - } - if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) { this->status_set_warning(); ESP_LOGD(TAG, "Write error: read measurement (%d)", this->last_error_); @@ -402,7 +366,29 @@ void SEN5XComponent::update() { if (this->nox_sensor_ != nullptr) { this->nox_sensor_->publish_state(nox); } - this->status_clear_warning(); + + if (!this->voc_sensor_ || !this->store_baseline_ || + (App.get_loop_component_start_time() - this->voc_baseline_time_) < SHORTEST_BASELINE_STORE_INTERVAL) { + this->status_clear_warning(); + } else { + this->voc_baseline_time_ = App.get_loop_component_start_time(); + if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) { + this->status_set_warning(); + ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL); + } else { + this->set_timeout(20, [this]() { + if (!this->read_data(this->voc_baseline_state_, 4)) { + this->status_set_warning(); + ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL); + } else { + if (this->pref_.save(&this->voc_baseline_state_)) { + ESP_LOGD(TAG, "VOC Baseline State saved"); + } + this->status_clear_warning(); + } + }); + } + } }); } diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h index 9e5b6bf231..aacdfa5717 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -24,11 +24,6 @@ enum RhtAccelerationMode : uint16_t { HIGH_ACCELERATION = 2, }; -struct Sen5xBaselines { - int32_t state0; - int32_t state1; -} PACKED; // NOLINT - struct GasTuning { uint16_t index_offset; uint16_t learning_time_offset_hours; @@ -44,11 +39,9 @@ struct TemperatureCompensation { uint16_t time_constant; }; -// Shortest time interval of 3H for storing baseline values. +// Shortest time interval of 2H (in milliseconds) for storing baseline values. // Prevents wear of the flash because of too many write operations -static const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800; -// Store anyway if the baseline difference exceeds the max storage diff value -static const uint32_t MAXIMUM_STORAGE_DIFF = 50; +static const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 2 * 60 * 60 * 1000; class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: @@ -58,18 +51,20 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri enum Sen5xType { SEN50, SEN54, SEN55, UNKNOWN }; - void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; } - void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; } - void set_pm_4_0_sensor(sensor::Sensor *pm_4_0) { pm_4_0_sensor_ = pm_4_0; } - void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { pm_10_0_sensor_ = pm_10_0; } + void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { this->pm_1_0_sensor_ = pm_1_0; } + void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { this->pm_2_5_sensor_ = pm_2_5; } + void set_pm_4_0_sensor(sensor::Sensor *pm_4_0) { this->pm_4_0_sensor_ = pm_4_0; } + void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { this->pm_10_0_sensor_ = pm_10_0; } - void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; } - void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; } - void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } - void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } - void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; } - void set_acceleration_mode(RhtAccelerationMode mode) { acceleration_mode_ = mode; } - void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) { auto_cleaning_interval_ = auto_cleaning_interval; } + void set_voc_sensor(sensor::Sensor *voc_sensor) { this->voc_sensor_ = voc_sensor; } + void set_nox_sensor(sensor::Sensor *nox_sensor) { this->nox_sensor_ = nox_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_store_baseline(bool store_baseline) { this->store_baseline_ = store_baseline; } + void set_acceleration_mode(RhtAccelerationMode mode) { this->acceleration_mode_ = mode; } + void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) { + this->auto_cleaning_interval_ = auto_cleaning_interval; + } void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, uint16_t std_initial, uint16_t gain_factor) { @@ -80,7 +75,7 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri tuning_params.gating_max_duration_minutes = gating_max_duration_minutes; tuning_params.std_initial = std_initial; tuning_params.gain_factor = gain_factor; - voc_tuning_params_ = tuning_params; + this->voc_tuning_params_ = tuning_params; } void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, @@ -92,14 +87,14 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri tuning_params.gating_max_duration_minutes = gating_max_duration_minutes; tuning_params.std_initial = 50; tuning_params.gain_factor = gain_factor; - nox_tuning_params_ = tuning_params; + this->nox_tuning_params_ = tuning_params; } void set_temperature_compensation(float offset, float normalized_offset_slope, uint16_t time_constant) { TemperatureCompensation temp_comp; temp_comp.offset = offset * 200; temp_comp.normalized_offset_slope = normalized_offset_slope * 10000; temp_comp.time_constant = time_constant; - temperature_compensation_ = temp_comp; + this->temperature_compensation_ = temp_comp; } bool start_fan_cleaning(); @@ -107,7 +102,8 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning); bool write_temperature_compensation_(const TemperatureCompensation &compensation); - uint32_t seconds_since_last_store_; + uint16_t voc_baseline_state_[4]{0}; + uint32_t voc_baseline_time_; uint16_t firmware_version_; ERRORCODE error_code_; uint8_t serial_number_[4]; @@ -132,7 +128,6 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri optional temperature_compensation_; ESPPreferenceObject pref_; std::string product_name_; - Sen5xBaselines voc_baselines_storage_; }; } // namespace sen5x diff --git a/esphome/components/sen5x/sensor.py b/esphome/components/sen5x/sensor.py index 9c3114b9e2..538a2f5239 100644 --- a/esphome/components/sen5x/sensor.py +++ b/esphome/components/sen5x/sensor.py @@ -210,6 +210,7 @@ SENSOR_MAP = { SETTING_MAP = { CONF_AUTO_CLEANING_INTERVAL: "set_auto_cleaning_interval", CONF_ACCELERATION_MODE: "set_acceleration_mode", + CONF_STORE_BASELINE: "set_store_baseline", } diff --git a/esphome/components/sensirion_common/i2c_sensirion.cpp b/esphome/components/sensirion_common/i2c_sensirion.cpp index 9eac6b4525..26702c148c 100644 --- a/esphome/components/sensirion_common/i2c_sensirion.cpp +++ b/esphome/components/sensirion_common/i2c_sensirion.cpp @@ -39,42 +39,23 @@ bool SensirionI2CDevice::read_data(uint16_t *data, const uint8_t len) { */ bool SensirionI2CDevice::write_command_(uint16_t command, CommandLen command_len, const uint16_t *data, const uint8_t data_len) { - uint8_t temp_stack[BUFFER_STACK_SIZE]; - std::unique_ptr temp_heap; - uint8_t *temp; size_t required_buffer_len = data_len * 3 + 2; - - // Is a dynamic allocation required ? - if (required_buffer_len >= BUFFER_STACK_SIZE) { - temp_heap = std::unique_ptr(new uint8_t[required_buffer_len]); - temp = temp_heap.get(); - } else { - temp = temp_stack; - } + SmallBufferWithHeapFallback buffer(required_buffer_len); + uint8_t *temp = buffer.get(); // First byte or word is the command uint8_t raw_idx = 0; if (command_len == 1) { temp[raw_idx++] = command & 0xFF; } else { // command is 2 bytes -#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ temp[raw_idx++] = command >> 8; temp[raw_idx++] = command & 0xFF; -#else - temp[raw_idx++] = command & 0xFF; - temp[raw_idx++] = command >> 8; -#endif } // add parameters followed by crc // skipped if len == 0 for (size_t i = 0; i < data_len; i++) { -#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ temp[raw_idx++] = data[i] >> 8; temp[raw_idx++] = data[i] & 0xFF; -#else - temp[raw_idx++] = data[i] & 0xFF; - temp[raw_idx++] = data[i] >> 8; -#endif // Use MSB first since Sensirion devices use CRC-8 with MSB first uint8_t crc = crc8(&temp[raw_idx - 2], 2, 0xFF, CRC_POLYNOMIAL, true); temp[raw_idx++] = crc; diff --git a/esphome/components/sensor/automation.h b/esphome/components/sensor/automation.h index 996c7fc9b5..b4de712727 100644 --- a/esphome/components/sensor/automation.h +++ b/esphome/components/sensor/automation.h @@ -39,7 +39,7 @@ class ValueRangeTrigger : public Trigger, public Component { template void set_max(V max) { this->max_ = max; } void setup() override { - this->rtc_ = global_preferences->make_preference(this->parent_->get_preference_hash()); + this->rtc_ = this->parent_->make_entity_preference(); bool initial_state; if (this->rtc_.load(&initial_state)) { this->previous_in_range_ = initial_state; diff --git a/esphome/components/slow_pwm/slow_pwm_output.cpp b/esphome/components/slow_pwm/slow_pwm_output.cpp index 48ded94b3a..033729c407 100644 --- a/esphome/components/slow_pwm/slow_pwm_output.cpp +++ b/esphome/components/slow_pwm/slow_pwm_output.cpp @@ -1,6 +1,7 @@ #include "slow_pwm_output.h" -#include "esphome/core/log.h" #include "esphome/core/application.h" +#include "esphome/core/gpio.h" +#include "esphome/core/log.h" namespace esphome { namespace slow_pwm { @@ -20,7 +21,9 @@ void SlowPWMOutput::set_output_state_(bool new_state) { } if (new_state != current_state_) { if (this->pin_) { - ESP_LOGV(TAG, "Switching output pin %s to %s", this->pin_->dump_summary().c_str(), ONOFF(new_state)); + char pin_summary[GPIO_SUMMARY_MAX_LEN]; + this->pin_->dump_summary(pin_summary, sizeof(pin_summary)); + ESP_LOGV(TAG, "Switching output pin %s to %s", pin_summary, ONOFF(new_state)); } else { ESP_LOGV(TAG, "Switching to %s", ONOFF(new_state)); } diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index 9a3a47bac8..172bc980a8 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -55,7 +55,7 @@ void SpeakerMediaPlayer::setup() { this->media_control_command_queue_ = xQueueCreate(MEDIA_CONTROLS_QUEUE_LENGTH, sizeof(MediaCallCommand)); - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); VolumeRestoreState volume_restore_state; if (this->pref_.load(&volume_restore_state)) { diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 369ee5e6ff..2a60eb042b 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -16,7 +16,7 @@ void SprinklerControllerNumber::setup() { if (!this->restore_value_) { value = this->initial_value_; } else { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (!this->pref_.load(&value)) { if (!std::isnan(this->initial_value_)) { value = this->initial_value_; diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 069533fa78..d880139c5f 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -34,7 +34,7 @@ optional Switch::get_initial_state() { if (!(restore_mode & RESTORE_MODE_PERSISTENT_MASK)) return {}; - this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); + this->rtc_ = this->make_entity_preference(); bool initial_state; if (!this->rtc_.load(&initial_state)) return {}; diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index 028d6f0879..1a5aef6b8d 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -82,7 +82,7 @@ void TemplateAlarmControlPanel::setup() { this->current_state_ = ACP_STATE_DISARMED; if (this->restore_mode_ == ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED) { uint8_t value; - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (this->pref_.load(&value)) { this->current_state_ = static_cast(value); } diff --git a/esphome/components/template/datetime/template_date.cpp b/esphome/components/template/datetime/template_date.cpp index 303d5ae2b0..be1d875a7e 100644 --- a/esphome/components/template/datetime/template_date.cpp +++ b/esphome/components/template/datetime/template_date.cpp @@ -18,8 +18,7 @@ void TemplateDate::setup() { state = this->initial_value_; } else { datetime::DateEntityRestoreState temp; - this->pref_ = - global_preferences->make_preference(194434030U ^ this->get_preference_hash()); + this->pref_ = this->make_entity_preference(194434030U); if (this->pref_.load(&temp)) { temp.apply(this); return; diff --git a/esphome/components/template/datetime/template_datetime.cpp b/esphome/components/template/datetime/template_datetime.cpp index 81a823f53e..e134f2b654 100644 --- a/esphome/components/template/datetime/template_datetime.cpp +++ b/esphome/components/template/datetime/template_datetime.cpp @@ -18,8 +18,7 @@ void TemplateDateTime::setup() { state = this->initial_value_; } else { datetime::DateTimeEntityRestoreState temp; - this->pref_ = global_preferences->make_preference( - 194434090U ^ this->get_preference_hash()); + this->pref_ = this->make_entity_preference(194434090U); if (this->pref_.load(&temp)) { temp.apply(this); return; diff --git a/esphome/components/template/datetime/template_time.cpp b/esphome/components/template/datetime/template_time.cpp index 21f843dcc7..586e126e3b 100644 --- a/esphome/components/template/datetime/template_time.cpp +++ b/esphome/components/template/datetime/template_time.cpp @@ -18,8 +18,7 @@ void TemplateTime::setup() { state = this->initial_value_; } else { datetime::TimeEntityRestoreState temp; - this->pref_ = - global_preferences->make_preference(194434060U ^ this->get_preference_hash()); + this->pref_ = this->make_entity_preference(194434060U); if (this->pref_.load(&temp)) { temp.apply(this); return; diff --git a/esphome/components/template/number/template_number.cpp b/esphome/components/template/number/template_number.cpp index 76fef82225..885265cf5d 100644 --- a/esphome/components/template/number/template_number.cpp +++ b/esphome/components/template/number/template_number.cpp @@ -13,7 +13,7 @@ void TemplateNumber::setup() { if (!this->restore_value_) { value = this->initial_value_; } else { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (!this->pref_.load(&value)) { if (!std::isnan(this->initial_value_)) { value = this->initial_value_; diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 818abfc1d7..fa34aa9fa7 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -11,7 +11,7 @@ void TemplateSelect::setup() { size_t index = this->initial_option_index_; if (this->restore_value_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); size_t restored_index; if (this->pref_.load(&restored_index) && this->has_index(restored_index)) { index = restored_index; diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 0416438dcd..44087969b5 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -1060,11 +1060,11 @@ bool ThermostatClimate::cooling_required_() { auto temperature = this->supports_two_points_ ? this->target_temperature_high : this->target_temperature; if (this->supports_cool_) { - if (this->current_temperature > temperature + this->cooling_deadband_) { - // if the current temperature exceeds the target + deadband, cooling is required + if (this->current_temperature >= temperature + this->cooling_deadband_) { + // if the current temperature reaches or exceeds the target + deadband, cooling is required return true; - } else if (this->current_temperature < temperature - this->cooling_overrun_) { - // if the current temperature is less than the target - overrun, cooling should stop + } else if (this->current_temperature <= temperature - this->cooling_overrun_) { + // if the current temperature is less than or equal to the target - overrun, cooling should stop return false; } else { // if we get here, the current temperature is between target + deadband and target - overrun, @@ -1081,11 +1081,11 @@ bool ThermostatClimate::fanning_required_() { if (this->supports_fan_only_) { if (this->supports_fan_only_cooling_) { - if (this->current_temperature > temperature + this->cooling_deadband_) { - // if the current temperature exceeds the target + deadband, fanning is required + if (this->current_temperature >= temperature + this->cooling_deadband_) { + // if the current temperature reaches or exceeds the target + deadband, fanning is required return true; - } else if (this->current_temperature < temperature - this->cooling_overrun_) { - // if the current temperature is less than the target - overrun, fanning should stop + } else if (this->current_temperature <= temperature - this->cooling_overrun_) { + // if the current temperature is less than or equal to the target - overrun, fanning should stop return false; } else { // if we get here, the current temperature is between target + deadband and target - overrun, @@ -1103,11 +1103,12 @@ bool ThermostatClimate::heating_required_() { auto temperature = this->supports_two_points_ ? this->target_temperature_low : this->target_temperature; if (this->supports_heat_) { - if (this->current_temperature < temperature - this->heating_deadband_) { - // if the current temperature is below the target - deadband, heating is required + if (this->current_temperature <= temperature - this->heating_deadband_) { + // if the current temperature is below or equal to the target - deadband, heating is required return true; - } else if (this->current_temperature > temperature + this->heating_overrun_) { - // if the current temperature is above the target + overrun, heating should stop + } else if (this->current_temperature >= temperature + this->heating_overrun_) { + // if the current temperature is above or equal to the target + overrun, heating should stop + return false; } else { // if we get here, the current temperature is between target - deadband and target + overrun, diff --git a/esphome/components/total_daily_energy/total_daily_energy.cpp b/esphome/components/total_daily_energy/total_daily_energy.cpp index 818696f99b..e7a45a5edf 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.cpp +++ b/esphome/components/total_daily_energy/total_daily_energy.cpp @@ -10,7 +10,7 @@ void TotalDailyEnergy::setup() { float initial_value = 0; if (this->restore_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); this->pref_.load(&initial_value); } this->publish_state_and_save(initial_value); diff --git a/esphome/components/tuya/number/tuya_number.cpp b/esphome/components/tuya/number/tuya_number.cpp index 44b22167de..fd22e642c6 100644 --- a/esphome/components/tuya/number/tuya_number.cpp +++ b/esphome/components/tuya/number/tuya_number.cpp @@ -8,7 +8,7 @@ static const char *const TAG = "tuya.number"; void TuyaNumber::setup() { if (this->restore_value_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); } this->parent_->register_listener(this->number_id_, [this](const TuyaDatapoint &datapoint) { diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index a9086747ce..df077e9eb7 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -161,7 +161,7 @@ void Valve::publish_state(bool save) { } } optional Valve::restore_state_() { - this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); + this->rtc_ = this->make_entity_preference(); ValveRestoreState recovered{}; if (!this->rtc_.load(&recovered)) return {}; diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index 7b947057e1..3f14ad04c6 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -146,9 +146,7 @@ void WaterHeaterCall::validate_() { } } -void WaterHeater::setup() { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); -} +void WaterHeater::setup() { this->pref_ = this->make_entity_preference(); } void WaterHeater::publish_state() { auto traits = this->get_traits(); diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 8508b93411..07df13008d 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -92,6 +92,37 @@ StringRef EntityBase::get_object_id_to(std::span buf) c uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; } +ESPPreferenceObject EntityBase::make_entity_preference_(size_t size, uint32_t version) { + // This helper centralizes preference creation to enable fixing hash collisions. + // See: https://github.com/esphome/backlog/issues/85 + // + // COLLISION PROBLEM: get_preference_hash() uses fnv1_hash on sanitized object_id. + // Multiple entity names can sanitize to the same object_id: + // - "Living Room" and "living_room" both become "living_room" + // - UTF-8 names like "温度" and "湿度" both become "__" (underscores) + // This causes entities to overwrite each other's stored preferences. + // + // FUTURE MIGRATION: When implementing get_preference_hash_v2() that hashes + // the original entity name (not sanitized object_id), migration logic goes here: + // + // uint32_t old_key = this->get_preference_hash() ^ version; + // uint32_t new_key = this->get_preference_hash_v2() ^ version; + // if (old_key != new_key) { + // auto old_pref = global_preferences->make_preference(size, old_key); + // auto new_pref = global_preferences->make_preference(size, new_key); + // SmallBufferWithHeapFallback<64> buffer(size); + // if (old_pref.load(buffer.data(), size)) { + // new_pref.save(buffer.data(), size); + // } + // } + // return global_preferences->make_preference(size, new_key); + // + // This will require raw load/save methods on ESPPreferenceObject (uint8_t*, size). + // + uint32_t key = this->get_preference_hash() ^ version; + return global_preferences->make_preference(size, key); +} + std::string EntityBase_DeviceClass::get_device_class() { if (this->device_class_ == nullptr) { return ""; diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index f91bd9b20c..ae10b86140 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -6,6 +6,7 @@ #include "string_ref.h" #include "helpers.h" #include "log.h" +#include "preferences.h" #ifdef USE_DEVICES #include "device.h" @@ -151,7 +152,19 @@ class EntityBase { #endif } + /// Create a preference object for storing this entity's state/settings. + /// @tparam T The type of data to store (must be trivially copyable) + /// @param version Optional version hash XORed with preference key (change when struct layout changes) + template ESPPreferenceObject make_entity_preference(uint32_t version = 0) { + static_assert(std::is_trivially_copyable::value, "T must be trivially copyable"); + return this->make_entity_preference_(sizeof(T), version); + } + protected: + /// Non-template helper for make_entity_preference() to avoid code bloat. + /// When preference hash algorithm changes, migration logic goes here. + ESPPreferenceObject make_entity_preference_(size_t size, uint32_t version); + void calc_object_id_(); StringRef name_; diff --git a/requirements_test.txt b/requirements_test.txt index d93a5d108f..5d90764021 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.4 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.14.13 # also change in .pre-commit-config.yaml when updating +ruff==0.14.14 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit diff --git a/tests/components/bmp581/common.yaml b/tests/components/bmp581_i2c/common.yaml similarity index 86% rename from tests/components/bmp581/common.yaml rename to tests/components/bmp581_i2c/common.yaml index 250b1f5857..258d8a5020 100644 --- a/tests/components/bmp581/common.yaml +++ b/tests/components/bmp581_i2c/common.yaml @@ -1,5 +1,5 @@ sensor: - - platform: bmp581 + - platform: bmp581_i2c i2c_id: i2c_bus temperature: name: BMP581 Temperature diff --git a/tests/components/bmp581/test.esp32-idf.yaml b/tests/components/bmp581_i2c/test.esp32-idf.yaml similarity index 100% rename from tests/components/bmp581/test.esp32-idf.yaml rename to tests/components/bmp581_i2c/test.esp32-idf.yaml diff --git a/tests/components/bmp581/test.esp8266-ard.yaml b/tests/components/bmp581_i2c/test.esp8266-ard.yaml similarity index 100% rename from tests/components/bmp581/test.esp8266-ard.yaml rename to tests/components/bmp581_i2c/test.esp8266-ard.yaml diff --git a/tests/components/bmp581/test.rp2040-ard.yaml b/tests/components/bmp581_i2c/test.rp2040-ard.yaml similarity index 100% rename from tests/components/bmp581/test.rp2040-ard.yaml rename to tests/components/bmp581_i2c/test.rp2040-ard.yaml diff --git a/tests/components/ir_rf_proxy/common-rx.yaml b/tests/components/ir_rf_proxy/common-rx.yaml new file mode 100644 index 0000000000..0f758f832d --- /dev/null +++ b/tests/components/ir_rf_proxy/common-rx.yaml @@ -0,0 +1,18 @@ +remote_receiver: + id: ir_receiver + pin: ${rx_pin} + +# Test various hardware types with transmitter/receiver using infrared platform +infrared: + # Infrared receiver + - platform: ir_rf_proxy + id: ir_rx + name: "IR Receiver" + remote_receiver_id: ir_receiver + + # RF 900MHz receiver + - platform: ir_rf_proxy + id: rf_900_rx + name: "RF 900 Receiver" + frequency: 900 MHz + remote_receiver_id: ir_receiver diff --git a/tests/components/ir_rf_proxy/common-tx.yaml b/tests/components/ir_rf_proxy/common-tx.yaml new file mode 100644 index 0000000000..4af9e2635e --- /dev/null +++ b/tests/components/ir_rf_proxy/common-tx.yaml @@ -0,0 +1,19 @@ +remote_transmitter: + id: ir_transmitter + pin: ${tx_pin} + carrier_duty_percent: 50% + +# Test various hardware types with transmitter/receiver using infrared platform +infrared: + # Infrared transmitter + - platform: ir_rf_proxy + id: ir_tx + name: "IR Transmitter" + remote_transmitter_id: ir_transmitter + + # RF 433MHz transmitter + - platform: ir_rf_proxy + id: rf_433_tx + name: "RF 433 Transmitter" + frequency: 433 MHz + remote_transmitter_id: ir_transmitter diff --git a/tests/components/ir_rf_proxy/common.yaml b/tests/components/ir_rf_proxy/common.yaml index cd2b10d31b..53a0cd379a 100644 --- a/tests/components/ir_rf_proxy/common.yaml +++ b/tests/components/ir_rf_proxy/common.yaml @@ -1,42 +1,7 @@ +network: + wifi: ssid: MySSID password: password1 api: - -remote_transmitter: - id: ir_transmitter - pin: ${tx_pin} - carrier_duty_percent: 50% - -remote_receiver: - id: ir_receiver - pin: ${rx_pin} - -# Test various hardware types with transmitter/receiver using infrared platform -infrared: - # Infrared transmitter - - platform: ir_rf_proxy - id: ir_tx - name: "IR Transmitter" - remote_transmitter_id: ir_transmitter - - # Infrared receiver - - platform: ir_rf_proxy - id: ir_rx - name: "IR Receiver" - remote_receiver_id: ir_receiver - - # RF 433MHz transmitter - - platform: ir_rf_proxy - id: rf_433_tx - name: "RF 433 Transmitter" - frequency: 433 MHz - remote_transmitter_id: ir_transmitter - - # RF 900MHz receiver - - platform: ir_rf_proxy - id: rf_900_rx - name: "RF 900 Receiver" - frequency: 900 MHz - remote_receiver_id: ir_receiver diff --git a/tests/components/ir_rf_proxy/test-rx.esp32-idf.yaml b/tests/components/ir_rf_proxy/test-rx.esp32-idf.yaml new file mode 100644 index 0000000000..8172885b31 --- /dev/null +++ b/tests/components/ir_rf_proxy/test-rx.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + common: !include common.yaml + rx: !include common-rx.yaml diff --git a/tests/components/ir_rf_proxy/test-rx.esp8266-ard.yaml b/tests/components/ir_rf_proxy/test-rx.esp8266-ard.yaml new file mode 100644 index 0000000000..8172885b31 --- /dev/null +++ b/tests/components/ir_rf_proxy/test-rx.esp8266-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + common: !include common.yaml + rx: !include common-rx.yaml diff --git a/tests/components/ir_rf_proxy/test-rx.rp2040-ard.yaml b/tests/components/ir_rf_proxy/test-rx.rp2040-ard.yaml new file mode 100644 index 0000000000..8172885b31 --- /dev/null +++ b/tests/components/ir_rf_proxy/test-rx.rp2040-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + common: !include common.yaml + rx: !include common-rx.yaml diff --git a/tests/components/ir_rf_proxy/test-tx.esp32-idf.yaml b/tests/components/ir_rf_proxy/test-tx.esp32-idf.yaml new file mode 100644 index 0000000000..7162f15b2d --- /dev/null +++ b/tests/components/ir_rf_proxy/test-tx.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + common: !include common.yaml + tx: !include common-tx.yaml diff --git a/tests/components/ir_rf_proxy/test-tx.esp8266-ard.yaml b/tests/components/ir_rf_proxy/test-tx.esp8266-ard.yaml new file mode 100644 index 0000000000..7162f15b2d --- /dev/null +++ b/tests/components/ir_rf_proxy/test-tx.esp8266-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + common: !include common.yaml + tx: !include common-tx.yaml diff --git a/tests/components/ir_rf_proxy/test-tx.rp2040-ard.yaml b/tests/components/ir_rf_proxy/test-tx.rp2040-ard.yaml new file mode 100644 index 0000000000..7162f15b2d --- /dev/null +++ b/tests/components/ir_rf_proxy/test-tx.rp2040-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + common: !include common.yaml + tx: !include common-tx.yaml diff --git a/tests/components/ir_rf_proxy/test.bk72xx-ard.yaml b/tests/components/ir_rf_proxy/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..a0e145f476 --- /dev/null +++ b/tests/components/ir_rf_proxy/test.bk72xx-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + common: !include common.yaml + rx: !include common-rx.yaml + tx: !include common-tx.yaml diff --git a/tests/components/ir_rf_proxy/test.esp32-idf.yaml b/tests/components/ir_rf_proxy/test.esp32-idf.yaml index b516342f3b..a0e145f476 100644 --- a/tests/components/ir_rf_proxy/test.esp32-idf.yaml +++ b/tests/components/ir_rf_proxy/test.esp32-idf.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 -<<: !include common.yaml +packages: + common: !include common.yaml + rx: !include common-rx.yaml + tx: !include common-tx.yaml diff --git a/tests/components/ir_rf_proxy/test.esp8266-ard.yaml b/tests/components/ir_rf_proxy/test.esp8266-ard.yaml index b516342f3b..a0e145f476 100644 --- a/tests/components/ir_rf_proxy/test.esp8266-ard.yaml +++ b/tests/components/ir_rf_proxy/test.esp8266-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 -<<: !include common.yaml +packages: + common: !include common.yaml + rx: !include common-rx.yaml + tx: !include common-tx.yaml diff --git a/tests/components/ir_rf_proxy/test.rp2040-ard.yaml b/tests/components/ir_rf_proxy/test.rp2040-ard.yaml index b516342f3b..a0e145f476 100644 --- a/tests/components/ir_rf_proxy/test.rp2040-ard.yaml +++ b/tests/components/ir_rf_proxy/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 -<<: !include common.yaml +packages: + common: !include common.yaml + rx: !include common-rx.yaml + tx: !include common-tx.yaml diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 65d629bcdf..3635fc710f 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -197,6 +197,9 @@ lvgl: - lvgl.label.update: id: msgbox_label text: Unloaded + - lvgl.label.update: + id: msgbox_label + text: "" # Empty text on_all_events: logger.log: format: "Event %s"