ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
cse7766.cpp
Go to the documentation of this file.
1#include "cse7766.h"
4#include "esphome/core/log.h"
5#include <cmath>
6
7namespace esphome::cse7766 {
8
9ESPHOME_LOG_TAG(TAG, "cse7766");
10
11// The chip only sends the time since the last pulse once a cycle is longer than 1 s (in us)
12static constexpr uint32_t MIN_HELD_POWER_CYCLE = 1000000;
13
16 if (now - this->last_transmission_ >= 500) {
17 // last transmission too long ago. Reset RX index.
18 this->raw_data_index_ = 0;
19 }
20
21 // Early return prevents updating last_transmission_ when no data is available.
22 size_t avail = this->available();
23 if (avail == 0) {
24 return;
25 }
26
27 this->last_transmission_ = now;
28
29 // Read all available bytes in batches to reduce UART call overhead.
30 // At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call.
31 uint8_t buf[CSE7766_RAW_DATA_SIZE];
32 while (avail > 0) {
33 size_t to_read = std::min(avail, sizeof(buf));
34 if (!this->read_array(buf, to_read)) {
35 break;
36 }
37 avail -= to_read;
38
39 for (size_t i = 0; i < to_read; i++) {
40 this->raw_data_[this->raw_data_index_] = buf[i];
41 if (!this->check_byte_()) {
42 this->raw_data_index_ = 0;
43 this->status_set_warning();
44 continue;
45 }
46
47 if (this->raw_data_index_ == CSE7766_RAW_DATA_SIZE - 1) {
48 this->parse_data_();
50 }
51
52 this->raw_data_index_ = (this->raw_data_index_ + 1) % CSE7766_RAW_DATA_SIZE;
53 }
54 }
55}
56
58 uint8_t index = this->raw_data_index_;
59 uint8_t byte = this->raw_data_[index];
60 if (index == 0) {
61 return (byte == 0x55) || ((byte & 0xF0) == 0xF0) || (byte == 0xAA);
62 }
63
64 if (index == 1) {
65 if (byte != 0x5A) {
66 ESP_LOGV(TAG, "Invalid Header 2 Start: 0x%02X!", byte);
67 return false;
68 }
69 return true;
70 }
71
72 if (index == CSE7766_RAW_DATA_SIZE - 1) {
73 const uint8_t checksum = sum_bytes(this->raw_data_ + 2, CSE7766_RAW_DATA_SIZE - 3);
74
75 if (checksum != this->raw_data_[CSE7766_RAW_DATA_SIZE - 1]) {
76 ESP_LOGW(TAG, "Invalid checksum from CSE7766: 0x%02X != 0x%02X", checksum,
77 this->raw_data_[CSE7766_RAW_DATA_SIZE - 1]);
78 return false;
79 }
80 return true;
81 }
82
83 return true;
84}
86#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
87 {
88 char hex_buf[format_hex_pretty_size(CSE7766_RAW_DATA_SIZE)];
89 ESP_LOGVV(TAG, "Raw data: %s", format_hex_pretty_to(hex_buf, this->raw_data_, sizeof(this->raw_data_)));
90 }
91#endif
92
93 // Parse header
94 uint8_t header1 = this->raw_data_[0];
95
96 if (header1 == 0xAA) {
97 ESP_LOGE(TAG, "CSE7766 not calibrated!");
98 return;
99 }
100
101 bool power_cycle_exceeds_range = false;
102 if ((header1 & 0xF0) == 0xF0) {
103 if (header1 & 0xD) {
104 ESP_LOGE(TAG, "CSE7766 reports abnormal external circuit or chip damage: (0x%02X)", header1);
105 if (header1 & (1 << 3)) {
106 ESP_LOGE(TAG, " Voltage cycle exceeds range.");
107 }
108 if (header1 & (1 << 2)) {
109 ESP_LOGE(TAG, " Current cycle exceeds range.");
110 }
111 if (header1 & (1 << 0)) {
112 ESP_LOGE(TAG, " Coefficient storage area is abnormal.");
113 }
114
115 // Datasheet: voltage or current cycle exceeding range means invalid values
116 return;
117 }
118
119 power_cycle_exceeds_range = header1 & (1 << 1);
120 }
121
122 // Parse data frame
123 uint32_t voltage_coeff = this->get_24_bit_uint_(2);
124 uint32_t voltage_cycle = this->get_24_bit_uint_(5);
125 uint32_t current_coeff = this->get_24_bit_uint_(8);
126 uint32_t current_cycle = this->get_24_bit_uint_(11);
127 uint32_t power_coeff = this->get_24_bit_uint_(14);
128 uint32_t power_cycle = this->get_24_bit_uint_(17);
129 uint8_t adj = this->raw_data_[20];
130 uint16_t cf_pulses = (this->raw_data_[21] << 8) + this->raw_data_[22];
131
132 bool have_power = adj & 0x10;
133 bool have_current = adj & 0x20;
134 bool have_voltage = adj & 0x40;
135
136 float voltage = 0.0f;
137 if (have_voltage) {
138 voltage = voltage_coeff / float(voltage_cycle);
139 if (this->voltage_sensor_ != nullptr) {
140 this->voltage_sensor_->publish_state(voltage);
141 }
142 }
143
144 float energy = 0.0;
145 if (this->energy_sensor_ != nullptr) {
146 if (this->cf_pulses_last_ == 0 && !this->energy_sensor_->has_state()) {
147 this->cf_pulses_last_ = cf_pulses;
148 }
149 uint16_t cf_diff = cf_pulses - this->cf_pulses_last_;
150 this->cf_pulses_total_ += cf_diff;
151 this->cf_pulses_last_ = cf_pulses;
152 energy = this->cf_pulses_total_ * float(power_coeff) / 1000000.0f / 3600.0f;
153 this->energy_sensor_->publish_state(energy);
154 }
155
156 float power = 0.0f;
157 if (power_cycle_exceeds_range) {
158 // Datasheet: power cycle exceeding range means active power is 0
159 have_power = true;
160 this->last_power_cycle_ = 0;
161 if (this->power_sensor_ != nullptr) {
162 this->power_sensor_->publish_state(0.0f);
163 }
164 } else if (have_power) {
165 power = power_coeff / float(power_cycle);
166 this->last_power_cycle_ = power_cycle;
167 if (this->power_sensor_ != nullptr) {
168 this->power_sensor_->publish_state(power);
169 }
170 } else if (this->last_power_cycle_ > MIN_HELD_POWER_CYCLE && power_cycle <= 2 * this->last_power_cycle_) {
171 // No new measurement in this frame. With cycles over 1 s the register holds the last
172 // cycle or the time elapsed since the last pulse. While that time is within twice the
173 // last cycle, the next pulse is not overdue, so the last measurement still applies.
174 // Shorter cycles never get here in normal operation and keep the previous behaviour.
175 have_power = true;
176 power = power_coeff / float(this->last_power_cycle_);
177 if (this->power_sensor_ != nullptr) {
178 this->power_sensor_->publish_state(power);
179 }
180 } else {
181 // The next pulse is overdue: the load has dropped, report 0 W. A load that drops to less
182 // than half also reads 0 W here until its first new pulse arrives.
183 this->last_power_cycle_ = 0;
184 if (this->power_sensor_ != nullptr) {
185 this->power_sensor_->publish_state(0.0f);
186 }
187 }
188
189 float current = 0.0f;
190 float calculated_current = 0.0f;
191 if (have_current) {
192 // Assumption: if we don't have power measurement, then current is likely below 50mA
193 if (have_power && voltage > 1.0f) {
194 calculated_current = power / voltage;
195 }
196 // Datasheet: minimum measured current is 50mA
197 if (calculated_current > 0.05f) {
198 current = current_coeff / float(current_cycle);
199 }
200 if (this->current_sensor_ != nullptr) {
201 this->current_sensor_->publish_state(current);
202 }
203 }
204
205 if (have_voltage && have_current) {
206 const float apparent_power = voltage * current;
207 if (this->apparent_power_sensor_ != nullptr) {
208 this->apparent_power_sensor_->publish_state(apparent_power);
209 }
210 if (have_power && this->reactive_power_sensor_ != nullptr) {
211 const float q_squared = apparent_power * apparent_power - power * power;
212 if (q_squared < 0.0f) {
213 ESP_LOGD(TAG, "Impossible reactive power: S^2-P^2 is negative (%.4f)", q_squared);
215 } else {
216 this->reactive_power_sensor_->publish_state(std::sqrt(q_squared));
217 }
218 }
219 if (this->power_factor_sensor_ != nullptr && (have_power || power_cycle_exceeds_range)) {
220 float pf = NAN;
221 if (apparent_power > 0) {
222 pf = power / apparent_power;
223 if (pf > 1) {
224 // Noise near the chip's resolution limit, seen during inrush, pushes the ratio past unity
225 ESP_LOGD(TAG, "Power factor %.4f above 1, clamped", pf);
226 pf = 1.0f;
227 }
228 } else if (apparent_power == 0 && power == 0) {
229 // No load, report ideal power factor
230 pf = 1.0f;
231 } else if (current == 0 && calculated_current <= 0.05f) {
232 // Datasheet: minimum measured current is 50mA
233 ESP_LOGV(TAG, "Can't calculate power factor (current below minimum for CSE7766)");
234 } else {
235 ESP_LOGW(TAG, "Can't calculate power factor from P = %.4f W, S = %.4f VA", power, apparent_power);
236 }
238 }
239 }
240
241#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
242 {
243 // Buffer: 7 + 15 + 33 + 15 + 25 = 95 chars max + null, rounded to 128 for safety margin.
244 // Float sizes with %.4f can be up to 11 chars for large values (e.g., 999999.9999).
245 char buf[128];
246 size_t pos = buf_append_printf(buf, sizeof(buf), 0, "Parsed:");
247 if (have_voltage) {
248 pos = buf_append_printf(buf, sizeof(buf), pos, " V=%.4fV", voltage);
249 }
250 if (have_current) {
251 pos = buf_append_printf(buf, sizeof(buf), pos, " I=%.4fmA (~%.4fmA)", current * 1000.0f,
252 calculated_current * 1000.0f);
253 }
254 if (have_power) {
255 pos = buf_append_printf(buf, sizeof(buf), pos, " P=%.4fW", power);
256 }
257 if (energy != 0.0f) {
258 buf_append_printf(buf, sizeof(buf), pos, " E=%.4fkWh (%u)", energy, cf_pulses);
259 }
260 ESP_LOGVV(TAG, "%s", buf);
261 }
262#endif
263}
264
266 ESP_LOGCONFIG(TAG, "CSE7766:");
267 LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
268 LOG_SENSOR(" ", "Current", this->current_sensor_);
269 LOG_SENSOR(" ", "Power", this->power_sensor_);
270 LOG_SENSOR(" ", "Energy", this->energy_sensor_);
271 LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_);
272 LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_);
273 LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_);
274}
275
276} // namespace esphome::cse7766
uint8_t checksum
Definition bl0906.h:3
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void status_clear_warning()
Definition component.h:296
bool has_state() const
sensor::Sensor * current_sensor_
Definition cse7766.h:42
sensor::Sensor * voltage_sensor_
Definition cse7766.h:41
sensor::Sensor * reactive_power_sensor_
Definition cse7766.h:46
uint32_t get_24_bit_uint_(uint8_t start_index) const
Definition cse7766.h:32
sensor::Sensor * apparent_power_sensor_
Definition cse7766.h:45
sensor::Sensor * power_sensor_
Definition cse7766.h:43
uint8_t raw_data_[CSE7766_RAW_DATA_SIZE]
Definition cse7766.h:37
sensor::Sensor * power_factor_sensor_
Definition cse7766.h:47
sensor::Sensor * energy_sensor_
Definition cse7766.h:44
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:65
optional< std::array< uint8_t, N > > read_array()
Definition uart.h:43
ESPHOME_LOG_TAG(TAG, "cse7766")
constexpr uint8_t sum_bytes(const uint8_t *data, size_t len, uint8_t sum=0)
Add up the bytes of data with size len, truncated to 8 bits: the checksum many serial and IR protocol...
Definition helpers.h:894
char * format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator)
Format byte array as uppercase hex to buffer (base implementation).
Definition helpers.cpp:498
size_t size_t pos
Definition helpers.h:1255
constexpr size_t format_hex_pretty_size(size_t byte_count)
Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0".
Definition helpers.h:1609
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t