ESPHome 2026.10.0-dev
Loading...
Searching...
No Matches
lc709203f.cpp
Go to the documentation of this file.
1#include "lc709203f.h"
3#include "esphome/core/log.h"
4
6
7static const char *const TAG = "lc709203f.sensor";
8
9// Device I2C address. This address is fixed.
10static const uint8_t LC709203F_I2C_ADDR_DEFAULT = 0x0B;
11
12// Device registers
13static const uint8_t LC709203F_BEFORE_RSOC = 0x04;
14static const uint8_t LC709203F_THERMISTOR_B = 0x06;
15static const uint8_t LC709203F_INITIAL_RSOC = 0x07;
16static const uint8_t LC709203F_CELL_TEMPERATURE = 0x08;
17static const uint8_t LC709203F_CELL_VOLTAGE = 0x09;
18static const uint8_t LC709203F_CURRENT_DIRECTION = 0x0A;
19static const uint8_t LC709203F_APA = 0x0B;
20static const uint8_t LC709203F_APT = 0x0C;
21static const uint8_t LC709203F_RSOC = 0x0D;
22static const uint8_t LC709203F_ITE = 0x0F;
23static const uint8_t LC709203F_IC_VERSION = 0x11;
24static const uint8_t LC709203F_CHANGE_OF_THE_PARAMETER = 0x12;
25static const uint8_t LC709203F_ALARM_LOW_RSOC = 0x13;
26static const uint8_t LC709203F_ALARM_LOW_CELL_VOLTAGE = 0x14;
27static const uint8_t LC709203F_IC_POWER_MODE = 0x15;
28static const uint8_t LC709203F_STATUS_BIT = 0x16;
29static const uint8_t LC709203F_NUMBER_OF_THE_PARAMETER = 0x1A;
30
31static const uint8_t LC709203F_POWER_MODE_ON = 0x0001;
32static const uint8_t LC709203F_POWER_MODE_SLEEP = 0x0002;
33
34// The number of times to retry an I2C transaction before giving up. In my experience,
35// 10 is a good number here that will take care of most bus issues that require retry.
36static const uint8_t LC709203F_I2C_RETRY_COUNT = 10;
37
39 // Note: The setup implements a small state machine. This is because we want to have
40 // delays before and after sending the RSOC command. The full init process should be:
41 // INIT->RSOC->TEMP_SETUP->NORMAL
42 // The setup() function will only perform the first part of the initialization process.
43 // Assuming no errors, the whole process should occur during the setup() function and
44 // the first two calls to update(). After that, the part should remain in normal mode
45 // until a device reset.
46 //
47 // This device can be picky about I2C communication and can error out occasionally. The
48 // get/set register functions impelment retry logic to retry the I2C transactions. The
49 // initialization code checks the return code from those functions. If they don't return
50 // NO_ERROR (0x00), that part of the initialization aborts and will be retried on the next
51 // call to update().
52 // Set power mode to on. Note that, unlike some other similar devices, in sleep mode the IC
53 // does not record power usage. If there is significant power consumption during sleep mode,
54 // the pack RSOC will likely no longer be correct. Because of that, I do not implement
55 // sleep mode on this device.
56
57 // Initialize device registers. If any of these fail, retry during the update() function.
58 if (this->set_register_(LC709203F_IC_POWER_MODE, LC709203F_POWER_MODE_ON) != i2c::NO_ERROR) {
59 return;
60 }
61
62 if (this->set_register_(LC709203F_APA, this->apa_) != i2c::NO_ERROR) {
63 return;
64 }
65
66 if (this->set_register_(LC709203F_CHANGE_OF_THE_PARAMETER, this->pack_voltage_) != i2c::NO_ERROR) {
67 return;
68 }
69
70 this->state_ = STATE_RSOC;
71 // Note: Initialization continues in the update() function.
72}
73
75 uint16_t buffer;
76
77 if (this->state_ == STATE_NORMAL) {
78 // Note: If we fail to read from the data registers, we do not report any sensor reading.
79 if (this->voltage_sensor_ != nullptr) {
80 if (this->get_register_(LC709203F_CELL_VOLTAGE, &buffer) == i2c::NO_ERROR) {
81 // Raw units are mV
82 this->voltage_sensor_->publish_state(static_cast<float>(buffer) / 1000.0f);
84 }
85 }
86 if (this->battery_remaining_sensor_ != nullptr) {
87 if (this->get_register_(LC709203F_ITE, &buffer) == i2c::NO_ERROR) {
88 // Raw units are .1%
89 this->battery_remaining_sensor_->publish_state(static_cast<float>(buffer) / 10.0f);
91 }
92 }
93 if (this->temperature_sensor_ != nullptr) {
94 // I can't test this with a real thermistor because I don't have a device with
95 // an attached thermistor. I have turned on the sensor and made sure that it
96 // sets up the registers properly.
97 if (this->get_register_(LC709203F_CELL_TEMPERATURE, &buffer) == i2c::NO_ERROR) {
98 // Raw units are .1 K
99 this->temperature_sensor_->publish_state((static_cast<float>(buffer) / 10.0f) - 273.15f);
100 this->status_clear_warning();
101 }
102 }
103 } else if (this->state_ == STATE_INIT) {
104 // Retry initializing the device registers. We should only get here if the init sequence
105 // failed during the setup() function. This would likely occur because of a repeated failures
106 // on the I2C bus. If any of these fail, retry the next time the update() function is called.
107 if (this->set_register_(LC709203F_IC_POWER_MODE, LC709203F_POWER_MODE_ON) != i2c::NO_ERROR) {
108 return;
109 }
110
111 if (this->set_register_(LC709203F_APA, this->apa_) != i2c::NO_ERROR) {
112 return;
113 }
114
115 if (this->set_register_(LC709203F_CHANGE_OF_THE_PARAMETER, this->pack_voltage_) != i2c::NO_ERROR) {
116 return;
117 }
118
119 this->state_ = STATE_RSOC;
120
121 } else if (this->state_ == STATE_RSOC) {
122 // We implement a delay here to send the initial RSOC command.
123 // This should run once on the first update() after initialization.
124 if (this->set_register_(LC709203F_INITIAL_RSOC, 0xAA55) == i2c::NO_ERROR) {
125 this->state_ = STATE_TEMP_SETUP;
126 }
127 } else if (this->state_ == STATE_TEMP_SETUP) {
128 // This should run once on the second update() after initialization.
129 if (this->temperature_sensor_ != nullptr) {
130 // This assumes that a thermistor is attached to the device as shown in the datahseet.
131 if (this->set_register_(LC709203F_STATUS_BIT, 0x0001) == i2c::NO_ERROR) {
132 if (this->set_register_(LC709203F_THERMISTOR_B, this->b_constant_) == i2c::NO_ERROR) {
133 this->state_ = STATE_NORMAL;
134 }
135 }
136 } else if (this->set_register_(LC709203F_STATUS_BIT, 0x0000) == i2c::NO_ERROR) {
137 // The device expects to get updates to the temperature in this mode.
138 // I am not doing that now. The temperature register defaults to 25C.
139 // In theory, we could have another temperature sensor and have ESPHome
140 // send updated temperature to the device occasionally, but I have no idea
141 // how to make that happen.
142 this->state_ = STATE_NORMAL;
143 }
144 }
145}
146
148 ESP_LOGCONFIG(TAG,
149 "LC709203F:\n"
150 " Pack Size: %d mAH\n"
151 " Pack APA: 0x%02X\n"
152 " Pack Rated Voltage: 3.%sV",
153 this->pack_size_, this->apa_,
154 this->pack_voltage_ == 0x0000 ? LOG_STR_LITERAL("8") : LOG_STR_LITERAL("7"));
155 LOG_I2C_DEVICE(this);
156 LOG_UPDATE_INTERVAL(this);
157 LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
158 LOG_SENSOR(" ", "Battery Remaining", this->battery_remaining_sensor_);
159
160 if (this->temperature_sensor_ != nullptr) {
161 LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
162 ESP_LOGCONFIG(TAG, " B_Constant: %d", this->b_constant_);
163 } else {
164 ESP_LOGCONFIG(TAG, " No Temperature Sensor");
165 }
166}
167
168uint8_t Lc709203f::get_register_(uint8_t register_to_read, uint16_t *register_value) {
169 i2c::ErrorCode return_code;
170 uint8_t read_buffer[6];
171
172 read_buffer[0] = (this->address_) << 1;
173 read_buffer[1] = register_to_read;
174 read_buffer[2] = ((this->address_) << 1) | 0x01;
175
176 for (uint8_t i = 0; i <= LC709203F_I2C_RETRY_COUNT; i++) {
177 // Note: the read_register() function does not send a stop between the write and
178 // the read portions of the I2C transation when you set the last variable to 'false'
179 // as we do below. Some of the other I2C read functions such as the generic read()
180 // function will send a stop between the read and the write portion of the I2C
181 // transaction. This is bad in this case and will result in reading nothing but 0xFFFF
182 // from the registers.
183 return_code = this->read_register(register_to_read, &read_buffer[3], 3);
184 if (return_code != i2c::NO_ERROR) {
185 // Error on the i2c bus
186 char buf[64];
187 snprintf(buf, sizeof(buf), "Error code %d when reading from register 0x%02X", return_code, register_to_read);
188 this->status_set_warning(buf);
189 } else if (crc8(read_buffer, 5, 0x00, 0x07, true) != read_buffer[5]) {
190 // I2C indicated OK, but the CRC of the data does not matcth.
191 char buf[64];
192 snprintf(buf, sizeof(buf), "CRC error reading from register 0x%02X", register_to_read);
193 this->status_set_warning(buf);
194 } else {
195 *register_value = ((uint16_t) read_buffer[4] << 8) | (uint16_t) read_buffer[3];
196 return i2c::NO_ERROR;
197 }
198 }
199
200 // If we get here, we tried LC709203F_I2C_RETRY_COUNT times to read the register and
201 // failed each time. Set the register value to 0 and return the I2C error code or 0xFF
202 // to indicate a CRC failure. It will be up to the higher level code what to do when
203 // this happens.
204 *register_value = 0x0000;
205 if (return_code != i2c::NO_ERROR) {
206 return return_code;
207 } else {
208 return 0xFF;
209 }
210}
211
212uint8_t Lc709203f::set_register_(uint8_t register_to_set, uint16_t value_to_set) {
213 i2c::ErrorCode return_code;
214 uint8_t write_buffer[5];
215
216 // Note: We don't actually send byte[0] of the buffer. We include it because it is
217 // part of the CRC calculation.
218 write_buffer[0] = (this->address_) << 1;
219 write_buffer[1] = register_to_set;
220 write_buffer[2] = value_to_set & 0xFF; // Low byte
221 write_buffer[3] = (value_to_set >> 8) & 0xFF; // High byte
222 write_buffer[4] = crc8(write_buffer, 4, 0x00, 0x07, true);
223
224 for (uint8_t i = 0; i <= LC709203F_I2C_RETRY_COUNT; i++) {
225 // Note: we don't write the first byte of the write buffer to the device.
226 // This is done automatically by the write() function.
227 return_code = this->write(&write_buffer[1], 4);
228 if (return_code == i2c::NO_ERROR) {
229 return return_code;
230 } else {
231 char buf[64];
232 snprintf(buf, sizeof(buf), "Error code %d when writing to register 0x%02X", return_code, register_to_set);
233 this->status_set_warning(buf);
234 }
235 }
236
237 // If we get here, we tried to send the data LC709203F_I2C_RETRY_COUNT times and failed.
238 // We return the I2C error code, it is up to the higher level code what to do about it.
239 return return_code;
240}
241
242void Lc709203f::set_pack_size(uint16_t pack_size) {
243 static const uint16_t PACK_SIZE_ARRAY[6] = {100, 200, 500, 1000, 2000, 3000};
244 static const uint16_t APA_ARRAY[6] = {0x08, 0x0B, 0x10, 0x19, 0x2D, 0x36};
245 float slope;
246 float intercept;
247
248 this->pack_size_ = pack_size; // Pack size in mAH
249
250 // The size is used to calculate the 'Adjustment Pack Application' number.
251 // Here we assume a type 01 or type 03 battery and do a linear curve fit to find the APA.
252 for (uint8_t i = 0; i < 6; i++) {
253 if (PACK_SIZE_ARRAY[i] == pack_size) {
254 // If the pack size is exactly one of the values in the array.
255 this->apa_ = APA_ARRAY[i];
256 return;
257 } else if ((i > 0) && (PACK_SIZE_ARRAY[i] > pack_size) && (PACK_SIZE_ARRAY[i - 1] < pack_size)) {
258 // If the pack size is between the current array element and the previous. Do a linear
259 // Curve fit to determine the APA value.
260
261 // Type casting is required here to avoid interger division
262 slope = static_cast<float>(APA_ARRAY[i] - APA_ARRAY[i - 1]) /
263 static_cast<float>(PACK_SIZE_ARRAY[i] - PACK_SIZE_ARRAY[i - 1]);
264
265 // Type casting might not be needed here.
266 intercept = static_cast<float>(APA_ARRAY[i]) - slope * static_cast<float>(PACK_SIZE_ARRAY[i]);
267
268 this->apa_ = static_cast<uint8_t>(slope * pack_size + intercept);
269 return;
270 }
271 }
272 // We should never get here. If we do, it means we never set the pack APA. This should
273 // not be possible because of the config validation. However, if it does happen, the
274 // consequence is that the RSOC values will likley not be as accurate. However, it should
275 // not cause an error or crash, so I am not doing any additional checking here.
276}
277
278void Lc709203f::set_thermistor_b_constant(uint16_t b_constant) { this->b_constant_ = b_constant; }
279
280void Lc709203f::set_pack_voltage(LC709203FBatteryVoltage pack_voltage) { this->pack_voltage_ = pack_voltage; }
281
282} // namespace esphome::lc709203f
void status_clear_warning()
Definition component.h:289
ErrorCode write(const uint8_t *data, size_t len) const
writes an array of bytes to a device using an I2CBus
Definition i2c.h:183
uint8_t address_
store the address of the device on the bus
Definition i2c.h:270
ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len)
reads an array of bytes from a specific register in the I²C device
Definition i2c.cpp:25
sensor::Sensor * voltage_sensor_
Definition lc709203f.h:42
void set_pack_voltage(LC709203FBatteryVoltage pack_voltage)
void set_thermistor_b_constant(uint16_t b_constant)
sensor::Sensor * battery_remaining_sensor_
Definition lc709203f.h:43
void set_pack_size(uint16_t pack_size)
sensor::Sensor * temperature_sensor_
Definition lc709203f.h:44
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:68
ErrorCode
Error codes returned by I2CBus and I2CDevice methods.
Definition i2c_bus.h:12
@ NO_ERROR
No error found during execution of method.
Definition i2c_bus.h:13
LC709203FBatteryVoltage
Enum listing allowable voltage settings for the LC709203F.
Definition lc709203f.h:17
uint8_t crc8(const uint8_t *data, uint8_t len, uint8_t crc, uint8_t poly, bool msb_first)
Calculate a CRC-8 checksum of data with size len.
Definition helpers.cpp:59