ESPHome 2026.10.0
Loading...
Searching...
No Matches
pzem6l24.cpp
Go to the documentation of this file.
1#include "pzem6l24.h"
2#include "esphome/core/hal.h"
4#include "esphome/core/log.h"
5
6#include <cmath>
7#include <type_traits>
8
10
11static const char *const TAG = "pzem6l24";
12
13// -----------------------------------------------------------------------
14// Register map (input registers, starting address 0x0000):
15//
16// The PZEM-6L24 returns all register bytes in little-endian order,
17// i.e. the low byte of each 16-bit register is transmitted first.
18// 32-bit quantities occupy two consecutive registers with the low
19// word at the lower address.
20//
21// NOTE: this is the opposite of standard Modbus, and of the single-phase
22// pzemac component, which decodes big-endian. It is not an oversight: the
23// byte order below was established against a live PZEM-6L24, so please do
24// not "correct" it to big-endian without a device to verify against.
25//
26// Byte offset = register_address × 2
27//
28// 0x0000 (byte 0) – Voltage A (uint16, ×0.1 V)
29// 0x0001 (byte 2) – Voltage B (uint16, ×0.1 V)
30// 0x0002 (byte 4) – Voltage C (uint16, ×0.1 V)
31// 0x0003 (byte 6) – Current A (uint16, ×0.01 A)
32// 0x0004 (byte 8) – Current B (uint16, ×0.01 A)
33// 0x0005 (byte 10) – Current C (uint16, ×0.01 A)
34// 0x0006 (byte 12) – Frequency A (uint16, ×0.01 Hz)
35// 0x0007 (byte 14) – Frequency B (uint16, ×0.01 Hz)
36// 0x0008 (byte 16) – Frequency C (uint16, ×0.01 Hz)
37// 0x0009 (byte 18) – Voltage angle B (uint16, ×0.01 °)
38// 0x000A (byte 20) – Voltage angle C (uint16, ×0.01 °)
39// 0x000B (byte 22) – Current angle A (uint16, ×0.01 °)
40// 0x000C (byte 24) – Current angle B (uint16, ×0.01 °)
41// 0x000D (byte 26) – Current angle C (uint16, ×0.01 °)
42// 0x000E (byte 28) – Active power A (int32 lo-word, ×0.1 W)
43// 0x000F (byte 30) – Active power A (int32 hi-word)
44// 0x0010 (byte 32) – Active power B (int32 lo-word, ×0.1 W)
45// 0x0011 (byte 34) – Active power B (int32 hi-word)
46// 0x0012 (byte 36) – Active power C (int32 lo-word, ×0.1 W)
47// 0x0013 (byte 38) – Active power C (int32 hi-word)
48// 0x0014 (byte 40) – Reactive power A (int32 lo-word, ×0.1 var)
49// 0x0015 (byte 42) – Reactive power A (int32 hi-word)
50// 0x0016 (byte 44) – Reactive power B (int32 lo-word, ×0.1 var)
51// 0x0017 (byte 46) – Reactive power B (int32 hi-word)
52// 0x0018 (byte 48) – Reactive power C (int32 lo-word, ×0.1 var)
53// 0x0019 (byte 50) – Reactive power C (int32 hi-word)
54// 0x001A (byte 52) – Apparent power A (int32 lo-word, ×0.1 VA)
55// 0x001B (byte 54) – Apparent power A (int32 hi-word)
56// 0x001C (byte 56) – Apparent power B (int32 lo-word, ×0.1 VA)
57// 0x001D (byte 58) – Apparent power B (int32 hi-word)
58// 0x001E (byte 60) – Apparent power C (int32 lo-word, ×0.1 VA)
59// 0x001F (byte 62) – Apparent power C (int32 hi-word)
60// 0x0020 (byte 64) – Total active pwr (int32 lo-word, ×0.1 W)
61// 0x0021 (byte 66) – Total active pwr (int32 hi-word)
62// 0x0022 (byte 68) – Total reactive pwr (int32 lo-word, ×0.1 var)
63// 0x0023 (byte 70) – Total reactive pwr (int32 hi-word)
64// 0x0024 (byte 72) – Total apparent pwr (int32 lo-word, ×0.1 VA)
65// 0x0025 (byte 74) – Total apparent pwr (int32 hi-word)
66// 0x0026 (byte 76) – Power factor A/B: hi-byte = A (×0.01), lo-byte = B (×0.01)
67// 0x0027 (byte 78) – Power factor C/tot:hi-byte = C (×0.01), lo-byte = total (×0.01)
68// 0x0028 (byte 80) – Active energy A (uint32 lo-word, ×0.1 kWh)
69// 0x0029 (byte 82) – Active energy A (uint32 hi-word)
70// 0x002A (byte 84) – Active energy B (uint32 lo-word, ×0.1 kWh)
71// 0x002B (byte 86) – Active energy B (uint32 hi-word)
72// 0x002C (byte 88) – Active energy C (uint32 lo-word, ×0.1 kWh)
73// 0x002D (byte 90) – Active energy C (uint32 hi-word)
74// 0x002E (byte 92) – Reactive energy A (uint32 lo-word, ×0.1 kvarh)
75// 0x002F (byte 94) – Reactive energy A (uint32 hi-word)
76// 0x0030 (byte 96) – Reactive energy B (uint32 lo-word, ×0.1 kvarh)
77// 0x0031 (byte 98) – Reactive energy B (uint32 hi-word)
78// 0x0032 (byte 100)– Reactive energy C (uint32 lo-word, ×0.1 kvarh)
79// 0x0033 (byte 102)– Reactive energy C (uint32 hi-word)
80// 0x0034 (byte 104)– Apparent energy A (uint32 lo-word, ×0.1 kVAh)
81// 0x0035 (byte 106)– Apparent energy A (uint32 hi-word)
82// 0x0036 (byte 108)– Apparent energy B (uint32 lo-word, ×0.1 kVAh)
83// 0x0037 (byte 110)– Apparent energy B (uint32 hi-word)
84// 0x0038 (byte 112)– Apparent energy C (uint32 lo-word, ×0.1 kVAh)
85// 0x0039 (byte 114)– Apparent energy C (uint32 hi-word)
86// 0x003A (byte 116)– Total active nrg (uint32 lo-word, ×0.1 kWh)
87// 0x003B (byte 118)– Total active nrg (uint32 hi-word)
88// 0x003C (byte 120)– Total reactive nrg (uint32 lo-word, ×0.1 kvarh)
89// 0x003D (byte 122)– Total reactive nrg (uint32 hi-word)
90// 0x003E (byte 124)– Total apparent nrg (uint32 lo-word, ×0.1 kVAh)
91// 0x003F (byte 126)– Total apparent nrg (uint32 hi-word)
92// -----------------------------------------------------------------------
93
94// Width of a quantity in the register map above.
95enum RegType : uint8_t {
96 REG_U8, // single byte (the packed power factors)
97 REG_U16, // one register, unsigned
98 REG_U32, // two registers, unsigned, low word first
99 REG_I32, // two registers, signed, low word first
100};
101
102// Scale factor of a quantity; the table has only these two, so a byte replaces a float per entry.
103enum Scale : uint8_t {
104 SCALE_DECI, // ×0.1
105 SCALE_CENTI, // ×0.01
106};
107
108// One decodable quantity: where it lives in the payload, how to read it and which sensor it feeds.
109// Copied out of flash with memcpy, so it must stay trivially copyable.
110struct SensorEntry {
111 sensor::Sensor *PZEM6L24::*member;
112 uint8_t offset;
113 RegType type;
114 Scale scale;
115};
116static_assert(std::is_trivially_copyable_v<SensorEntry>, "SENSORS is copied out of flash with memcpy");
117
118// True for the periodic register read issued by update(); the only other request is the 0x42 reset.
119static bool is_register_read(std::span<const uint8_t> request_pdu) {
120 return modbus::helpers::pdu_function_code(request_pdu) ==
121 static_cast<uint8_t>(modbus::FunctionCode::READ_INPUT_REGISTERS);
122}
123
124void PZEM6L24::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
125 if (!is_register_read(request_pdu)) {
126 return;
127 }
128 this->read_finished_();
129 const auto data = modbus::helpers::server_pdu_payload(response_pdu);
130 // Anything but exactly PZEM_PAYLOAD_SIZE bytes cannot be decoded by the table.
131 if (data.size() != PZEM_PAYLOAD_SIZE) {
132 ESP_LOGW(TAG, "Invalid data size for PZEM-6L24: expected %zu bytes, got %zu", PZEM_PAYLOAD_SIZE, data.size());
133 this->read_failed_();
134 return;
135 }
136 this->consecutive_failures_ = 0;
137 this->publish_(data.data());
138}
139
140void PZEM6L24::on_error(std::span<const uint8_t> request_pdu, modbus::ExceptionCode) {
141 this->request_failed_(request_pdu);
142}
143
144bool PZEM6L24::on_no_response(std::span<const uint8_t> request_pdu) {
145 this->request_failed_(request_pdu);
146 return false; // no retry; the next update() polls again.
147}
148
149void PZEM6L24::on_not_sent(std::span<const uint8_t> request_pdu) { this->request_failed_(request_pdu); }
150
151// A register read that produced no measurements counts toward blanking; a failed energy reset does
152// not, but the user is told. The hub has already logged the cause.
153void PZEM6L24::request_failed_(std::span<const uint8_t> request_pdu) {
154 if (is_register_read(request_pdu)) {
155 this->read_finished_();
156 this->read_failed_();
157 } else {
158 ESP_LOGW(TAG, "Energy reset failed; the counters were not cleared");
159 }
160}
161
162// `data` points at PZEM_PAYLOAD_SIZE validated bytes, or is nullptr to blank every sensor; both walk
163// the same table.
164void PZEM6L24::publish_(const uint8_t *data) {
165 const bool available = data != nullptr;
166
167 // Byte offset, width and scale for every quantity, in register-map order. All three phases share the
168 // same grid frequency, so phase A's register is reported.
169 //
170 // PROGMEM: on ESP8266 .rodata is DRAM, so the table would otherwise cost ~280 bytes of RAM; the price
171 // is one 8-byte copy per sensor per poll. No name column for the same reason; dump_config() names
172 // every sensor from flash.
173 static constexpr SensorEntry SENSORS[] PROGMEM = {
174 // Voltages (×0.1 V)
178 // Currents (×0.01 A)
182 // Frequency (×0.01 Hz)
184 // Active powers (×0.1 W, signed)
189 // Reactive powers (×0.1 var, signed)
194 // Apparent powers (×0.1 VA, signed)
199 // Power factors (×0.01), packed two per register:
200 // register 0x0026 (bytes 76/77): lo-byte = phase B, hi-byte = phase A
201 // register 0x0027 (bytes 78/79): lo-byte = combined, hi-byte = phase C
206 // Active energies (×0.1 kWh, unsigned)
211 // Reactive energies (×0.1 kvarh, unsigned)
216 // Apparent energies (×0.1 kVAh, unsigned)
221 };
222
223 for (const SensorEntry &flash_entry : SENSORS) {
224 SensorEntry entry;
225 progmem_memcpy(&entry, &flash_entry, sizeof(entry));
226 sensor::Sensor *sens = this->*entry.member;
227 if (sens == nullptr)
228 continue;
229 if (!available) {
230 sens->publish_state(NAN);
231 continue;
232 }
233 // No default: an added RegType must fail to compile. The wire is little-endian, hence the reversed
234 // byte arguments.
235 const size_t o = entry.offset;
236 float raw = 0.0f;
237 switch (entry.type) {
238 case REG_U8:
239 raw = data[o];
240 break;
241 case REG_U16:
242 raw = encode_uint16(data[o + 1], data[o]);
243 break;
244 case REG_U32:
245 raw = encode_uint32(data[o + 3], data[o + 2], data[o + 1], data[o]);
246 break;
247 case REG_I32:
248 raw = static_cast<int32_t>(encode_uint32(data[o + 3], data[o + 2], data[o + 1], data[o]));
249 break;
250 }
251 sens->publish_state(raw * (entry.scale == SCALE_CENTI ? 0.01f : 0.1f));
252 }
253}
254
256 if (this->read_input_registers(0x0000, PZEM_REGISTER_COUNT)) {
257 this->reads_outstanding_++;
258 } else if (this->reads_outstanding_ == 0) {
259 // Refused with nothing in flight: no callback is coming, and the hub has logged why. A refusal
260 // while a read is outstanding is a duplicate of it, which still resolves in that read's callback.
261 this->read_failed_();
262 }
263}
264
266 ESP_LOGCONFIG(TAG,
267 "PZEM-6L24:\n"
268 " Address: 0x%02X",
269 this->address_);
270 LOG_UPDATE_INTERVAL(this);
271 LOG_SENSOR(" ", "Voltage A", this->voltage_a_);
272 LOG_SENSOR(" ", "Voltage B", this->voltage_b_);
273 LOG_SENSOR(" ", "Voltage C", this->voltage_c_);
274 LOG_SENSOR(" ", "Current A", this->current_a_);
275 LOG_SENSOR(" ", "Current B", this->current_b_);
276 LOG_SENSOR(" ", "Current C", this->current_c_);
277 LOG_SENSOR(" ", "Active Power A", this->active_power_a_);
278 LOG_SENSOR(" ", "Active Power B", this->active_power_b_);
279 LOG_SENSOR(" ", "Active Power C", this->active_power_c_);
280 LOG_SENSOR(" ", "Reactive Power A", this->reactive_power_a_);
281 LOG_SENSOR(" ", "Reactive Power B", this->reactive_power_b_);
282 LOG_SENSOR(" ", "Reactive Power C", this->reactive_power_c_);
283 LOG_SENSOR(" ", "Apparent Power A", this->apparent_power_a_);
284 LOG_SENSOR(" ", "Apparent Power B", this->apparent_power_b_);
285 LOG_SENSOR(" ", "Apparent Power C", this->apparent_power_c_);
286 LOG_SENSOR(" ", "Power Factor A", this->power_factor_a_);
287 LOG_SENSOR(" ", "Power Factor B", this->power_factor_b_);
288 LOG_SENSOR(" ", "Power Factor C", this->power_factor_c_);
289 LOG_SENSOR(" ", "Active Energy A", this->active_energy_a_);
290 LOG_SENSOR(" ", "Active Energy B", this->active_energy_b_);
291 LOG_SENSOR(" ", "Active Energy C", this->active_energy_c_);
292 LOG_SENSOR(" ", "Reactive Energy A", this->reactive_energy_a_);
293 LOG_SENSOR(" ", "Reactive Energy B", this->reactive_energy_b_);
294 LOG_SENSOR(" ", "Reactive Energy C", this->reactive_energy_c_);
295 LOG_SENSOR(" ", "Apparent Energy A", this->apparent_energy_a_);
296 LOG_SENSOR(" ", "Apparent Energy B", this->apparent_energy_b_);
297 LOG_SENSOR(" ", "Apparent Energy C", this->apparent_energy_c_);
298 LOG_SENSOR(" ", "Frequency", this->frequency_);
299 LOG_SENSOR(" ", "Total Active Power", this->total_active_power_);
300 LOG_SENSOR(" ", "Total Reactive Power", this->total_reactive_power_);
301 LOG_SENSOR(" ", "Total Apparent Power", this->total_apparent_power_);
302 LOG_SENSOR(" ", "Total Power Factor", this->total_power_factor_);
303 LOG_SENSOR(" ", "Total Active Energy", this->total_active_energy_);
304 LOG_SENSOR(" ", "Total Reactive Energy", this->total_reactive_energy_);
305 LOG_SENSOR(" ", "Total Apparent Energy", this->total_apparent_energy_);
306}
307
308// Blank after MAX_CONSECUTIVE_READ_FAILURES; stays blanked until a poll succeeds.
310 if (this->consecutive_failures_ >= MAX_CONSECUTIVE_READ_FAILURES) {
311 return;
312 }
313 if (++this->consecutive_failures_ == MAX_CONSECUTIVE_READ_FAILURES) {
314 ESP_LOGW(TAG, "No valid reading in %u consecutive polls; the readings are now unavailable",
315 MAX_CONSECUTIVE_READ_FAILURES);
316 this->publish_(nullptr);
317 }
318}
319
320// One terminal has arrived for a register read, so that read is no longer in flight.
322 if (this->reads_outstanding_ > 0) {
323 this->reads_outstanding_--;
324 }
325}
326
328 const auto pdu = build_reset_pdu(phase_option);
329 // A refused request gets no callback, so report it here.
330 if (!this->queue_pdu(pdu)) {
331 this->request_failed_(pdu);
332 }
333}
334
335} // namespace esphome::pzem6l24
uint8_t raw[35]
Definition bl0939.h:0
sensor::Sensor * active_energy_c_
Definition pzem6l24.h:140
sensor::Sensor * voltage_b_
Definition pzem6l24.h:115
sensor::Sensor * apparent_energy_c_
Definition pzem6l24.h:148
sensor::Sensor * active_power_c_
Definition pzem6l24.h:124
void publish_(const uint8_t *data)
Definition pzem6l24.cpp:164
sensor::Sensor * reactive_energy_b_
Definition pzem6l24.h:143
sensor::Sensor * reactive_energy_a_
Definition pzem6l24.h:142
sensor::Sensor * power_factor_c_
Definition pzem6l24.h:136
sensor::Sensor * power_factor_b_
Definition pzem6l24.h:135
sensor::Sensor * active_energy_a_
Definition pzem6l24.h:138
sensor::Sensor * apparent_energy_a_
Definition pzem6l24.h:146
bool on_no_response(std::span< const uint8_t > request_pdu) override
Definition pzem6l24.cpp:144
void reset_energy(ResetPhase phase_option)
Definition pzem6l24.cpp:327
sensor::Sensor * reactive_power_c_
Definition pzem6l24.h:128
sensor::Sensor * total_reactive_power_
Definition pzem6l24.h:153
sensor::Sensor * reactive_power_a_
Definition pzem6l24.h:126
sensor::Sensor * apparent_power_a_
Definition pzem6l24.h:130
sensor::Sensor * voltage_a_
Definition pzem6l24.h:114
sensor::Sensor * current_b_
Definition pzem6l24.h:119
void on_response(std::span< const uint8_t > request_pdu, std::span< const uint8_t > response_pdu) override
Definition pzem6l24.cpp:124
void dump_config() override
Definition pzem6l24.cpp:265
sensor::Sensor * total_active_energy_
Definition pzem6l24.h:156
sensor::Sensor * active_power_a_
Definition pzem6l24.h:122
sensor::Sensor * total_power_factor_
Definition pzem6l24.h:155
sensor::Sensor * total_active_power_
Definition pzem6l24.h:152
sensor::Sensor * reactive_energy_c_
Definition pzem6l24.h:144
sensor::Sensor * current_a_
Definition pzem6l24.h:118
sensor::Sensor * active_power_b_
Definition pzem6l24.h:123
sensor::Sensor * total_reactive_energy_
Definition pzem6l24.h:157
void on_error(std::span< const uint8_t > request_pdu, modbus::ExceptionCode exception_code) override
Definition pzem6l24.cpp:140
sensor::Sensor * apparent_energy_b_
Definition pzem6l24.h:147
void on_not_sent(std::span< const uint8_t > request_pdu) override
Definition pzem6l24.cpp:149
void request_failed_(std::span< const uint8_t > request_pdu)
Definition pzem6l24.cpp:153
sensor::Sensor * frequency_
Definition pzem6l24.h:151
sensor::Sensor * total_apparent_energy_
Definition pzem6l24.h:158
sensor::Sensor * apparent_power_b_
Definition pzem6l24.h:131
sensor::Sensor * voltage_c_
Definition pzem6l24.h:116
sensor::Sensor * active_energy_b_
Definition pzem6l24.h:139
sensor::Sensor * apparent_power_c_
Definition pzem6l24.h:132
sensor::Sensor * current_c_
Definition pzem6l24.h:120
sensor::Sensor * total_apparent_power_
Definition pzem6l24.h:154
sensor::Sensor * power_factor_a_
Definition pzem6l24.h:134
sensor::Sensor * reactive_power_b_
Definition pzem6l24.h:127
Base-class for all sensors.
Definition sensor.h:47
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:65
std::span< const uint8_t > server_pdu_payload(std::span< const uint8_t > pdu)
Returns the payload portion of a server response PDU: the bytes after the function code,...
uint8_t pdu_function_code(std::span< const uint8_t > pdu)
Function code of a PDU, exception flag masked; 0 for an empty PDU.
constexpr std::array< uint8_t, 3 > build_reset_pdu(ResetPhase phase)
Definition pzem6l24.h:34
void progmem_memcpy(void *dst, const void *src, size_t len)
Definition hal.h:48
constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, uint8_t byte4)
Encode a 32-bit value given four bytes in most to least significant byte order.
Definition helpers.h:986
constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb)
Encode a 16-bit value given the most and least significant byte.
Definition helpers.h:978