ESPHome 2026.11.0-dev
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
11ESPHOME_LOG_TAG(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)
175 {&PZEM6L24::voltage_a_sensor_, 0, REG_U16, SCALE_DECI},
176 {&PZEM6L24::voltage_b_sensor_, 2, REG_U16, SCALE_DECI},
177 {&PZEM6L24::voltage_c_sensor_, 4, REG_U16, SCALE_DECI},
178 // Currents (×0.01 A)
179 {&PZEM6L24::current_a_sensor_, 6, REG_U16, SCALE_CENTI},
180 {&PZEM6L24::current_b_sensor_, 8, REG_U16, SCALE_CENTI},
181 {&PZEM6L24::current_c_sensor_, 10, REG_U16, SCALE_CENTI},
182 // Frequency (×0.01 Hz)
183 {&PZEM6L24::frequency_sensor_, 12, REG_U16, SCALE_CENTI},
184 // Active powers (×0.1 W, signed)
185 {&PZEM6L24::active_power_a_sensor_, 28, REG_I32, SCALE_DECI},
186 {&PZEM6L24::active_power_b_sensor_, 32, REG_I32, SCALE_DECI},
187 {&PZEM6L24::active_power_c_sensor_, 36, REG_I32, SCALE_DECI},
188 {&PZEM6L24::total_active_power_sensor_, 64, REG_I32, SCALE_DECI},
189 // Reactive powers (×0.1 var, signed)
190 {&PZEM6L24::reactive_power_a_sensor_, 40, REG_I32, SCALE_DECI},
191 {&PZEM6L24::reactive_power_b_sensor_, 44, REG_I32, SCALE_DECI},
192 {&PZEM6L24::reactive_power_c_sensor_, 48, REG_I32, SCALE_DECI},
193 {&PZEM6L24::total_reactive_power_sensor_, 68, REG_I32, SCALE_DECI},
194 // Apparent powers (×0.1 VA, signed)
195 {&PZEM6L24::apparent_power_a_sensor_, 52, REG_I32, SCALE_DECI},
196 {&PZEM6L24::apparent_power_b_sensor_, 56, REG_I32, SCALE_DECI},
197 {&PZEM6L24::apparent_power_c_sensor_, 60, REG_I32, SCALE_DECI},
198 {&PZEM6L24::total_apparent_power_sensor_, 72, REG_I32, SCALE_DECI},
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
202 {&PZEM6L24::power_factor_a_sensor_, 77, REG_U8, SCALE_CENTI},
203 {&PZEM6L24::power_factor_b_sensor_, 76, REG_U8, SCALE_CENTI},
204 {&PZEM6L24::power_factor_c_sensor_, 79, REG_U8, SCALE_CENTI},
205 {&PZEM6L24::total_power_factor_sensor_, 78, REG_U8, SCALE_CENTI},
206 // Active energies (×0.1 kWh, unsigned)
207 {&PZEM6L24::active_energy_a_sensor_, 80, REG_U32, SCALE_DECI},
208 {&PZEM6L24::active_energy_b_sensor_, 84, REG_U32, SCALE_DECI},
209 {&PZEM6L24::active_energy_c_sensor_, 88, REG_U32, SCALE_DECI},
210 {&PZEM6L24::total_active_energy_sensor_, 116, REG_U32, SCALE_DECI},
211 // Reactive energies (×0.1 kvarh, unsigned)
212 {&PZEM6L24::reactive_energy_a_sensor_, 92, REG_U32, SCALE_DECI},
213 {&PZEM6L24::reactive_energy_b_sensor_, 96, REG_U32, SCALE_DECI},
214 {&PZEM6L24::reactive_energy_c_sensor_, 100, REG_U32, SCALE_DECI},
215 {&PZEM6L24::total_reactive_energy_sensor_, 120, REG_U32, SCALE_DECI},
216 // Apparent energies (×0.1 kVAh, unsigned)
217 {&PZEM6L24::apparent_energy_a_sensor_, 104, REG_U32, SCALE_DECI},
218 {&PZEM6L24::apparent_energy_b_sensor_, 108, REG_U32, SCALE_DECI},
219 {&PZEM6L24::apparent_energy_c_sensor_, 112, REG_U32, SCALE_DECI},
220 {&PZEM6L24::total_apparent_energy_sensor_, 124, REG_U32, SCALE_DECI},
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_sensor_);
272 LOG_SENSOR(" ", "Voltage B", this->voltage_b_sensor_);
273 LOG_SENSOR(" ", "Voltage C", this->voltage_c_sensor_);
274 LOG_SENSOR(" ", "Current A", this->current_a_sensor_);
275 LOG_SENSOR(" ", "Current B", this->current_b_sensor_);
276 LOG_SENSOR(" ", "Current C", this->current_c_sensor_);
277 LOG_SENSOR(" ", "Active Power A", this->active_power_a_sensor_);
278 LOG_SENSOR(" ", "Active Power B", this->active_power_b_sensor_);
279 LOG_SENSOR(" ", "Active Power C", this->active_power_c_sensor_);
280 LOG_SENSOR(" ", "Reactive Power A", this->reactive_power_a_sensor_);
281 LOG_SENSOR(" ", "Reactive Power B", this->reactive_power_b_sensor_);
282 LOG_SENSOR(" ", "Reactive Power C", this->reactive_power_c_sensor_);
283 LOG_SENSOR(" ", "Apparent Power A", this->apparent_power_a_sensor_);
284 LOG_SENSOR(" ", "Apparent Power B", this->apparent_power_b_sensor_);
285 LOG_SENSOR(" ", "Apparent Power C", this->apparent_power_c_sensor_);
286 LOG_SENSOR(" ", "Power Factor A", this->power_factor_a_sensor_);
287 LOG_SENSOR(" ", "Power Factor B", this->power_factor_b_sensor_);
288 LOG_SENSOR(" ", "Power Factor C", this->power_factor_c_sensor_);
289 LOG_SENSOR(" ", "Active Energy A", this->active_energy_a_sensor_);
290 LOG_SENSOR(" ", "Active Energy B", this->active_energy_b_sensor_);
291 LOG_SENSOR(" ", "Active Energy C", this->active_energy_c_sensor_);
292 LOG_SENSOR(" ", "Reactive Energy A", this->reactive_energy_a_sensor_);
293 LOG_SENSOR(" ", "Reactive Energy B", this->reactive_energy_b_sensor_);
294 LOG_SENSOR(" ", "Reactive Energy C", this->reactive_energy_c_sensor_);
295 LOG_SENSOR(" ", "Apparent Energy A", this->apparent_energy_a_sensor_);
296 LOG_SENSOR(" ", "Apparent Energy B", this->apparent_energy_b_sensor_);
297 LOG_SENSOR(" ", "Apparent Energy C", this->apparent_energy_c_sensor_);
298 LOG_SENSOR(" ", "Frequency", this->frequency_sensor_);
299 LOG_SENSOR(" ", "Total Active Power", this->total_active_power_sensor_);
300 LOG_SENSOR(" ", "Total Reactive Power", this->total_reactive_power_sensor_);
301 LOG_SENSOR(" ", "Total Apparent Power", this->total_apparent_power_sensor_);
302 LOG_SENSOR(" ", "Total Power Factor", this->total_power_factor_sensor_);
303 LOG_SENSOR(" ", "Total Active Energy", this->total_active_energy_sensor_);
304 LOG_SENSOR(" ", "Total Reactive Energy", this->total_reactive_energy_sensor_);
305 LOG_SENSOR(" ", "Total Apparent Energy", this->total_apparent_energy_sensor_);
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
327void PZEM6L24::reset_energy(ResetPhase phase_option) {
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
void publish_(const uint8_t *data)
Definition pzem6l24.cpp:164
bool on_no_response(std::span< const uint8_t > request_pdu) override
Definition pzem6l24.cpp:144
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
void on_error(std::span< const uint8_t > request_pdu, modbus::ExceptionCode exception_code) override
Definition pzem6l24.cpp:140
SUB_SENSOR(voltage_a) SUB_SENSOR(voltage_b) SUB_SENSOR(voltage_c) SUB_SENSOR(current_a) SUB_SENSOR(current_b) SUB_SENSOR(current_c) SUB_SENSOR(active_power_a) SUB_SENSOR(active_power_b) SUB_SENSOR(active_power_c) SUB_SENSOR(reactive_power_a) SUB_SENSOR(reactive_power_b) SUB_SENSOR(reactive_power_c) SUB_SENSOR(apparent_power_a) SUB_SENSOR(apparent_power_b) SUB_SENSOR(apparent_power_c) SUB_SENSOR(power_factor_a) SUB_SENSOR(power_factor_b) SUB_SENSOR(power_factor_c) SUB_SENSOR(active_energy_a) SUB_SENSOR(active_energy_b) SUB_SENSOR(active_energy_c) SUB_SENSOR(reactive_energy_a) SUB_SENSOR(reactive_energy_b) SUB_SENSOR(reactive_energy_c) SUB_SENSOR(apparent_energy_a) SUB_SENSOR(apparent_energy_b) SUB_SENSOR(apparent_energy_c) SUB_SENSOR(frequency) SUB_SENSOR(total_active_power) SUB_SENSOR(total_reactive_power) SUB_SENSOR(total_apparent_power) SUB_SENSOR(total_power_factor) SUB_SENSOR(total_active_energy) SUB_SENSOR(total_reactive_energy) SUB_SENSOR(total_apparent_energy) public void update() override
Definition pzem6l24.cpp:255
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
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.
ESPHOME_LOG_TAG(TAG, "pzem6l24")
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:56
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:988
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:980