diff --git a/CODEOWNERS b/CODEOWNERS index dade427a45..08ce43b94c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -235,6 +235,7 @@ esphome/components/kamstrup_kmp/* @cfeenstra1024 esphome/components/key_collector/* @ssieb esphome/components/key_provider/* @ssieb esphome/components/kuntze/* @ssieb +esphome/components/lc709203f/* @ilikecake esphome/components/lcd_menu/* @numo68 esphome/components/ld2410/* @regevbr @sebcaps esphome/components/ld2420/* @descipher diff --git a/esphome/components/lc709203f/__init__.py b/esphome/components/lc709203f/__init__.py new file mode 100644 index 0000000000..3be68d174f --- /dev/null +++ b/esphome/components/lc709203f/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@ilikecake"] diff --git a/esphome/components/lc709203f/lc709203f.cpp b/esphome/components/lc709203f/lc709203f.cpp new file mode 100644 index 0000000000..689478b383 --- /dev/null +++ b/esphome/components/lc709203f/lc709203f.cpp @@ -0,0 +1,297 @@ +#include "esphome/core/log.h" +#include "lc709203f.h" + +namespace esphome { +namespace lc709203f { + +static const char *const TAG = "lc709203f.sensor"; + +// Device I2C address. This address is fixed. +static const uint8_t LC709203F_I2C_ADDR_DEFAULT = 0x0B; + +// Device registers +static const uint8_t LC709203F_BEFORE_RSOC = 0x04; +static const uint8_t LC709203F_THERMISTOR_B = 0x06; +static const uint8_t LC709203F_INITIAL_RSOC = 0x07; +static const uint8_t LC709203F_CELL_TEMPERATURE = 0x08; +static const uint8_t LC709203F_CELL_VOLTAGE = 0x09; +static const uint8_t LC709203F_CURRENT_DIRECTION = 0x0A; +static const uint8_t LC709203F_APA = 0x0B; +static const uint8_t LC709203F_APT = 0x0C; +static const uint8_t LC709203F_RSOC = 0x0D; +static const uint8_t LC709203F_ITE = 0x0F; +static const uint8_t LC709203F_IC_VERSION = 0x11; +static const uint8_t LC709203F_CHANGE_OF_THE_PARAMETER = 0x12; +static const uint8_t LC709203F_ALARM_LOW_RSOC = 0x13; +static const uint8_t LC709203F_ALARM_LOW_CELL_VOLTAGE = 0x14; +static const uint8_t LC709203F_IC_POWER_MODE = 0x15; +static const uint8_t LC709203F_STATUS_BIT = 0x16; +static const uint8_t LC709203F_NUMBER_OF_THE_PARAMETER = 0x1A; + +static const uint8_t LC709203F_POWER_MODE_ON = 0x0001; +static const uint8_t LC709203F_POWER_MODE_SLEEP = 0x0002; + +// The number of times to retry an I2C transaction before giving up. In my experience, +// 10 is a good number here that will take care of most bus issues that require retry. +static const uint8_t LC709203F_I2C_RETRY_COUNT = 10; + +void Lc709203f::setup() { + // Note: The setup implements a small state machine. This is because we want to have + // delays before and after sending the RSOC command. The full init process should be: + // INIT->RSOC->TEMP_SETUP->NORMAL + // The setup() function will only perform the first part of the initialization process. + // Assuming no errors, the whole process should occur during the setup() function and + // the first two calls to update(). After that, the part should remain in normal mode + // until a device reset. + // + // This device can be picky about I2C communication and can error out occasionally. The + // get/set register functions impelment retry logic to retry the I2C transactions. The + // initialization code checks the return code from those functions. If they don't return + // NO_ERROR (0x00), that part of the initialization aborts and will be retried on the next + // call to update(). + ESP_LOGCONFIG(TAG, "Running setup"); + + // Set power mode to on. Note that, unlike some other similar devices, in sleep mode the IC + // does not record power usage. If there is significant power consumption during sleep mode, + // the pack RSOC will likely no longer be correct. Because of that, I do not implement + // sleep mode on this device. + + // Initialize device registers. If any of these fail, retry during the update() function. + if (this->set_register_(LC709203F_IC_POWER_MODE, LC709203F_POWER_MODE_ON) != i2c::NO_ERROR) { + return; + } + + if (this->set_register_(LC709203F_APA, this->apa_) != i2c::NO_ERROR) { + return; + } + + if (this->set_register_(LC709203F_CHANGE_OF_THE_PARAMETER, this->pack_voltage_) != i2c::NO_ERROR) { + return; + } + + this->state_ = STATE_RSOC; + // Note: Initialization continues in the update() function. +} + +void Lc709203f::update() { + uint16_t buffer; + + if (this->state_ == STATE_NORMAL) { + // Note: If we fail to read from the data registers, we do not report any sensor reading. + if (this->voltage_sensor_ != nullptr) { + if (this->get_register_(LC709203F_CELL_VOLTAGE, &buffer) == i2c::NO_ERROR) { + // Raw units are mV + this->voltage_sensor_->publish_state(static_cast(buffer) / 1000.0f); + this->status_clear_warning(); + } + } + if (this->battery_remaining_sensor_ != nullptr) { + if (this->get_register_(LC709203F_ITE, &buffer) == i2c::NO_ERROR) { + // Raw units are .1% + this->battery_remaining_sensor_->publish_state(static_cast(buffer) / 10.0f); + this->status_clear_warning(); + } + } + if (this->temperature_sensor_ != nullptr) { + // I can't test this with a real thermistor because I don't have a device with + // an attached thermistor. I have turned on the sensor and made sure that it + // sets up the registers properly. + if (this->get_register_(LC709203F_CELL_TEMPERATURE, &buffer) == i2c::NO_ERROR) { + // Raw units are .1 K + this->temperature_sensor_->publish_state((static_cast(buffer) / 10.0f) - 273.15f); + this->status_clear_warning(); + } + } + } else if (this->state_ == STATE_INIT) { + // Retry initializing the device registers. We should only get here if the init sequence + // failed during the setup() function. This would likely occur because of a repeated failures + // on the I2C bus. If any of these fail, retry the next time the update() function is called. + if (this->set_register_(LC709203F_IC_POWER_MODE, LC709203F_POWER_MODE_ON) != i2c::NO_ERROR) { + return; + } + + if (this->set_register_(LC709203F_APA, this->apa_) != i2c::NO_ERROR) { + return; + } + + if (this->set_register_(LC709203F_CHANGE_OF_THE_PARAMETER, this->pack_voltage_) != i2c::NO_ERROR) { + return; + } + + this->state_ = STATE_RSOC; + + } else if (this->state_ == STATE_RSOC) { + // We implement a delay here to send the initial RSOC command. + // This should run once on the first update() after initialization. + if (this->set_register_(LC709203F_INITIAL_RSOC, 0xAA55) == i2c::NO_ERROR) { + this->state_ = STATE_TEMP_SETUP; + } + } else if (this->state_ == STATE_TEMP_SETUP) { + // This should run once on the second update() after initialization. + if (this->temperature_sensor_ != nullptr) { + // This assumes that a thermistor is attached to the device as shown in the datahseet. + if (this->set_register_(LC709203F_STATUS_BIT, 0x0001) == i2c::NO_ERROR) { + if (this->set_register_(LC709203F_THERMISTOR_B, this->b_constant_) == i2c::NO_ERROR) { + this->state_ = STATE_NORMAL; + } + } + } else if (this->set_register_(LC709203F_STATUS_BIT, 0x0000) == i2c::NO_ERROR) { + // The device expects to get updates to the temperature in this mode. + // I am not doing that now. The temperature register defaults to 25C. + // In theory, we could have another temperature sensor and have ESPHome + // send updated temperature to the device occasionally, but I have no idea + // how to make that happen. + this->state_ = STATE_NORMAL; + } + } +} + +void Lc709203f::dump_config() { + ESP_LOGCONFIG(TAG, "LC709203F:"); + LOG_I2C_DEVICE(this); + + LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, " Pack Size: %d mAH", this->pack_size_); + ESP_LOGCONFIG(TAG, " Pack APA: 0x%02X", this->apa_); + + // This is only true if the pack_voltage_ is either 0x0000 or 0x0001. The config validator + // should have already verified this. + ESP_LOGCONFIG(TAG, " Pack Rated Voltage: 3.%sV", this->pack_voltage_ == 0x0000 ? "8" : "7"); + + LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); + LOG_SENSOR(" ", "Battery Remaining", this->battery_remaining_sensor_); + + if (this->temperature_sensor_ != nullptr) { + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + ESP_LOGCONFIG(TAG, " B_Constant: %d", this->b_constant_); + } else { + ESP_LOGCONFIG(TAG, " No Temperature Sensor"); + } +} + +uint8_t Lc709203f::get_register_(uint8_t register_to_read, uint16_t *register_value) { + i2c::ErrorCode return_code; + uint8_t read_buffer[6]; + + read_buffer[0] = (this->address_) << 1; + read_buffer[1] = register_to_read; + read_buffer[2] = ((this->address_) << 1) | 0x01; + + for (uint8_t i = 0; i <= LC709203F_I2C_RETRY_COUNT; i++) { + // Note: the read_register() function does not send a stop between the write and + // the read portions of the I2C transation when you set the last variable to 'false' + // as we do below. Some of the other I2C read functions such as the generic read() + // function will send a stop between the read and the write portion of the I2C + // transaction. This is bad in this case and will result in reading nothing but 0xFFFF + // from the registers. + return_code = this->read_register(register_to_read, &read_buffer[3], 3, false); + if (return_code != i2c::NO_ERROR) { + // Error on the i2c bus + this->status_set_warning( + str_sprintf("Error code %d when reading from register 0x%02X", return_code, register_to_read).c_str()); + } else if (this->crc8_(read_buffer, 5) != read_buffer[5]) { + // I2C indicated OK, but the CRC of the data does not matcth. + this->status_set_warning(str_sprintf("CRC error reading from register 0x%02X", register_to_read).c_str()); + } else { + *register_value = ((uint16_t) read_buffer[4] << 8) | (uint16_t) read_buffer[3]; + return i2c::NO_ERROR; + } + } + + // If we get here, we tried LC709203F_I2C_RETRY_COUNT times to read the register and + // failed each time. Set the register value to 0 and return the I2C error code or 0xFF + // to indicate a CRC failure. It will be up to the higher level code what to do when + // this happens. + *register_value = 0x0000; + if (return_code != i2c::NO_ERROR) { + return return_code; + } else { + return 0xFF; + } +} + +uint8_t Lc709203f::set_register_(uint8_t register_to_set, uint16_t value_to_set) { + i2c::ErrorCode return_code; + uint8_t write_buffer[5]; + + // Note: We don't actually send byte[0] of the buffer. We include it because it is + // part of the CRC calculation. + write_buffer[0] = (this->address_) << 1; + write_buffer[1] = register_to_set; + write_buffer[2] = value_to_set & 0xFF; // Low byte + write_buffer[3] = (value_to_set >> 8) & 0xFF; // High byte + write_buffer[4] = this->crc8_(write_buffer, 4); + + for (uint8_t i = 0; i <= LC709203F_I2C_RETRY_COUNT; i++) { + // Note: we don't write the first byte of the write buffer to the device. + // This is done automatically by the write() function. + return_code = this->write(&write_buffer[1], 4, true); + if (return_code == i2c::NO_ERROR) { + return return_code; + } else { + this->status_set_warning( + str_sprintf("Error code %d when writing to register 0x%02X", return_code, register_to_set).c_str()); + } + } + + // If we get here, we tried to send the data LC709203F_I2C_RETRY_COUNT times and failed. + // We return the I2C error code, it is up to the higher level code what to do about it. + return return_code; +} + +uint8_t Lc709203f::crc8_(uint8_t *byte_buffer, uint8_t length_of_crc) { + uint8_t crc = 0x00; + const uint8_t polynomial(0x07); + + for (uint8_t j = length_of_crc; j; --j) { + crc ^= *byte_buffer++; + + for (uint8_t i = 8; i; --i) { + crc = (crc & 0x80) ? (crc << 1) ^ polynomial : (crc << 1); + } + } + return crc; +} + +void Lc709203f::set_pack_size(uint16_t pack_size) { + static const uint16_t PACK_SIZE_ARRAY[6] = {100, 200, 500, 1000, 2000, 3000}; + static const uint16_t APA_ARRAY[6] = {0x08, 0x0B, 0x10, 0x19, 0x2D, 0x36}; + float slope; + float intercept; + + this->pack_size_ = pack_size; // Pack size in mAH + + // The size is used to calculate the 'Adjustment Pack Application' number. + // Here we assume a type 01 or type 03 battery and do a linear curve fit to find the APA. + for (uint8_t i = 0; i < 6; i++) { + if (PACK_SIZE_ARRAY[i] == pack_size) { + // If the pack size is exactly one of the values in the array. + this->apa_ = APA_ARRAY[i]; + return; + } else if ((i > 0) && (PACK_SIZE_ARRAY[i] > pack_size) && (PACK_SIZE_ARRAY[i - 1] < pack_size)) { + // If the pack size is between the current array element and the previous. Do a linear + // Curve fit to determine the APA value. + + // Type casting is required here to avoid interger division + slope = static_cast(APA_ARRAY[i] - APA_ARRAY[i - 1]) / + static_cast(PACK_SIZE_ARRAY[i] - PACK_SIZE_ARRAY[i - 1]); + + // Type casting might not be needed here. + intercept = static_cast(APA_ARRAY[i]) - slope * static_cast(PACK_SIZE_ARRAY[i]); + + this->apa_ = static_cast(slope * pack_size + intercept); + return; + } + } + // We should never get here. If we do, it means we never set the pack APA. This should + // not be possible because of the config validation. However, if it does happen, the + // consequence is that the RSOC values will likley not be as accurate. However, it should + // not cause an error or crash, so I am not doing any additional checking here. +} + +void Lc709203f::set_thermistor_b_constant(uint16_t b_constant) { this->b_constant_ = b_constant; } + +void Lc709203f::set_pack_voltage(LC709203FBatteryVoltage pack_voltage) { this->pack_voltage_ = pack_voltage; } + +} // namespace lc709203f +} // namespace esphome diff --git a/esphome/components/lc709203f/lc709203f.h b/esphome/components/lc709203f/lc709203f.h new file mode 100644 index 0000000000..3b5b04775f --- /dev/null +++ b/esphome/components/lc709203f/lc709203f.h @@ -0,0 +1,55 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace lc709203f { + +enum LC709203FState { + STATE_INIT, + STATE_RSOC, + STATE_TEMP_SETUP, + STATE_NORMAL, +}; + +/// Enum listing allowable voltage settings for the LC709203F. +enum LC709203FBatteryVoltage { + LC709203F_BATTERY_VOLTAGE_3_8 = 0x0000, + LC709203F_BATTERY_VOLTAGE_3_7 = 0x0001, +}; + +class Lc709203f : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + + void set_pack_size(uint16_t pack_size); + void set_thermistor_b_constant(uint16_t b_constant); + void set_pack_voltage(LC709203FBatteryVoltage pack_voltage); + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_battery_remaining_sensor(sensor::Sensor *battery_remaining_sensor) { + battery_remaining_sensor_ = battery_remaining_sensor; + } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + + private: + uint8_t get_register_(uint8_t register_to_read, uint16_t *register_value); + uint8_t set_register_(uint8_t register_to_set, uint16_t value_to_set); + uint8_t crc8_(uint8_t *byte_buffer, uint8_t length_of_crc); + + protected: + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *battery_remaining_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + uint16_t pack_size_; + uint16_t apa_; + uint16_t b_constant_; + LC709203FState state_ = STATE_INIT; + uint16_t pack_voltage_; +}; + +} // namespace lc709203f +} // namespace esphome diff --git a/esphome/components/lc709203f/sensor.py b/esphome/components/lc709203f/sensor.py new file mode 100644 index 0000000000..eb08a522e5 --- /dev/null +++ b/esphome/components/lc709203f/sensor.py @@ -0,0 +1,93 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_BATTERY_LEVEL, + CONF_BATTERY_VOLTAGE, + CONF_ID, + CONF_SIZE, + CONF_TEMPERATURE, + CONF_VOLTAGE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + UNIT_VOLT, +) + +DEPENDENCIES = ["i2c"] + +lc709203f_ns = cg.esphome_ns.namespace("lc709203f") + +CONF_B_CONSTANT = "b_constant" + +LC709203FBatteryVoltage = lc709203f_ns.enum("LC709203FBatteryVoltage") +BATTERY_VOLTAGE_OPTIONS = { + "3.7": LC709203FBatteryVoltage.LC709203F_BATTERY_VOLTAGE_3_7, + "3.8": LC709203FBatteryVoltage.LC709203F_BATTERY_VOLTAGE_3_8, +} + +lc709203f = lc709203f_ns.class_("Lc709203f", cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(lc709203f), + cv.Optional(CONF_SIZE, default="500"): cv.int_range(100, 3000), + cv.Optional(CONF_VOLTAGE, default="3.7"): cv.enum( + BATTERY_VOLTAGE_OPTIONS, upper=True + ), + cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ).extend( + { + cv.Required(CONF_B_CONSTANT): cv.int_range(0, 0xFFFF), + } + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x0B)) +) + + +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) + cg.add(var.set_pack_size(config.get(CONF_SIZE))) + cg.add(var.set_pack_voltage(BATTERY_VOLTAGE_OPTIONS[config[CONF_VOLTAGE]])) + + if voltage_config := config.get(CONF_BATTERY_VOLTAGE): + sens = await sensor.new_sensor(voltage_config) + cg.add(var.set_voltage_sensor(sens)) + + if level_config := config.get(CONF_BATTERY_LEVEL): + sens = await sensor.new_sensor(level_config) + cg.add(var.set_battery_remaining_sensor(sens)) + + if temp_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temp_config) + cg.add(var.set_temperature_sensor(sens)) + cg.add(var.set_thermistor_b_constant(temp_config[CONF_B_CONSTANT])) diff --git a/tests/components/lc709203f/common.yaml b/tests/components/lc709203f/common.yaml new file mode 100644 index 0000000000..53177c0d4a --- /dev/null +++ b/tests/components/lc709203f/common.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_lc709203f + scl: ${scl_pin} + sda: ${sda_pin} + +sensor: + - platform: lc709203f + size: 2000 + voltage: 3.7 + battery_voltage: + name: "Battery Voltage" + battery_level: + name: "Battery" + temperature: + name: "Pack Temperature" + b_constant: 0xA5A5 diff --git a/tests/components/lc709203f/test.esp32-ard.yaml b/tests/components/lc709203f/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/lc709203f/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/lc709203f/test.esp32-c3-ard.yaml b/tests/components/lc709203f/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/lc709203f/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/lc709203f/test.esp32-c3-idf.yaml b/tests/components/lc709203f/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/lc709203f/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/lc709203f/test.esp32-idf.yaml b/tests/components/lc709203f/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/lc709203f/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/lc709203f/test.esp8266-ard.yaml b/tests/components/lc709203f/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/lc709203f/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/lc709203f/test.rp2040-ard.yaml b/tests/components/lc709203f/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/lc709203f/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml