From f93e84397272b811825ed55420022f74222250cc Mon Sep 17 00:00:00 2001 From: Big Mike Date: Sat, 24 Jan 2026 13:55:51 -0600 Subject: [PATCH 1/4] [sen5x] Fix mangled serial number (#13491) --- esphome/components/sen5x/sen5x.cpp | 36 +++++++++++++++++------------- esphome/components/sen5x/sen5x.h | 2 +- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index ea08a5ee77e..b23837f2b17 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -43,6 +43,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 +84,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)) { @@ -153,12 +162,8 @@ 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); + // 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_)) { @@ -264,9 +269,8 @@ void SEN5XComponent::dump_config() { ESP_LOGCONFIG(TAG, " Product name: %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", + this->product_name_.c_str(), 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()); } diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h index aacdfa57179..822160833ef 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -102,11 +102,11 @@ 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); + char serial_number_[17] = "UNKNOWN"; uint16_t voc_baseline_state_[4]{0}; uint32_t voc_baseline_time_; uint16_t firmware_version_; ERRORCODE error_code_; - uint8_t serial_number_[4]; bool initialized_{false}; bool store_baseline_; From 58746b737fb58c81df860360e469629452ef1e28 Mon Sep 17 00:00:00 2001 From: Big Mike Date: Sat, 24 Jan 2026 15:07:12 -0600 Subject: [PATCH 2/4] [sen5x] Eliminate product name string (#13489) --- esphome/components/sen5x/sen5x.cpp | 68 +++++++++++++++--------------- esphome/components/sen5x/sen5x.h | 6 +-- 2 files changed, 36 insertions(+), 38 deletions(-) diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index b23837f2b17..fc4932d8675 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: @@ -104,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; - } - } + this->type_ = Sen5xType::UNKNOWN; + ESP_LOGE(TAG, "Unknown product name: %.32s", product_name); + this->error_code_ = PRODUCT_NAME_FAILED; + this->mark_failed(); + return; } - ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str()); - 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 } @@ -267,10 +265,10 @@ void SEN5XComponent::dump_config() { } } ESP_LOGCONFIG(TAG, - " Product name: %s\n" + " Type: %s\n" " Firmware version: %d\n" " Serial number: %s", - this->product_name_.c_str(), this->firmware_version_, this->serial_number_); + 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()); } diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h index 822160833ef..e3bf931b416 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -24,6 +24,8 @@ enum RhtAccelerationMode : uint16_t { HIGH_ACCELERATION = 2, }; +enum class Sen5xType : uint8_t { SEN50, SEN54, SEN55, UNKNOWN }; + struct GasTuning { uint16_t index_offset; uint16_t learning_time_offset_hours; @@ -49,8 +51,6 @@ 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; } @@ -106,6 +106,7 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri uint16_t voc_baseline_state_[4]{0}; uint32_t voc_baseline_time_; uint16_t firmware_version_; + Sen5xType type_{Sen5xType::UNKNOWN}; ERRORCODE error_code_; bool initialized_{false}; bool store_baseline_; @@ -127,7 +128,6 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri optional nox_tuning_params_; optional temperature_compensation_; ESPPreferenceObject pref_; - std::string product_name_; }; } // namespace sen5x From 8d84fe0113587c9a2f4e0b37e263092881917af6 Mon Sep 17 00:00:00 2001 From: Stephen Cox <69124219+linkedupbits@users.noreply.github.com> Date: Sun, 25 Jan 2026 10:31:26 +1300 Subject: [PATCH 3/4] [sy6970] Support for the sy6970 BMS chip (#13311) --- CODEOWNERS | 1 + esphome/components/sy6970/__init__.py | 63 ++++++ .../sy6970/binary_sensor/__init__.py | 56 +++++ .../binary_sensor/sy6970_binary_sensor.h | 43 ++++ esphome/components/sy6970/sensor/__init__.py | 95 +++++++++ .../components/sy6970/sensor/sy6970_sensor.h | 46 ++++ esphome/components/sy6970/sy6970.cpp | 201 ++++++++++++++++++ esphome/components/sy6970/sy6970.h | 122 +++++++++++ .../components/sy6970/text_sensor/__init__.py | 52 +++++ .../sy6970/text_sensor/sy6970_text_sensor.h | 96 +++++++++ tests/components/sy6970/common.yaml | 57 +++++ tests/components/sy6970/test.esp32-idf.yaml | 4 + 12 files changed, 836 insertions(+) create mode 100644 esphome/components/sy6970/__init__.py create mode 100644 esphome/components/sy6970/binary_sensor/__init__.py create mode 100644 esphome/components/sy6970/binary_sensor/sy6970_binary_sensor.h create mode 100644 esphome/components/sy6970/sensor/__init__.py create mode 100644 esphome/components/sy6970/sensor/sy6970_sensor.h create mode 100644 esphome/components/sy6970/sy6970.cpp create mode 100644 esphome/components/sy6970/sy6970.h create mode 100644 esphome/components/sy6970/text_sensor/__init__.py create mode 100644 esphome/components/sy6970/text_sensor/sy6970_text_sensor.h create mode 100644 tests/components/sy6970/common.yaml create mode 100644 tests/components/sy6970/test.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 8537d451dbc..00a22fed7cc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -482,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/sy6970/__init__.py b/esphome/components/sy6970/__init__.py new file mode 100644 index 00000000000..2390d046e4d --- /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 00000000000..132b2820517 --- /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 00000000000..4a374d7e3d7 --- /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 00000000000..e6ee9d1337c --- /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 00000000000..f912d726b24 --- /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 00000000000..1f1648cfa77 --- /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 00000000000..bacc072f9b3 --- /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 00000000000..2a4eb90811a --- /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 00000000000..665c5eca643 --- /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/tests/components/sy6970/common.yaml b/tests/components/sy6970/common.yaml new file mode 100644 index 00000000000..53699fe6fb8 --- /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 00000000000..b47e39c3898 --- /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 From 993765d732e0c67dec192bb73577ea30aad2cb29 Mon Sep 17 00:00:00 2001 From: Douwe <61123717+dhoeben@users.noreply.github.com> Date: Sun, 25 Jan 2026 02:18:13 +0100 Subject: [PATCH 4/4] [water_heater] Remove Component inheritance from base class (#13510) Co-authored-by: J. Nick Koston Co-authored-by: Claude Opus 4.5 --- .../template/water_heater/__init__.py | 33 +++++++++++-------- .../water_heater/template_water_heater.cpp | 2 +- .../water_heater/template_water_heater.h | 2 +- esphome/components/water_heater/__init__.py | 6 ++-- .../components/water_heater/water_heater.cpp | 7 ++-- .../components/water_heater/water_heater.h | 9 +++-- 6 files changed, 29 insertions(+), 30 deletions(-) diff --git a/esphome/components/template/water_heater/__init__.py b/esphome/components/template/water_heater/__init__.py index 716289035a2..bddd378b231 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 5ae5c30f365..e89c96ca484 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 e5f51b72dcd..c2a2dcbb236 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/water_heater/__init__.py b/esphome/components/water_heater/__init__.py index 5420e7c4351..db32c2d9193 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 7b947057e11..fbb4181209e 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 e223dd59b22..84fc46d2084 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().