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