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..00a22fed7c 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 @@ -481,6 +482,7 @@ esphome/components/switch/* @esphome/core esphome/components/switch/binary_sensor/* @ssieb esphome/components/sx126x/* @swoboda1337 esphome/components/sx127x/* @swoboda1337 +esphome/components/sy6970/* @linkedupbits esphome/components/syslog/* @clydebarrow esphome/components/t6615/* @tylermenezes esphome/components/tc74/* @sethgirvan 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/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index e40431c851..170f436f02 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -12,6 +12,7 @@ from esphome.const import ( KEY_FRAMEWORK_VERSION, ) from esphome.core import CORE +from esphome.cpp_generator import add_define CODEOWNERS = ["@swoboda1337"] @@ -42,6 +43,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): + add_define("USE_ESP32_HOSTED") if config[CONF_ACTIVE_HIGH]: esp32.add_idf_sdkconfig_option( "CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH", diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index 191c849aa3..7a965ce5ad 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/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/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/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/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/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index d5c9dfa3ae..fc4932d867 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -30,6 +30,19 @@ static const int8_t SEN5X_INDEX_SCALE_FACTOR = 10; // static const int8_t SEN5X_MIN_INDEX_VALUE = 1 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor static const int16_t SEN5X_MAX_INDEX_VALUE = 500 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor +static const LogString *type_to_string(Sen5xType type) { + switch (type) { + case Sen5xType::SEN50: + return LOG_STR("SEN50"); + case Sen5xType::SEN54: + return LOG_STR("SEN54"); + case Sen5xType::SEN55: + return LOG_STR("SEN55"); + default: + return LOG_STR("UNKNOWN"); + } +} + static const LogString *rht_accel_mode_to_string(RhtAccelerationMode mode) { switch (mode) { case LOW_ACCELERATION: @@ -43,6 +56,15 @@ static const LogString *rht_accel_mode_to_string(RhtAccelerationMode mode) { } } +// This function performs an in-place conversion of the provided buffer +// from uint16_t values to big endianness +static inline const char *sensirion_convert_to_string_in_place(uint16_t *array, size_t length) { + for (size_t i = 0; i < length; i++) { + array[i] = convert_big_endian(array[i]); + } + return reinterpret_cast(array); +} + void SEN5XComponent::setup() { // the sensor needs 1000 ms to enter the idle state this->set_timeout(1000, [this]() { @@ -75,18 +97,18 @@ void SEN5XComponent::setup() { stop_measurement_delay = 200; } this->set_timeout(stop_measurement_delay, [this]() { - uint16_t raw_serial_number[3]; - if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 20)) { + // note: serial number register is actually 32-bytes long but we grab only the first 16-bytes, + // this appears to be all that Sensirion uses for serial numbers, this could change + uint16_t raw_serial_number[8]; + if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 8, 20)) { ESP_LOGE(TAG, "Failed to read serial number"); this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED; this->mark_failed(); return; } - this->serial_number_[0] = static_cast(uint16_t(raw_serial_number[0]) & 0xFF); - this->serial_number_[1] = static_cast(raw_serial_number[0] & 0xFF); - this->serial_number_[2] = static_cast(raw_serial_number[1] >> 8); - ESP_LOGV(TAG, "Serial number %02d.%02d.%02d", this->serial_number_[0], this->serial_number_[1], - this->serial_number_[2]); + const char *serial_number = sensirion_convert_to_string_in_place(raw_serial_number, 8); + snprintf(this->serial_number_, sizeof(this->serial_number_), "%s", serial_number); + ESP_LOGV(TAG, "Serial number %s", this->serial_number_); uint16_t raw_product_name[16]; if (!this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) { @@ -95,50 +117,35 @@ void SEN5XComponent::setup() { this->mark_failed(); return; } - // 2 ASCII bytes are encoded in an int - const uint16_t *current_int = raw_product_name; - char current_char; - uint8_t max = 16; - do { - // first char - current_char = *current_int >> 8; - if (current_char) { - this->product_name_.push_back(current_char); - // second char - current_char = *current_int & 0xFF; - if (current_char) { - this->product_name_.push_back(current_char); - } - } - current_int++; - } while (current_char && --max); - - Sen5xType sen5x_type = UNKNOWN; - if (this->product_name_ == "SEN50") { - sen5x_type = SEN50; + const char *product_name = sensirion_convert_to_string_in_place(raw_product_name, 16); + if (strncmp(product_name, "SEN50", 5) == 0) { + this->type_ = Sen5xType::SEN50; + } else if (strncmp(product_name, "SEN54", 5) == 0) { + this->type_ = Sen5xType::SEN54; + } else if (strncmp(product_name, "SEN55", 5) == 0) { + this->type_ = Sen5xType::SEN55; } else { - if (this->product_name_ == "SEN54") { - sen5x_type = SEN54; - } else { - if (this->product_name_ == "SEN55") { - sen5x_type = SEN55; - } - } - ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str()); + this->type_ = Sen5xType::UNKNOWN; + ESP_LOGE(TAG, "Unknown product name: %.32s", product_name); + this->error_code_ = PRODUCT_NAME_FAILED; + this->mark_failed(); + return; } - if (this->humidity_sensor_ && sen5x_type == SEN50) { + + ESP_LOGD(TAG, "Type: %s", LOG_STR_ARG(type_to_string(this->type_))); + if (this->humidity_sensor_ && this->type_ == Sen5xType::SEN50) { ESP_LOGE(TAG, "Relative humidity requires a SEN54 or SEN55"); this->humidity_sensor_ = nullptr; // mark as not used } - if (this->temperature_sensor_ && sen5x_type == SEN50) { + if (this->temperature_sensor_ && this->type_ == Sen5xType::SEN50) { ESP_LOGE(TAG, "Temperature requires a SEN54 or SEN55"); this->temperature_sensor_ = nullptr; // mark as not used } - if (this->voc_sensor_ && sen5x_type == SEN50) { + if (this->voc_sensor_ && this->type_ == Sen5xType::SEN50) { ESP_LOGE(TAG, "VOC requires a SEN54 or SEN55"); this->voc_sensor_ = nullptr; // mark as not used } - if (this->nox_sensor_ && sen5x_type != SEN55) { + if (this->nox_sensor_ && this->type_ != Sen5xType::SEN55) { ESP_LOGE(TAG, "NOx requires a SEN55"); this->nox_sensor_ = nullptr; // mark as not used } @@ -153,43 +160,25 @@ void SEN5XComponent::setup() { ESP_LOGV(TAG, "Firmware version %d", this->firmware_version_); if (this->voc_sensor_ && this->store_baseline_) { - uint32_t combined_serial = - encode_uint24(this->serial_number_[0], this->serial_number_[1], this->serial_number_[2]); - // Hash with config hash, version, and serial number - // 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"); + // Hash with serial number, serial numbers are unique, so multiple sensors can be used without conflict + uint32_t hash = fnv1a_hash(this->serial_number_); + 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); @@ -276,11 +265,10 @@ void SEN5XComponent::dump_config() { } } ESP_LOGCONFIG(TAG, - " Product name: %s\n" + " Type: %s\n" " Firmware version: %d\n" - " Serial number %02d.%02d.%02d", - this->product_name_.c_str(), this->firmware_version_, this->serial_number_[0], this->serial_number_[1], - this->serial_number_[2]); + " Serial number: %s", + LOG_STR_ARG(type_to_string(this->type_)), this->firmware_version_, this->serial_number_); if (this->auto_cleaning_interval_.has_value()) { ESP_LOGCONFIG(TAG, " Auto cleaning interval: %" PRId32 "s", this->auto_cleaning_interval_.value()); } @@ -288,6 +276,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 +300,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 +368,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..e3bf931b41 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -24,10 +24,7 @@ enum RhtAccelerationMode : uint16_t { HIGH_ACCELERATION = 2, }; -struct Sen5xBaselines { - int32_t state0; - int32_t state1; -} PACKED; // NOLINT +enum class Sen5xType : uint8_t { SEN50, SEN54, SEN55, UNKNOWN }; struct GasTuning { uint16_t index_offset; @@ -44,11 +41,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: @@ -56,20 +51,20 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri void dump_config() override; void update() override; - enum Sen5xType { SEN50, SEN54, SEN55, UNKNOWN }; + 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_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_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,10 +102,12 @@ 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_; + char serial_number_[17] = "UNKNOWN"; + uint16_t voc_baseline_state_[4]{0}; + uint32_t voc_baseline_time_; uint16_t firmware_version_; + Sen5xType type_{Sen5xType::UNKNOWN}; ERRORCODE error_code_; - uint8_t serial_number_[4]; bool initialized_{false}; bool store_baseline_; @@ -131,8 +128,6 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri optional nox_tuning_params_; 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/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/sy6970/__init__.py b/esphome/components/sy6970/__init__.py new file mode 100644 index 0000000000..2390d046e4 --- /dev/null +++ b/esphome/components/sy6970/__init__.py @@ -0,0 +1,63 @@ +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv +from esphome.const import CONF_ID + +CODEOWNERS = ["@linkedupbits"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +CONF_SY6970_ID = "sy6970_id" +CONF_ENABLE_STATUS_LED = "enable_status_led" +CONF_INPUT_CURRENT_LIMIT = "input_current_limit" +CONF_CHARGE_VOLTAGE = "charge_voltage" +CONF_CHARGE_CURRENT = "charge_current" +CONF_PRECHARGE_CURRENT = "precharge_current" +CONF_CHARGE_ENABLED = "charge_enabled" +CONF_ENABLE_ADC = "enable_adc" + +sy6970_ns = cg.esphome_ns.namespace("sy6970") +SY6970Component = sy6970_ns.class_( + "SY6970Component", cg.PollingComponent, i2c.I2CDevice +) +SY6970Listener = sy6970_ns.class_("SY6970Listener") + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SY6970Component), + cv.Optional(CONF_ENABLE_STATUS_LED, default=True): cv.boolean, + cv.Optional(CONF_INPUT_CURRENT_LIMIT, default=500): cv.int_range( + min=100, max=3200 + ), + cv.Optional(CONF_CHARGE_VOLTAGE, default=4208): cv.int_range( + min=3840, max=4608 + ), + cv.Optional(CONF_CHARGE_CURRENT, default=2048): cv.int_range( + min=0, max=5056 + ), + cv.Optional(CONF_PRECHARGE_CURRENT, default=128): cv.int_range( + min=64, max=1024 + ), + cv.Optional(CONF_CHARGE_ENABLED, default=True): cv.boolean, + cv.Optional(CONF_ENABLE_ADC, default=True): cv.boolean, + } + ) + .extend(cv.polling_component_schema("5s")) + .extend(i2c.i2c_device_schema(0x6A)) +) + + +async def to_code(config): + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_ENABLE_STATUS_LED], + config[CONF_INPUT_CURRENT_LIMIT], + config[CONF_CHARGE_VOLTAGE], + config[CONF_CHARGE_CURRENT], + config[CONF_PRECHARGE_CURRENT], + config[CONF_CHARGE_ENABLED], + config[CONF_ENABLE_ADC], + ) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/sy6970/binary_sensor/__init__.py b/esphome/components/sy6970/binary_sensor/__init__.py new file mode 100644 index 0000000000..132b282051 --- /dev/null +++ b/esphome/components/sy6970/binary_sensor/__init__.py @@ -0,0 +1,56 @@ +import esphome.codegen as cg +from esphome.components import binary_sensor +import esphome.config_validation as cv +from esphome.const import DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_POWER + +from .. import CONF_SY6970_ID, SY6970Component, sy6970_ns + +DEPENDENCIES = ["sy6970"] + +CONF_VBUS_CONNECTED = "vbus_connected" +CONF_CHARGING = "charging" +CONF_CHARGE_DONE = "charge_done" + +SY6970VbusConnectedBinarySensor = sy6970_ns.class_( + "SY6970VbusConnectedBinarySensor", binary_sensor.BinarySensor +) +SY6970ChargingBinarySensor = sy6970_ns.class_( + "SY6970ChargingBinarySensor", binary_sensor.BinarySensor +) +SY6970ChargeDoneBinarySensor = sy6970_ns.class_( + "SY6970ChargeDoneBinarySensor", binary_sensor.BinarySensor +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_SY6970_ID): cv.use_id(SY6970Component), + cv.Optional(CONF_VBUS_CONNECTED): binary_sensor.binary_sensor_schema( + SY6970VbusConnectedBinarySensor, + device_class=DEVICE_CLASS_CONNECTIVITY, + ), + cv.Optional(CONF_CHARGING): binary_sensor.binary_sensor_schema( + SY6970ChargingBinarySensor, + device_class=DEVICE_CLASS_POWER, + ), + cv.Optional(CONF_CHARGE_DONE): binary_sensor.binary_sensor_schema( + SY6970ChargeDoneBinarySensor, + device_class=DEVICE_CLASS_POWER, + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_SY6970_ID]) + + if vbus_connected_config := config.get(CONF_VBUS_CONNECTED): + sens = await binary_sensor.new_binary_sensor(vbus_connected_config) + cg.add(parent.add_listener(sens)) + + if charging_config := config.get(CONF_CHARGING): + sens = await binary_sensor.new_binary_sensor(charging_config) + cg.add(parent.add_listener(sens)) + + if charge_done_config := config.get(CONF_CHARGE_DONE): + sens = await binary_sensor.new_binary_sensor(charge_done_config) + cg.add(parent.add_listener(sens)) diff --git a/esphome/components/sy6970/binary_sensor/sy6970_binary_sensor.h b/esphome/components/sy6970/binary_sensor/sy6970_binary_sensor.h new file mode 100644 index 0000000000..4a374d7e3d --- /dev/null +++ b/esphome/components/sy6970/binary_sensor/sy6970_binary_sensor.h @@ -0,0 +1,43 @@ +#pragma once + +#include "../sy6970.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome::sy6970 { + +template +class StatusBinarySensor : public SY6970Listener, public binary_sensor::BinarySensor { + public: + void on_data(const SY6970Data &data) override { + uint8_t value = (data.registers[REG] >> SHIFT) & MASK; + this->publish_state(value == TRUE_VALUE); + } +}; + +template +class InverseStatusBinarySensor : public SY6970Listener, public binary_sensor::BinarySensor { + public: + void on_data(const SY6970Data &data) override { + uint8_t value = (data.registers[REG] >> SHIFT) & MASK; + this->publish_state(value != FALSE_VALUE); + } +}; + +// Custom binary sensor for charging (true when pre-charge or fast charge) +class SY6970ChargingBinarySensor : public SY6970Listener, public binary_sensor::BinarySensor { + public: + void on_data(const SY6970Data &data) override { + uint8_t chrg_stat = (data.registers[SY6970_REG_STATUS] >> 3) & 0x03; + bool charging = chrg_stat != CHARGE_STATUS_NOT_CHARGING && chrg_stat != CHARGE_STATUS_CHARGE_DONE; + this->publish_state(charging); + } +}; + +// Specialized sensor types using templates +// VBUS connected: BUS_STATUS != NO_INPUT +using SY6970VbusConnectedBinarySensor = InverseStatusBinarySensor; + +// Charge done: CHARGE_STATUS == CHARGE_DONE +using SY6970ChargeDoneBinarySensor = StatusBinarySensor; + +} // namespace esphome::sy6970 diff --git a/esphome/components/sy6970/sensor/__init__.py b/esphome/components/sy6970/sensor/__init__.py new file mode 100644 index 0000000000..e6ee9d1337 --- /dev/null +++ b/esphome/components/sy6970/sensor/__init__.py @@ -0,0 +1,95 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_BATTERY_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_MILLIAMP, + UNIT_VOLT, +) + +from .. import CONF_SY6970_ID, SY6970Component, sy6970_ns + +DEPENDENCIES = ["sy6970"] + +CONF_VBUS_VOLTAGE = "vbus_voltage" +CONF_SYSTEM_VOLTAGE = "system_voltage" +CONF_CHARGE_CURRENT = "charge_current" +CONF_PRECHARGE_CURRENT = "precharge_current" + +SY6970VbusVoltageSensor = sy6970_ns.class_("SY6970VbusVoltageSensor", sensor.Sensor) +SY6970BatteryVoltageSensor = sy6970_ns.class_( + "SY6970BatteryVoltageSensor", sensor.Sensor +) +SY6970SystemVoltageSensor = sy6970_ns.class_("SY6970SystemVoltageSensor", sensor.Sensor) +SY6970ChargeCurrentSensor = sy6970_ns.class_("SY6970ChargeCurrentSensor", sensor.Sensor) +SY6970PrechargeCurrentSensor = sy6970_ns.class_( + "SY6970PrechargeCurrentSensor", sensor.Sensor +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_SY6970_ID): cv.use_id(SY6970Component), + cv.Optional(CONF_VBUS_VOLTAGE): sensor.sensor_schema( + SY6970VbusVoltageSensor, + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( + SY6970BatteryVoltageSensor, + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_SYSTEM_VOLTAGE): sensor.sensor_schema( + SY6970SystemVoltageSensor, + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CHARGE_CURRENT): sensor.sensor_schema( + SY6970ChargeCurrentSensor, + unit_of_measurement=UNIT_MILLIAMP, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PRECHARGE_CURRENT): sensor.sensor_schema( + SY6970PrechargeCurrentSensor, + unit_of_measurement=UNIT_MILLIAMP, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_SY6970_ID]) + + if vbus_voltage_config := config.get(CONF_VBUS_VOLTAGE): + sens = await sensor.new_sensor(vbus_voltage_config) + cg.add(parent.add_listener(sens)) + + if battery_voltage_config := config.get(CONF_BATTERY_VOLTAGE): + sens = await sensor.new_sensor(battery_voltage_config) + cg.add(parent.add_listener(sens)) + + if system_voltage_config := config.get(CONF_SYSTEM_VOLTAGE): + sens = await sensor.new_sensor(system_voltage_config) + cg.add(parent.add_listener(sens)) + + if charge_current_config := config.get(CONF_CHARGE_CURRENT): + sens = await sensor.new_sensor(charge_current_config) + cg.add(parent.add_listener(sens)) + + if precharge_current_config := config.get(CONF_PRECHARGE_CURRENT): + sens = await sensor.new_sensor(precharge_current_config) + cg.add(parent.add_listener(sens)) diff --git a/esphome/components/sy6970/sensor/sy6970_sensor.h b/esphome/components/sy6970/sensor/sy6970_sensor.h new file mode 100644 index 0000000000..f912d726b2 --- /dev/null +++ b/esphome/components/sy6970/sensor/sy6970_sensor.h @@ -0,0 +1,46 @@ +#pragma once + +#include "../sy6970.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome::sy6970 { + +// Template for voltage sensors (converts mV to V) +template +class VoltageSensor : public SY6970Listener, public sensor::Sensor { + public: + void on_data(const SY6970Data &data) override { + uint8_t val = data.registers[REG] & MASK; + uint16_t voltage_mv = BASE + (val * STEP); + this->publish_state(voltage_mv * 0.001f); // Convert mV to V + } +}; + +// Template for current sensors (returns mA) +template +class CurrentSensor : public SY6970Listener, public sensor::Sensor { + public: + void on_data(const SY6970Data &data) override { + uint8_t val = data.registers[REG] & MASK; + uint16_t current_ma = BASE + (val * STEP); + this->publish_state(current_ma); + } +}; + +// Specialized sensor types using templates +using SY6970VbusVoltageSensor = VoltageSensor; +using SY6970BatteryVoltageSensor = VoltageSensor; +using SY6970SystemVoltageSensor = VoltageSensor; +using SY6970ChargeCurrentSensor = CurrentSensor; + +// Precharge current sensor needs special handling (bit shift) +class SY6970PrechargeCurrentSensor : public SY6970Listener, public sensor::Sensor { + public: + void on_data(const SY6970Data &data) override { + uint8_t iprechg = (data.registers[SY6970_REG_PRECHARGE_CURRENT] >> 4) & 0x0F; + uint16_t iprechg_ma = PRE_CHG_BASE_MA + (iprechg * PRE_CHG_STEP_MA); + this->publish_state(iprechg_ma); + } +}; + +} // namespace esphome::sy6970 diff --git a/esphome/components/sy6970/sy6970.cpp b/esphome/components/sy6970/sy6970.cpp new file mode 100644 index 0000000000..1f1648cfa7 --- /dev/null +++ b/esphome/components/sy6970/sy6970.cpp @@ -0,0 +1,201 @@ +#include "sy6970.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome::sy6970 { + +static const char *const TAG = "sy6970"; + +bool SY6970Component::read_all_registers_() { + // Read all registers from 0x00 to 0x14 in one transaction (21 bytes) + // This includes unused registers 0x0F, 0x10 for performance + if (!this->read_bytes(SY6970_REG_INPUT_CURRENT_LIMIT, this->data_.registers, 21)) { + ESP_LOGW(TAG, "Failed to read registers 0x00-0x14"); + return false; + } + + return true; +} + +bool SY6970Component::write_register_(uint8_t reg, uint8_t value) { + if (!this->write_byte(reg, value)) { + ESP_LOGW(TAG, "Failed to write register 0x%02X", reg); + return false; + } + return true; +} + +bool SY6970Component::update_register_(uint8_t reg, uint8_t mask, uint8_t value) { + uint8_t reg_value; + if (!this->read_byte(reg, ®_value)) { + ESP_LOGW(TAG, "Failed to read register 0x%02X for update", reg); + return false; + } + reg_value = (reg_value & ~mask) | (value & mask); + return this->write_register_(reg, reg_value); +} + +void SY6970Component::setup() { + ESP_LOGV(TAG, "Setting up SY6970..."); + + // Try to read chip ID + uint8_t reg_value; + if (!this->read_byte(SY6970_REG_DEVICE_ID, ®_value)) { + ESP_LOGE(TAG, "Failed to communicate with SY6970"); + this->mark_failed(); + return; + } + + uint8_t chip_id = reg_value & 0x03; + if (chip_id != 0x00) { + ESP_LOGW(TAG, "Unexpected chip ID: 0x%02X (expected 0x00)", chip_id); + } + + // Apply configuration options (all have defaults now) + ESP_LOGV(TAG, "Setting LED enabled to %s", ONOFF(this->led_enabled_)); + this->set_led_enabled(this->led_enabled_); + + ESP_LOGV(TAG, "Setting input current limit to %u mA", this->input_current_limit_); + this->set_input_current_limit(this->input_current_limit_); + + ESP_LOGV(TAG, "Setting charge voltage to %u mV", this->charge_voltage_); + this->set_charge_target_voltage(this->charge_voltage_); + + ESP_LOGV(TAG, "Setting charge current to %u mA", this->charge_current_); + this->set_charge_current(this->charge_current_); + + ESP_LOGV(TAG, "Setting precharge current to %u mA", this->precharge_current_); + this->set_precharge_current(this->precharge_current_); + + ESP_LOGV(TAG, "Setting charge enabled to %s", ONOFF(this->charge_enabled_)); + this->set_charge_enabled(this->charge_enabled_); + + ESP_LOGV(TAG, "Setting ADC measurements to %s", ONOFF(this->enable_adc_)); + this->set_enable_adc_measure(this->enable_adc_); + + ESP_LOGV(TAG, "SY6970 initialized successfully"); +} + +void SY6970Component::dump_config() { + ESP_LOGCONFIG(TAG, + "SY6970:\n" + " LED Enabled: %s\n" + " Input Current Limit: %u mA\n" + " Charge Voltage: %u mV\n" + " Charge Current: %u mA\n" + " Precharge Current: %u mA\n" + " Charge Enabled: %s\n" + " ADC Enabled: %s", + ONOFF(this->led_enabled_), this->input_current_limit_, this->charge_voltage_, this->charge_current_, + this->precharge_current_, ONOFF(this->charge_enabled_), ONOFF(this->enable_adc_)); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with SY6970 failed!"); + } +} + +void SY6970Component::update() { + if (this->is_failed()) { + return; + } + + // Read all registers in one transaction + if (!this->read_all_registers_()) { + ESP_LOGW(TAG, "Failed to read registers during update"); + this->status_set_warning(); + return; + } + + this->status_clear_warning(); + + // Notify all listeners with the new data + for (auto *listener : this->listeners_) { + listener->on_data(this->data_); + } +} + +void SY6970Component::set_input_current_limit(uint16_t milliamps) { + if (this->is_failed()) + return; + + if (milliamps < INPUT_CURRENT_MIN) { + milliamps = INPUT_CURRENT_MIN; + } + + uint8_t val = (milliamps - INPUT_CURRENT_MIN) / INPUT_CURRENT_STEP; + if (val > 0x3F) { + val = 0x3F; + } + + this->update_register_(SY6970_REG_INPUT_CURRENT_LIMIT, 0x3F, val); +} + +void SY6970Component::set_charge_target_voltage(uint16_t millivolts) { + if (this->is_failed()) + return; + + if (millivolts < CHG_VOLTAGE_BASE) { + millivolts = CHG_VOLTAGE_BASE; + } + + uint8_t val = (millivolts - CHG_VOLTAGE_BASE) / CHG_VOLTAGE_STEP; + if (val > 0x3F) { + val = 0x3F; + } + + this->update_register_(SY6970_REG_CHARGE_VOLTAGE, 0xFC, val << 2); +} + +void SY6970Component::set_precharge_current(uint16_t milliamps) { + if (this->is_failed()) + return; + + if (milliamps < PRE_CHG_BASE_MA) { + milliamps = PRE_CHG_BASE_MA; + } + + uint8_t val = (milliamps - PRE_CHG_BASE_MA) / PRE_CHG_STEP_MA; + if (val > 0x0F) { + val = 0x0F; + } + + this->update_register_(SY6970_REG_PRECHARGE_CURRENT, 0xF0, val << 4); +} + +void SY6970Component::set_charge_current(uint16_t milliamps) { + if (this->is_failed()) + return; + + uint8_t val = milliamps / 64; + if (val > 0x7F) { + val = 0x7F; + } + + this->update_register_(SY6970_REG_CHARGE_CURRENT, 0x7F, val); +} + +void SY6970Component::set_charge_enabled(bool enabled) { + if (this->is_failed()) + return; + + this->update_register_(SY6970_REG_SYS_CONTROL, 0x10, enabled ? 0x10 : 0x00); +} + +void SY6970Component::set_led_enabled(bool enabled) { + if (this->is_failed()) + return; + + // Bit 6: 0 = LED enabled, 1 = LED disabled + this->update_register_(SY6970_REG_TIMER_CONTROL, 0x40, enabled ? 0x00 : 0x40); +} + +void SY6970Component::set_enable_adc_measure(bool enabled) { + if (this->is_failed()) + return; + + // Set bits to enable ADC conversion + this->update_register_(SY6970_REG_ADC_CONTROL, 0xC0, enabled ? 0xC0 : 0x00); +} + +} // namespace esphome::sy6970 diff --git a/esphome/components/sy6970/sy6970.h b/esphome/components/sy6970/sy6970.h new file mode 100644 index 0000000000..bacc072f9b --- /dev/null +++ b/esphome/components/sy6970/sy6970.h @@ -0,0 +1,122 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" +#include + +namespace esphome::sy6970 { + +// SY6970 Register addresses with descriptive names +static const uint8_t SY6970_REG_INPUT_CURRENT_LIMIT = 0x00; // Input current limit control +static const uint8_t SY6970_REG_VINDPM = 0x01; // Input voltage limit +static const uint8_t SY6970_REG_ADC_CONTROL = 0x02; // ADC control and function disable +static const uint8_t SY6970_REG_SYS_CONTROL = 0x03; // Charge enable and system config +static const uint8_t SY6970_REG_CHARGE_CURRENT = 0x04; // Fast charge current limit +static const uint8_t SY6970_REG_PRECHARGE_CURRENT = 0x05; // Pre-charge/termination current +static const uint8_t SY6970_REG_CHARGE_VOLTAGE = 0x06; // Charge voltage limit +static const uint8_t SY6970_REG_TIMER_CONTROL = 0x07; // Charge timer and status LED control +static const uint8_t SY6970_REG_IR_COMP = 0x08; // IR compensation +static const uint8_t SY6970_REG_FORCE_DPDM = 0x09; // Force DPDM detection +static const uint8_t SY6970_REG_BOOST_CONTROL = 0x0A; // Boost mode voltage/current +static const uint8_t SY6970_REG_STATUS = 0x0B; // System status (bus, charge status) +static const uint8_t SY6970_REG_FAULT = 0x0C; // Fault status (NTC) +static const uint8_t SY6970_REG_VINDPM_STATUS = 0x0D; // Input voltage limit status (also sys voltage) +static const uint8_t SY6970_REG_BATV = 0x0E; // Battery voltage +static const uint8_t SY6970_REG_VBUS_VOLTAGE = 0x11; // VBUS voltage +static const uint8_t SY6970_REG_CHARGE_CURRENT_MONITOR = 0x12; // Charge current +static const uint8_t SY6970_REG_INPUT_VOLTAGE_LIMIT = 0x13; // Input voltage limit +static const uint8_t SY6970_REG_DEVICE_ID = 0x14; // Part information + +// Constants for voltage and current calculations +static const uint16_t VBUS_BASE_MV = 2600; // mV +static const uint16_t VBUS_STEP_MV = 100; // mV +static const uint16_t VBAT_BASE_MV = 2304; // mV +static const uint16_t VBAT_STEP_MV = 20; // mV +static const uint16_t VSYS_BASE_MV = 2304; // mV +static const uint16_t VSYS_STEP_MV = 20; // mV +static const uint16_t CHG_CURRENT_STEP_MA = 50; // mA +static const uint16_t PRE_CHG_BASE_MA = 64; // mA +static const uint16_t PRE_CHG_STEP_MA = 64; // mA +static const uint16_t CHG_VOLTAGE_BASE = 3840; // mV +static const uint16_t CHG_VOLTAGE_STEP = 16; // mV +static const uint16_t INPUT_CURRENT_MIN = 100; // mA +static const uint16_t INPUT_CURRENT_STEP = 50; // mA + +// Bus Status values (REG_0B[7:5]) +enum BusStatus { + BUS_STATUS_NO_INPUT = 0, + BUS_STATUS_USB_SDP = 1, + BUS_STATUS_USB_CDP = 2, + BUS_STATUS_USB_DCP = 3, + BUS_STATUS_HVDCP = 4, + BUS_STATUS_ADAPTER = 5, + BUS_STATUS_NO_STD_ADAPTER = 6, + BUS_STATUS_OTG = 7, +}; + +// Charge Status values (REG_0B[4:3]) +enum ChargeStatus { + CHARGE_STATUS_NOT_CHARGING = 0, + CHARGE_STATUS_PRE_CHARGE = 1, + CHARGE_STATUS_FAST_CHARGE = 2, + CHARGE_STATUS_CHARGE_DONE = 3, +}; + +// Structure to hold all register data read in one transaction +struct SY6970Data { + uint8_t registers[21]; // Registers 0x00-0x14 (includes unused 0x0F, 0x10) +}; + +// Listener interface for components that want to receive SY6970 data updates +class SY6970Listener { + public: + virtual void on_data(const SY6970Data &data) = 0; +}; + +class SY6970Component : public PollingComponent, public i2c::I2CDevice { + public: + SY6970Component(bool led_enabled, uint16_t input_current_limit, uint16_t charge_voltage, uint16_t charge_current, + uint16_t precharge_current, bool charge_enabled, bool enable_adc) + : led_enabled_(led_enabled), + input_current_limit_(input_current_limit), + charge_voltage_(charge_voltage), + charge_current_(charge_current), + precharge_current_(precharge_current), + charge_enabled_(charge_enabled), + enable_adc_(enable_adc) {} + void setup() override; + void dump_config() override; + void update() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + // Listener registration + void add_listener(SY6970Listener *listener) { this->listeners_.push_back(listener); } + + // Configuration methods to be called from lambdas + void set_input_current_limit(uint16_t milliamps); + void set_charge_target_voltage(uint16_t millivolts); + void set_precharge_current(uint16_t milliamps); + void set_charge_current(uint16_t milliamps); + void set_charge_enabled(bool enabled); + void set_led_enabled(bool enabled); + void set_enable_adc_measure(bool enabled = true); + + protected: + bool read_all_registers_(); + bool write_register_(uint8_t reg, uint8_t value); + bool update_register_(uint8_t reg, uint8_t mask, uint8_t value); + + SY6970Data data_{}; + std::vector listeners_; + + // Configuration values to set during setup() + bool led_enabled_; + uint16_t input_current_limit_; + uint16_t charge_voltage_; + uint16_t charge_current_; + uint16_t precharge_current_; + bool charge_enabled_; + bool enable_adc_; +}; + +} // namespace esphome::sy6970 diff --git a/esphome/components/sy6970/text_sensor/__init__.py b/esphome/components/sy6970/text_sensor/__init__.py new file mode 100644 index 0000000000..2a4eb90811 --- /dev/null +++ b/esphome/components/sy6970/text_sensor/__init__.py @@ -0,0 +1,52 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv + +from .. import CONF_SY6970_ID, SY6970Component, sy6970_ns + +DEPENDENCIES = ["sy6970"] + +CONF_BUS_STATUS = "bus_status" +CONF_CHARGE_STATUS = "charge_status" +CONF_NTC_STATUS = "ntc_status" + +SY6970BusStatusTextSensor = sy6970_ns.class_( + "SY6970BusStatusTextSensor", text_sensor.TextSensor +) +SY6970ChargeStatusTextSensor = sy6970_ns.class_( + "SY6970ChargeStatusTextSensor", text_sensor.TextSensor +) +SY6970NtcStatusTextSensor = sy6970_ns.class_( + "SY6970NtcStatusTextSensor", text_sensor.TextSensor +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_SY6970_ID): cv.use_id(SY6970Component), + cv.Optional(CONF_BUS_STATUS): text_sensor.text_sensor_schema( + SY6970BusStatusTextSensor + ), + cv.Optional(CONF_CHARGE_STATUS): text_sensor.text_sensor_schema( + SY6970ChargeStatusTextSensor + ), + cv.Optional(CONF_NTC_STATUS): text_sensor.text_sensor_schema( + SY6970NtcStatusTextSensor + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_SY6970_ID]) + + if bus_status_config := config.get(CONF_BUS_STATUS): + sens = await text_sensor.new_text_sensor(bus_status_config) + cg.add(parent.add_listener(sens)) + + if charge_status_config := config.get(CONF_CHARGE_STATUS): + sens = await text_sensor.new_text_sensor(charge_status_config) + cg.add(parent.add_listener(sens)) + + if ntc_status_config := config.get(CONF_NTC_STATUS): + sens = await text_sensor.new_text_sensor(ntc_status_config) + cg.add(parent.add_listener(sens)) diff --git a/esphome/components/sy6970/text_sensor/sy6970_text_sensor.h b/esphome/components/sy6970/text_sensor/sy6970_text_sensor.h new file mode 100644 index 0000000000..665c5eca64 --- /dev/null +++ b/esphome/components/sy6970/text_sensor/sy6970_text_sensor.h @@ -0,0 +1,96 @@ +#pragma once + +#include "../sy6970.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome::sy6970 { + +// Bus status text sensor +class SY6970BusStatusTextSensor : public SY6970Listener, public text_sensor::TextSensor { + public: + void on_data(const SY6970Data &data) override { + uint8_t status = (data.registers[SY6970_REG_STATUS] >> 5) & 0x07; + const char *status_str = this->get_bus_status_string_(status); + this->publish_state(status_str); + } + + protected: + const char *get_bus_status_string_(uint8_t status) { + switch (status) { + case BUS_STATUS_NO_INPUT: + return "No Input"; + case BUS_STATUS_USB_SDP: + return "USB SDP"; + case BUS_STATUS_USB_CDP: + return "USB CDP"; + case BUS_STATUS_USB_DCP: + return "USB DCP"; + case BUS_STATUS_HVDCP: + return "HVDCP"; + case BUS_STATUS_ADAPTER: + return "Adapter"; + case BUS_STATUS_NO_STD_ADAPTER: + return "Non-Standard Adapter"; + case BUS_STATUS_OTG: + return "OTG"; + default: + return "Unknown"; + } + } +}; + +// Charge status text sensor +class SY6970ChargeStatusTextSensor : public SY6970Listener, public text_sensor::TextSensor { + public: + void on_data(const SY6970Data &data) override { + uint8_t status = (data.registers[SY6970_REG_STATUS] >> 3) & 0x03; + const char *status_str = this->get_charge_status_string_(status); + this->publish_state(status_str); + } + + protected: + const char *get_charge_status_string_(uint8_t status) { + switch (status) { + case CHARGE_STATUS_NOT_CHARGING: + return "Not Charging"; + case CHARGE_STATUS_PRE_CHARGE: + return "Pre-charge"; + case CHARGE_STATUS_FAST_CHARGE: + return "Fast Charge"; + case CHARGE_STATUS_CHARGE_DONE: + return "Charge Done"; + default: + return "Unknown"; + } + } +}; + +// NTC status text sensor +class SY6970NtcStatusTextSensor : public SY6970Listener, public text_sensor::TextSensor { + public: + void on_data(const SY6970Data &data) override { + uint8_t status = data.registers[SY6970_REG_FAULT] & 0x07; + const char *status_str = this->get_ntc_status_string_(status); + this->publish_state(status_str); + } + + protected: + const char *get_ntc_status_string_(uint8_t status) { + switch (status) { + case 0: + return "Normal"; + case 2: + return "Warm"; + case 3: + return "Cool"; + case 5: + return "Cold"; + case 6: + return "Hot"; + default: + return "Unknown"; + } + } +}; + +} // namespace esphome::sy6970 diff --git a/esphome/components/template/water_heater/__init__.py b/esphome/components/template/water_heater/__init__.py index 716289035a..bddd378b23 100644 --- a/esphome/components/template/water_heater/__init__.py +++ b/esphome/components/template/water_heater/__init__.py @@ -20,7 +20,7 @@ from .. import template_ns CONF_CURRENT_TEMPERATURE = "current_temperature" TemplateWaterHeater = template_ns.class_( - "TemplateWaterHeater", water_heater.WaterHeater + "TemplateWaterHeater", cg.Component, water_heater.WaterHeater ) TemplateWaterHeaterPublishAction = template_ns.class_( @@ -36,24 +36,29 @@ RESTORE_MODES = { "RESTORE_AND_CALL": TemplateWaterHeaterRestoreMode.WATER_HEATER_RESTORE_AND_CALL, } -CONFIG_SCHEMA = water_heater.water_heater_schema(TemplateWaterHeater).extend( - { - cv.Optional(CONF_OPTIMISTIC, default=True): cv.boolean, - cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_RESTORE_MODE, default="NO_RESTORE"): cv.enum( - RESTORE_MODES, upper=True - ), - cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda, - cv.Optional(CONF_MODE): cv.returning_lambda, - cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( - water_heater.validate_water_heater_mode - ), - } +CONFIG_SCHEMA = ( + water_heater.water_heater_schema(TemplateWaterHeater) + .extend( + { + cv.Optional(CONF_OPTIMISTIC, default=True): cv.boolean, + cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_RESTORE_MODE, default="NO_RESTORE"): cv.enum( + RESTORE_MODES, upper=True + ), + cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda, + cv.Optional(CONF_MODE): cv.returning_lambda, + cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( + water_heater.validate_water_heater_mode + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) ) async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) await water_heater.register_water_heater(var, config) cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) diff --git a/esphome/components/template/water_heater/template_water_heater.cpp b/esphome/components/template/water_heater/template_water_heater.cpp index 5ae5c30f36..e89c96ca48 100644 --- a/esphome/components/template/water_heater/template_water_heater.cpp +++ b/esphome/components/template/water_heater/template_water_heater.cpp @@ -10,7 +10,7 @@ TemplateWaterHeater::TemplateWaterHeater() : set_trigger_(new Trigger<>()) {} void TemplateWaterHeater::setup() { if (this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE || this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE_AND_CALL) { - auto restore = this->restore_state(); + auto restore = this->restore_state_(); if (restore.has_value()) { restore->perform(); diff --git a/esphome/components/template/water_heater/template_water_heater.h b/esphome/components/template/water_heater/template_water_heater.h index e5f51b72dc..c2a2dcbb23 100644 --- a/esphome/components/template/water_heater/template_water_heater.h +++ b/esphome/components/template/water_heater/template_water_heater.h @@ -13,7 +13,7 @@ enum TemplateWaterHeaterRestoreMode { WATER_HEATER_RESTORE_AND_CALL, }; -class TemplateWaterHeater : public water_heater::WaterHeater { +class TemplateWaterHeater : public Component, public water_heater::WaterHeater { public: TemplateWaterHeater(); 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/water_heater/__init__.py b/esphome/components/water_heater/__init__.py index 5420e7c435..db32c2d919 100644 --- a/esphome/components/water_heater/__init__.py +++ b/esphome/components/water_heater/__init__.py @@ -18,7 +18,7 @@ CODEOWNERS = ["@dhoeben"] IS_PLATFORM_COMPONENT = True water_heater_ns = cg.esphome_ns.namespace("water_heater") -WaterHeater = water_heater_ns.class_("WaterHeater", cg.EntityBase, cg.Component) +WaterHeater = water_heater_ns.class_("WaterHeater", cg.EntityBase) WaterHeaterCall = water_heater_ns.class_("WaterHeaterCall") WaterHeaterTraits = water_heater_ns.class_("WaterHeaterTraits") @@ -46,7 +46,7 @@ _WATER_HEATER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( } ), } -).extend(cv.COMPONENT_SCHEMA) +) _WATER_HEATER_SCHEMA.add_extra(entity_duplicate_validator("water_heater")) @@ -91,8 +91,6 @@ async def register_water_heater(var: cg.Pvariable, config: ConfigType) -> cg.Pva cg.add_define("USE_WATER_HEATER") - await cg.register_component(var, config) - cg.add(cg.App.register_water_heater(var)) CORE.register_platform_component("water_heater", var) diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index 7b947057e1..fbb4181209 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -146,10 +146,6 @@ void WaterHeaterCall::validate_() { } } -void WaterHeater::setup() { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); -} - void WaterHeater::publish_state() { auto traits = this->get_traits(); ESP_LOGD(TAG, @@ -188,7 +184,8 @@ void WaterHeater::publish_state() { this->pref_.save(&saved); } -optional WaterHeater::restore_state() { +optional WaterHeater::restore_state_() { + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); SavedWaterHeaterState recovered{}; if (!this->pref_.load(&recovered)) return {}; diff --git a/esphome/components/water_heater/water_heater.h b/esphome/components/water_heater/water_heater.h index e223dd59b2..84fc46d208 100644 --- a/esphome/components/water_heater/water_heater.h +++ b/esphome/components/water_heater/water_heater.h @@ -177,7 +177,7 @@ class WaterHeaterTraits { WaterHeaterModeMask supported_modes_; }; -class WaterHeater : public EntityBase, public Component { +class WaterHeater : public EntityBase { public: WaterHeaterMode get_mode() const { return this->mode_; } float get_current_temperature() const { return this->current_temperature_; } @@ -204,16 +204,15 @@ class WaterHeater : public EntityBase, public Component { #endif virtual void control(const WaterHeaterCall &call) = 0; - void setup() override; - - optional restore_state(); - protected: virtual WaterHeaterTraits traits() = 0; /// Log the traits of this water heater for dump_config(). void dump_traits_(const char *tag); + /// Restore the state of the water heater, call this from your setup() method. + optional restore_state_(); + /// Set the mode of the water heater. Should only be called from control(). void set_mode_(WaterHeaterMode mode) { this->mode_ = mode; } /// Set the target temperature of the water heater. Should only be called from control(). diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index a7524fd8b0..24bf367135 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -698,6 +698,10 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { if (!this->wifi_mode_(true, {})) return false; + // Reset scan_done_ before starting new scan to prevent stale flag from previous scan + // (e.g., roaming scan completed just before unexpected disconnect) + this->scan_done_ = false; + struct scan_config config {}; memset(&config, 0, sizeof(config)); config.ssid = nullptr; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index f7170eae4e..ff3687e68c 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #ifdef USE_WIFI_WPA2_EAP #if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) @@ -836,15 +837,31 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { this->scan_result_.reserve(WIFI_SCAN_RESULT_FILTERED_RESERVE); } - // Process one record at a time to avoid large buffer allocation - wifi_ap_record_t record; +#ifdef USE_ESP32_HOSTED + // getting records one at a time fails on P4 with hosted esp32 WiFi coprocessor + // Presumably an upstream bug, work-around by getting all records at once + // Use stack buffer (3904 bytes / ~80 bytes per record = ~48 records) with heap fallback + static constexpr size_t SCAN_RECORD_STACK_COUNT = 3904 / sizeof(wifi_ap_record_t); + SmallBufferWithHeapFallback records(number); + err = esp_wifi_scan_get_ap_records(&number, records.get()); + if (err != ESP_OK) { + esp_wifi_clear_ap_list(); + ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed: %s", esp_err_to_name(err)); + return; + } for (uint16_t i = 0; i < number; i++) { + wifi_ap_record_t &record = records.get()[i]; +#else + // Process one record at a time to avoid large buffer allocation + for (uint16_t i = 0; i < number; i++) { + wifi_ap_record_t record; err = esp_wifi_scan_get_ap_record(&record); if (err != ESP_OK) { ESP_LOGW(TAG, "esp_wifi_scan_get_ap_record failed: %s", esp_err_to_name(err)); esp_wifi_clear_ap_list(); // Free remaining records not yet retrieved break; } +#endif // USE_ESP32_HOSTED // Check C string first - avoid std::string construction for non-matching networks const char *ssid_cstr = reinterpret_cast(record.ssid); diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index c3c0d885e2..88d09ee53a 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -649,6 +649,10 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { if (!this->wifi_mode_(true, {})) return false; + // Reset scan_done_ before starting new scan to prevent stale flag from previous scan + // (e.g., roaming scan completed just before unexpected disconnect) + this->scan_done_ = false; + // need to use WiFi because of WiFiScanClass allocations :( int16_t err = WiFi.scanNetworks(true, true, passive, 200); if (err != WIFI_SCAN_RUNNING) { diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 7c13823fba..e98cdd0ba0 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -42,6 +42,7 @@ #define USE_DEVICES #define USE_DISPLAY #define USE_ENTITY_ICON +#define USE_ESP32_HOSTED #define USE_ESP32_IMPROV_STATE_CALLBACK #define USE_EVENT #define USE_FAN 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" diff --git a/tests/components/sy6970/common.yaml b/tests/components/sy6970/common.yaml new file mode 100644 index 0000000000..53699fe6fb --- /dev/null +++ b/tests/components/sy6970/common.yaml @@ -0,0 +1,57 @@ +sy6970: + id: sy6970_component + i2c_id: i2c_bus + address: 0x6A + enable_status_led: true + input_current_limit: 1000 + charge_voltage: 4200 + charge_current: 500 + precharge_current: 128 + charge_enabled: true + enable_adc: true + update_interval: 5s + +sensor: + - platform: sy6970 + sy6970_id: sy6970_component + vbus_voltage: + name: "VBUS Voltage" + id: vbus_voltage_sensor + battery_voltage: + name: "Battery Voltage" + id: battery_voltage_sensor + system_voltage: + name: "System Voltage" + id: system_voltage_sensor + charge_current: + name: "Charge Current" + id: charge_current_sensor + precharge_current: + name: "Precharge Current" + id: precharge_current_sensor + +binary_sensor: + - platform: sy6970 + sy6970_id: sy6970_component + vbus_connected: + name: "VBUS Connected" + id: vbus_connected_binary + charging: + name: "Charging" + id: charging_binary + charge_done: + name: "Charge Done" + id: charge_done_binary + +text_sensor: + - platform: sy6970 + sy6970_id: sy6970_component + bus_status: + name: "Bus Status" + id: bus_status_text + charge_status: + name: "Charge Status" + id: charge_status_text + ntc_status: + name: "NTC Status" + id: ntc_status_text diff --git a/tests/components/sy6970/test.esp32-idf.yaml b/tests/components/sy6970/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/sy6970/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml