ESPHome 2025.6.2
Loading...
Searching...
No Matches
lc709203f.cpp
Go to the documentation of this file.
1#include "esphome/core/log.h"
2#include "lc709203f.h"
3
4namespace esphome {
5namespace lc709203f {
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 ESP_LOGCONFIG(TAG, "Running setup");
53
54 // Set power mode to on. Note that, unlike some other similar devices, in sleep mode the IC
55 // does not record power usage. If there is significant power consumption during sleep mode,
56 // the pack RSOC will likely no longer be correct. Because of that, I do not implement
57 // sleep mode on this device.
58
59 // Initialize device registers. If any of these fail, retry during the update() function.
60 if (this->set_register_(LC709203F_IC_POWER_MODE, LC709203F_POWER_MODE_ON) != i2c::NO_ERROR) {
61 return;
62 }
63
64 if (this->set_register_(LC709203F_APA, this->apa_) != i2c::NO_ERROR) {
65 return;
66 }
67
68 if (this->set_register_(LC709203F_CHANGE_OF_THE_PARAMETER, this->pack_voltage_) != i2c::NO_ERROR) {
69 return;
70 }
71
72 this->state_ = STATE_RSOC;
73 // Note: Initialization continues in the update() function.
74}
75
77 uint16_t buffer;
78
79 if (this->state_ == STATE_NORMAL) {
80 // Note: If we fail to read from the data registers, we do not report any sensor reading.
81 if (this->voltage_sensor_ != nullptr) {
82 if (this->get_register_(LC709203F_CELL_VOLTAGE, &buffer) == i2c::NO_ERROR) {
83 // Raw units are mV
84 this->voltage_sensor_->publish_state(static_cast<float>(buffer) / 1000.0f);
86 }
87 }
88 if (this->battery_remaining_sensor_ != nullptr) {
89 if (this->get_register_(LC709203F_ITE, &buffer) == i2c::NO_ERROR) {
90 // Raw units are .1%
91 this->battery_remaining_sensor_->publish_state(static_cast<float>(buffer) / 10.0f);
93 }
94 }
95 if (this->temperature_sensor_ != nullptr) {
96 // I can't test this with a real thermistor because I don't have a device with
97 // an attached thermistor. I have turned on the sensor and made sure that it
98 // sets up the registers properly.
99 if (this->get_register_(LC709203F_CELL_TEMPERATURE, &buffer) == i2c::NO_ERROR) {
100 // Raw units are .1 K
101 this->temperature_sensor_->publish_state((static_cast<float>(buffer) / 10.0f) - 273.15f);
102 this->status_clear_warning();
103 }
104 }
105 } else if (this->state_ == STATE_INIT) {
106 // Retry initializing the device registers. We should only get here if the init sequence
107 // failed during the setup() function. This would likely occur because of a repeated failures
108 // on the I2C bus. If any of these fail, retry the next time the update() function is called.
109 if (this->set_register_(LC709203F_IC_POWER_MODE, LC709203F_POWER_MODE_ON) != i2c::NO_ERROR) {
110 return;
111 }
112
113 if (this->set_register_(LC709203F_APA, this->apa_) != i2c::NO_ERROR) {
114 return;
115 }
116
117 if (this->set_register_(LC709203F_CHANGE_OF_THE_PARAMETER, this->pack_voltage_) != i2c::NO_ERROR) {
118 return;
119 }
120
121 this->state_ = STATE_RSOC;
122
123 } else if (this->state_ == STATE_RSOC) {
124 // We implement a delay here to send the initial RSOC command.
125 // This should run once on the first update() after initialization.
126 if (this->set_register_(LC709203F_INITIAL_RSOC, 0xAA55) == i2c::NO_ERROR) {
127 this->state_ = STATE_TEMP_SETUP;
128 }
129 } else if (this->state_ == STATE_TEMP_SETUP) {
130 // This should run once on the second update() after initialization.
131 if (this->temperature_sensor_ != nullptr) {
132 // This assumes that a thermistor is attached to the device as shown in the datahseet.
133 if (this->set_register_(LC709203F_STATUS_BIT, 0x0001) == i2c::NO_ERROR) {
134 if (this->set_register_(LC709203F_THERMISTOR_B, this->b_constant_) == i2c::NO_ERROR) {
135 this->state_ = STATE_NORMAL;
136 }
137 }
138 } else if (this->set_register_(LC709203F_STATUS_BIT, 0x0000) == i2c::NO_ERROR) {
139 // The device expects to get updates to the temperature in this mode.
140 // I am not doing that now. The temperature register defaults to 25C.
141 // In theory, we could have another temperature sensor and have ESPHome
142 // send updated temperature to the device occasionally, but I have no idea
143 // how to make that happen.
144 this->state_ = STATE_NORMAL;
145 }
146 }
147}
148
150 ESP_LOGCONFIG(TAG, "LC709203F:");
151 LOG_I2C_DEVICE(this);
152
153 LOG_UPDATE_INTERVAL(this);
154 ESP_LOGCONFIG(TAG,
155 " Pack Size: %d mAH\n"
156 " Pack APA: 0x%02X",
157 this->pack_size_, this->apa_);
158
159 // This is only true if the pack_voltage_ is either 0x0000 or 0x0001. The config validator
160 // should have already verified this.
161 ESP_LOGCONFIG(TAG, " Pack Rated Voltage: 3.%sV", this->pack_voltage_ == 0x0000 ? "8" : "7");
162
163 LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
164 LOG_SENSOR(" ", "Battery Remaining", this->battery_remaining_sensor_);
165
166 if (this->temperature_sensor_ != nullptr) {
167 LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
168 ESP_LOGCONFIG(TAG, " B_Constant: %d", this->b_constant_);
169 } else {
170 ESP_LOGCONFIG(TAG, " No Temperature Sensor");
171 }
172}
173
174uint8_t Lc709203f::get_register_(uint8_t register_to_read, uint16_t *register_value) {
175 i2c::ErrorCode return_code;
176 uint8_t read_buffer[6];
177
178 read_buffer[0] = (this->address_) << 1;
179 read_buffer[1] = register_to_read;
180 read_buffer[2] = ((this->address_) << 1) | 0x01;
181
182 for (uint8_t i = 0; i <= LC709203F_I2C_RETRY_COUNT; i++) {
183 // Note: the read_register() function does not send a stop between the write and
184 // the read portions of the I2C transation when you set the last variable to 'false'
185 // as we do below. Some of the other I2C read functions such as the generic read()
186 // function will send a stop between the read and the write portion of the I2C
187 // transaction. This is bad in this case and will result in reading nothing but 0xFFFF
188 // from the registers.
189 return_code = this->read_register(register_to_read, &read_buffer[3], 3, false);
190 if (return_code != i2c::NO_ERROR) {
191 // Error on the i2c bus
192 this->status_set_warning(
193 str_sprintf("Error code %d when reading from register 0x%02X", return_code, register_to_read).c_str());
194 } else if (this->crc8_(read_buffer, 5) != read_buffer[5]) {
195 // I2C indicated OK, but the CRC of the data does not matcth.
196 this->status_set_warning(str_sprintf("CRC error reading from register 0x%02X", register_to_read).c_str());
197 } else {
198 *register_value = ((uint16_t) read_buffer[4] << 8) | (uint16_t) read_buffer[3];
199 return i2c::NO_ERROR;
200 }
201 }
202
203 // If we get here, we tried LC709203F_I2C_RETRY_COUNT times to read the register and
204 // failed each time. Set the register value to 0 and return the I2C error code or 0xFF
205 // to indicate a CRC failure. It will be up to the higher level code what to do when
206 // this happens.
207 *register_value = 0x0000;
208 if (return_code != i2c::NO_ERROR) {
209 return return_code;
210 } else {
211 return 0xFF;
212 }
213}
214
215uint8_t Lc709203f::set_register_(uint8_t register_to_set, uint16_t value_to_set) {
216 i2c::ErrorCode return_code;
217 uint8_t write_buffer[5];
218
219 // Note: We don't actually send byte[0] of the buffer. We include it because it is
220 // part of the CRC calculation.
221 write_buffer[0] = (this->address_) << 1;
222 write_buffer[1] = register_to_set;
223 write_buffer[2] = value_to_set & 0xFF; // Low byte
224 write_buffer[3] = (value_to_set >> 8) & 0xFF; // High byte
225 write_buffer[4] = this->crc8_(write_buffer, 4);
226
227 for (uint8_t i = 0; i <= LC709203F_I2C_RETRY_COUNT; i++) {
228 // Note: we don't write the first byte of the write buffer to the device.
229 // This is done automatically by the write() function.
230 return_code = this->write(&write_buffer[1], 4, true);
231 if (return_code == i2c::NO_ERROR) {
232 return return_code;
233 } else {
234 this->status_set_warning(
235 str_sprintf("Error code %d when writing to register 0x%02X", return_code, register_to_set).c_str());
236 }
237 }
238
239 // If we get here, we tried to send the data LC709203F_I2C_RETRY_COUNT times and failed.
240 // We return the I2C error code, it is up to the higher level code what to do about it.
241 return return_code;
242}
243
244uint8_t Lc709203f::crc8_(uint8_t *byte_buffer, uint8_t length_of_crc) {
245 uint8_t crc = 0x00;
246 const uint8_t polynomial(0x07);
247
248 for (uint8_t j = length_of_crc; j; --j) {
249 crc ^= *byte_buffer++;
250
251 for (uint8_t i = 8; i; --i) {
252 crc = (crc & 0x80) ? (crc << 1) ^ polynomial : (crc << 1);
253 }
254 }
255 return crc;
256}
257
258void Lc709203f::set_pack_size(uint16_t pack_size) {
259 static const uint16_t PACK_SIZE_ARRAY[6] = {100, 200, 500, 1000, 2000, 3000};
260 static const uint16_t APA_ARRAY[6] = {0x08, 0x0B, 0x10, 0x19, 0x2D, 0x36};
261 float slope;
262 float intercept;
263
264 this->pack_size_ = pack_size; // Pack size in mAH
265
266 // The size is used to calculate the 'Adjustment Pack Application' number.
267 // Here we assume a type 01 or type 03 battery and do a linear curve fit to find the APA.
268 for (uint8_t i = 0; i < 6; i++) {
269 if (PACK_SIZE_ARRAY[i] == pack_size) {
270 // If the pack size is exactly one of the values in the array.
271 this->apa_ = APA_ARRAY[i];
272 return;
273 } else if ((i > 0) && (PACK_SIZE_ARRAY[i] > pack_size) && (PACK_SIZE_ARRAY[i - 1] < pack_size)) {
274 // If the pack size is between the current array element and the previous. Do a linear
275 // Curve fit to determine the APA value.
276
277 // Type casting is required here to avoid interger division
278 slope = static_cast<float>(APA_ARRAY[i] - APA_ARRAY[i - 1]) /
279 static_cast<float>(PACK_SIZE_ARRAY[i] - PACK_SIZE_ARRAY[i - 1]);
280
281 // Type casting might not be needed here.
282 intercept = static_cast<float>(APA_ARRAY[i]) - slope * static_cast<float>(PACK_SIZE_ARRAY[i]);
283
284 this->apa_ = static_cast<uint8_t>(slope * pack_size + intercept);
285 return;
286 }
287 }
288 // We should never get here. If we do, it means we never set the pack APA. This should
289 // not be possible because of the config validation. However, if it does happen, the
290 // consequence is that the RSOC values will likley not be as accurate. However, it should
291 // not cause an error or crash, so I am not doing any additional checking here.
292}
293
294void Lc709203f::set_thermistor_b_constant(uint16_t b_constant) { this->b_constant_ = b_constant; }
295
296void Lc709203f::set_pack_voltage(LC709203FBatteryVoltage pack_voltage) { this->pack_voltage_ = pack_voltage; }
297
298} // namespace lc709203f
299} // namespace esphome
void status_set_warning(const char *message="unspecified")
void status_clear_warning()
ErrorCode write(const uint8_t *data, size_t len, bool stop=true)
writes an array of bytes to a device using an I2CBus
Definition i2c.h:190
uint8_t address_
store the address of the device on the bus
Definition i2c.h:273
ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop=true)
reads an array of bytes from a specific register in the I²C device
Definition i2c.cpp:10
sensor::Sensor * voltage_sensor_
Definition lc709203f.h:44
void set_pack_voltage(LC709203FBatteryVoltage pack_voltage)
void set_thermistor_b_constant(uint16_t b_constant)
sensor::Sensor * battery_remaining_sensor_
Definition lc709203f.h:45
void set_pack_size(uint16_t pack_size)
sensor::Sensor * temperature_sensor_
Definition lc709203f.h:46
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:39
ErrorCode
Error codes returned by I2CBus and I2CDevice methods.
Definition i2c_bus.h:11
@ NO_ERROR
No error found during execution of method.
Definition i2c_bus.h:12
LC709203FBatteryVoltage
Enum listing allowable voltage settings for the LC709203F.
Definition lc709203f.h:18
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
std::string str_sprintf(const char *fmt,...)
Definition helpers.cpp:323