diff --git a/CODEOWNERS b/CODEOWNERS index 8537d451db..00a22fed7c 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 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/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