ESPHome 2026.5.0b1
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_, this->pack_voltage_ == 0x0000 ? "8" : "7");
154 LOG_I2C_DEVICE(this);
155 LOG_UPDATE_INTERVAL(this);
156 LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
157 LOG_SENSOR(" ", "Battery Remaining", this->battery_remaining_sensor_);
158
159 if (this->temperature_sensor_ != nullptr) {
160 LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
161 ESP_LOGCONFIG(TAG, " B_Constant: %d", this->b_constant_);
162 } else {
163 ESP_LOGCONFIG(TAG, " No Temperature Sensor");
164 }
165}
166
167uint8_t Lc709203f::get_register_(uint8_t register_to_read, uint16_t *register_value) {
168 i2c::ErrorCode return_code;
169 uint8_t read_buffer[6];
170
171 read_buffer[0] = (this->address_) << 1;
172 read_buffer[1] = register_to_read;
173 read_buffer[2] = ((this->address_) << 1) | 0x01;
174
175 for (uint8_t i = 0; i <= LC709203F_I2C_RETRY_COUNT; i++) {
176 // Note: the read_register() function does not send a stop between the write and
177 // the read portions of the I2C transation when you set the last variable to 'false'
178 // as we do below. Some of the other I2C read functions such as the generic read()
179 // function will send a stop between the read and the write portion of the I2C
180 // transaction. This is bad in this case and will result in reading nothing but 0xFFFF
181 // from the registers.
182 return_code = this->read_register(register_to_read, &read_buffer[3], 3);
183 if (return_code != i2c::NO_ERROR) {
184 // Error on the i2c bus
185 char buf[64];
186 snprintf(buf, sizeof(buf), "Error code %d when reading from register 0x%02X", return_code, register_to_read);
187 this->status_set_warning(buf);
188 } else if (crc8(read_buffer, 5, 0x00, 0x07, true) != read_buffer[5]) {
189 // I2C indicated OK, but the CRC of the data does not matcth.
190 char buf[64];
191 snprintf(buf, sizeof(buf), "CRC error reading from register 0x%02X", register_to_read);
192 this->status_set_warning(buf);
193 } else {
194 *register_value = ((uint16_t) read_buffer[4] << 8) | (uint16_t) read_buffer[3];
195 return i2c::NO_ERROR;
196 }
197 }
198
199 // If we get here, we tried LC709203F_I2C_RETRY_COUNT times to read the register and
200 // failed each time. Set the register value to 0 and return the I2C error code or 0xFF
201 // to indicate a CRC failure. It will be up to the higher level code what to do when
202 // this happens.
203 *register_value = 0x0000;
204 if (return_code != i2c::NO_ERROR) {
205 return return_code;
206 } else {
207 return 0xFF;
208 }
209}
210
211uint8_t Lc709203f::set_register_(uint8_t register_to_set, uint16_t value_to_set) {
212 i2c::ErrorCode return_code;
213 uint8_t write_buffer[5];
214
215 // Note: We don't actually send byte[0] of the buffer. We include it because it is
216 // part of the CRC calculation.
217 write_buffer[0] = (this->address_) << 1;
218 write_buffer[1] = register_to_set;
219 write_buffer[2] = value_to_set & 0xFF; // Low byte
220 write_buffer[3] = (value_to_set >> 8) & 0xFF; // High byte
221 write_buffer[4] = crc8(write_buffer, 4, 0x00, 0x07, true);
222
223 for (uint8_t i = 0; i <= LC709203F_I2C_RETRY_COUNT; i++) {
224 // Note: we don't write the first byte of the write buffer to the device.
225 // This is done automatically by the write() function.
226 return_code = this->write(&write_buffer[1], 4);
227 if (return_code == i2c::NO_ERROR) {
228 return return_code;
229 } else {
230 char buf[64];
231 snprintf(buf, sizeof(buf), "Error code %d when writing to register 0x%02X", return_code, register_to_set);
232 this->status_set_warning(buf);
233 }
234 }
235
236 // If we get here, we tried to send the data LC709203F_I2C_RETRY_COUNT times and failed.
237 // We return the I2C error code, it is up to the higher level code what to do about it.
238 return return_code;
239}
240
241void Lc709203f::set_pack_size(uint16_t pack_size) {
242 static const uint16_t PACK_SIZE_ARRAY[6] = {100, 200, 500, 1000, 2000, 3000};
243 static const uint16_t APA_ARRAY[6] = {0x08, 0x0B, 0x10, 0x19, 0x2D, 0x36};
244 float slope;
245 float intercept;
246
247 this->pack_size_ = pack_size; // Pack size in mAH
248
249 // The size is used to calculate the 'Adjustment Pack Application' number.
250 // Here we assume a type 01 or type 03 battery and do a linear curve fit to find the APA.
251 for (uint8_t i = 0; i < 6; i++) {
252 if (PACK_SIZE_ARRAY[i] == pack_size) {
253 // If the pack size is exactly one of the values in the array.
254 this->apa_ = APA_ARRAY[i];
255 return;
256 } else if ((i > 0) && (PACK_SIZE_ARRAY[i] > pack_size) && (PACK_SIZE_ARRAY[i - 1] < pack_size)) {
257 // If the pack size is between the current array element and the previous. Do a linear
258 // Curve fit to determine the APA value.
259
260 // Type casting is required here to avoid interger division
261 slope = static_cast<float>(APA_ARRAY[i] - APA_ARRAY[i - 1]) /
262 static_cast<float>(PACK_SIZE_ARRAY[i] - PACK_SIZE_ARRAY[i - 1]);
263
264 // Type casting might not be needed here.
265 intercept = static_cast<float>(APA_ARRAY[i]) - slope * static_cast<float>(PACK_SIZE_ARRAY[i]);
266
267 this->apa_ = static_cast<uint8_t>(slope * pack_size + intercept);
268 return;
269 }
270 }
271 // We should never get here. If we do, it means we never set the pack APA. This should
272 // not be possible because of the config validation. However, if it does happen, the
273 // consequence is that the RSOC values will likley not be as accurate. However, it should
274 // not cause an error or crash, so I am not doing any additional checking here.
275}
276
277void Lc709203f::set_thermistor_b_constant(uint16_t b_constant) { this->b_constant_ = b_constant; }
278
279void Lc709203f::set_pack_voltage(LC709203FBatteryVoltage pack_voltage) { this->pack_voltage_ = pack_voltage; }
280
281} // namespace esphome::lc709203f
void status_clear_warning()
Definition component.h:306
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