Merge pull request #5370 from tonhuisman/feature/P102-add-support-for-pzem017v1

[P102] Add support for PZEM-017v1
This commit is contained in:
TD-er
2025-10-12 23:24:32 +02:00
committed by GitHub
11 changed files with 511 additions and 93 deletions
+223 -1
View File
@@ -24,7 +24,7 @@ Used libraries: |P102_usedlibraries|
Description
-----------
PZEM-004T V3.0 or Version 3.0 is the upgraded version to replace the old PZEM004T V1.0.
PZEM-004T (AC) V3.0 or Version 3.0 is the upgraded version to replace the old PZEM004T V1.0.
The old version has been sold out in most of the online store and is no longer being produced.
Improvements over V1:
@@ -99,18 +99,238 @@ Active Energy
* Resolution: 1Wh
* Measurement accuracy: 0.5%
Description
-----------
PZEM-017 v1.0/v2.0 (DC) For measuring DC loads. When the voltage is below 7V, an independent (**not from PC USB port!**) USB powersupply should be used on the USB input connector.
Specifications
--------------
Voltage
^^^^^^^
* Measuring range: 0.05 - 300V
* Resolution: 0.01V
* Measurement accuracy: 1%
Current
^^^^^^^
* Measuring range:
* 0.02 - 50/100/200/300A(Depending on shunt)
* Resolution: 0.01A
* Measurement accuracy: 1%
Active power
^^^^^^^^^^^^
* Measuring range:
* 0.2 - 90kW(Depending on shunt)
* Starting measure power: 0.2W
* Resolution: 0.1W
* Measurement accuracy: 1%
Active Energy
^^^^^^^^^^^^^
* Measuring range: 0 - 9999.99kWh
* Resolution: 1Wh
* Measurement accuracy: 1%
Installation and configuration
------------------------------
When installing multiple PZEM devices on a single ESP unit, some specific order of configuration is required to be able to correctly configure a second, third, etc. PZEM.
This is described below, in the :cyan:`Installing multiple PZEM units on a single ESP` paragraph.
Configuration
-------------
.. image:: P102_DeviceConfiguration.png
Task Settings
^^^^^^^^^^^^^
* **Name**: Required by ESPEasy, must be unique among the list of available devices/tasks.
* **Enabled**: The device can be disabled or enabled. When not enabled the device should not use any resources.
Sensor
^^^^^^
See: :ref:`SerialHelper_page`
Device Settings
^^^^^^^^^^^^^^^
* **PZEM Model**: Select the PZEM model that is to be handled by this task. (For changing the Address the model doesn't matter as they use the same command.)
Available options:
* *PZEM-004Tv30 (AC)*: The AC model, and default setting.
* *PZEM-017v1 (DC)*: The DC model. Also compatible with later v2/2.0 models.
For reading the correct values from a PZEM-017, that Model *must* be selected as it uses a different data format to fetch the results.
PZEM Actions
^^^^^^^^^^^^
* **PZEM Mode**: By default, the ``Read value`` option is selected.
Available options:
* *Read value*: Normal operating mode, for reading the configured values from the PZEM.
* *Reset Energy*: For resetting the counted total energy, this option can be selected. See below for a description.
* *Program address*: Only available on the *first* PZEM device, for changing the PZEM unit MODBUS address, as described below.
|
* **Address of PZEM**: Set to the address of the PZEM unit to be used. When only a single device is connected, can be set to 0, as a sort of 'universal' address.
Available address range for the *first* PZEM device: 0 .. 247, on subsequent PZEM devices: 1 .. 247.
Output Configuration
^^^^^^^^^^^^^^^^^^^^
For all 4 available Values slots for this task, a value to use can be selected here. The available options depend om the **PZEM Model** setting:
* *Voltage_V*
* *Current_A*
* *Power_W*
* *Energy_kWh*
* *Power_Factor_cosphi*: for ``PZEM-004Tv30 (AC)`` only.
* *Frequency_Hz*: for ``PZEM-004Tv30 (AC)`` only.
.. include:: DataAcquisition.repl
* **Interval**: By default, Interval will be set to 60 sec for this plugin. It is the frequency used to read sensor values and send these to any Controllers configured for this device.
Values
^^^^^^
The measured values are configured above, and get their names based on the selection made there.
For a complete description of all available columns see the `Plugin - Values <_Plugin.html#values>`_ section.
|
.. Events
.. ~~~~~~
.. .. include:: P102_events.repl
Resetting the Energy counter
----------------------------
For resetting the counted Energy value in the PZEM unit, there are 2 methods:
1. Use the *Reset Energy* **PZEM Mode** setting.
2. Send the command ``resetenergy,<modbus_address>`` to the ESP unit. This can be done via the Command entry on the Tools page, from Rules, or via a url command as shown in the configuration page.
Use the *Reset Energy* **PZEM Mode** setting
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For resetting the incrementing Energy counter in the PZEM unit, this PZEM Mode option can be used.
* Set **PZEM Mode** to ``Reset Energy``, and :cyan:`Submit` the page.
The command will be executed immediately, and afterward the status will shown:
.. image:: P102_ResetEnergyDone.png
The blue message showing that the command was sent. The **PZEM Mode** is already reset to ``Read value``.
To store this change in settings, the :cyan:`Submit` button should be used once more (you can of course change more settings if desired, that's why no automatic save is done.)
Send the ``resetenergy,<modbus_address>`` command
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For an easy alternative to reset the Energy counter, you can use the ``resetenergy,<modbus_address>`` command, f.e. from the ``Command`` field on the Tools page, to reset the counter for the specific modbus address (range 1..247, or 0 if only 1 device is on the bus).
Installing multiple PZEM units on a single ESP
----------------------------------------------
Configuring separate MODBUS addresses on each device
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To uniquely identify each PZEM unit on the bus, it has to be configured to use a unique address. The range of these addresses is 1 .. 247.
* Add the, numerically, first PZEM task, f.e. in Devices slot 1.
* Configure the serial port, and on ESP32 also the GPIO pins, to be used, enable the plugin, and connect a single PZEM to the RS485 converter. When using ``HW Serial0``, the Serial Console Settings, on the Tools/Advanced page, have to be disabled to avoid issues with his port.
* Set **PZEM Mode** to ``Program address`` and :cyan:`Submit` the page.
.. image:: P102_ProgramAddress.png
Fill the desired address in the **Address of PZEM** field, change the **Confirm address programming ?** field to ``YES``, and again :cyan:`Submit` the page.
When successful, a message :green:`Address programming done !` is displayed, and the **PZEM Mode** is set to the default ``Read value`` setting.
* Disconnect the PZEM unit from the RS485 bus, connect the next PZEM unit, and repeat above steps, using a diferent **Address of PZEM** value, for all units to be connected.
N.B.: The last set Address of PZEM will be kept in the configuration, but can be changed to the desired first PZEM unit.
N.B.2: To correctly store the **PZEM Mode** setting, the configuration should be saved once more, using the :cyan:`Submit` button!
Configuring a secondary PZEM unit
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The first PZEM unit can be configured as shown above in the generic Configuration paragraph. Once thats completed, with the correct, unique, MODBUS Address set, both in all PZEM units to connect and the task configuration for the first PZEM task, that task should be Enabled, so the next task added can detect that it will be the second (or third, etc.) device on the MODBUS chain.
After that task is running, a new PZEM task can be added, with a task number higher than the *first* PZEM task.
.. warning:: This is crucial for the PZEM tasks to start correctly, and in the correct order, on reboot or power-up of the ESP!
These secondary PZEM tasks do not use the Serial configuration, as they use the same connection as the *first* PZEM task.
.. image:: P102_SecondConfiguration.png
Sensor
^^^^^^
This selector should not be used, as it is ignored.
Device Settings
^^^^^^^^^^^^^^^
* **PZEM Model**: Select the PZEM model that is to be handled by this task. (Changing the selection will save the new setting and reload the page.)
For reading the correct values from a PZEM-017, that Model *must* be selected as it uses a different data format to fetch the results.
PZEM Actions
^^^^^^^^^^^^
* **PZEM Mode**: By default, the ``Read value`` option is selected.
Available options:
* *Read value*: Normal operating mode, for reading the configured values from the PZEM.
* *Reset Energy*: For resetting the counted total energy, this option can be selected. See above for a description.
* **Address of PZEM**: Set to the address of the PZEM unit to be used.
Available address range: 1 .. 247.
When using the same address as the *first* PZEM task, and the **PZEM Model** connected is a ``PZEM-004Tv30 (AC)``, the extra values ``Power_Factor_cosphi`` and ``Frequency_Hz`` can be made available in a task, for sending to a controller.
|
Commands
--------
.. include:: P102_commands.repl
Change log
@@ -119,4 +339,6 @@ Change log
.. versionchanged:: 2.0
...
|added| 2025-08-06: Support for PZEM-017, updated documentation.
|added| 2020-04-25
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+15
View File
@@ -0,0 +1,15 @@
.. csv-table::
:header: "Command Syntax", "Extra information"
:widths: 20, 30
"
| ``ResetEnergy,<modbus_address>``
","
| This command will reset the Energy counter for the PZEM unit (value is stored in the unit). The value retrieved on the next read should return the updated value. (Can be 0 if no energy is flowing, or the energy counted since the reset command was executed.)
| This command can be executed by any task if multiple PZEM tasks are configured, as they all share the single MODBUS Serial configuration of the *first* PZEM task.
"
+2 -2
View File
@@ -5,8 +5,8 @@
.. |Plugin_Display| replace:: :ref:`P012_page`, :ref:`P023_page`, :ref:`P036_page`, :ref:`P057_page`, :ref:`P073_page`, :ref:`P075_page`, :ref:`P095_page`, :ref:`P104_page`, :ref:`P116_page`, :ref:`P131_page`, :ref:`P148_page`
.. |Plugin_Distance| replace:: :ref:`P013_page`, :ref:`P110_page`, :ref:`P113_page`, :ref:`P134_page`
.. |Plugin_Dust| replace:: :ref:`P018_page`, :ref:`P053_page`, :ref:`P056_page`, :ref:`P144_page`, :ref:`P175_page`
.. |Plugin_Energy_AC| replace:: :ref:`P076_page`, :ref:`P077_page`, :ref:`P078_page`, :ref:`P102_page`, :ref:`P108_page`
.. |Plugin_Energy_DC| replace:: :ref:`P027_page`, :ref:`P085_page`, :ref:`P115_page`, :ref:`P132_page`
.. |Plugin_Energy_AC| replace:: :ref:`P076_page`, :ref:`P077_page`, :ref:`P078_page`, :ref:`P092_page`, :ref:`P102_page`, :ref:`P108_page`, :ref:`P176_page`
.. |Plugin_Energy_DC| replace:: :ref:`P027_page`, :ref:`P085_page`, :ref:`P102_page`, :ref:`P115_page`, :ref:`P132_page`, :ref:`P176_page`
.. |Plugin_Energy_Heat| replace:: :ref:`P088_page`, :ref:`P093_page`
.. |Plugin_Environment| replace:: :ref:`P004_page`, :ref:`P005_page`, :ref:`P006_page`, :ref:`P014_page`, :ref:`P024_page`, :ref:`P028_page`, :ref:`P030_page`, :ref:`P031_page`, :ref:`P032_page`, :ref:`P034_page`, :ref:`P039_page`, :ref:`P047_page`, :ref:`P051_page`, :ref:`P068_page`, :ref:`P069_page`, :ref:`P072_page`, :ref:`P103_page`, :ref:`P105_page`, :ref:`P106_page`, :ref:`P122_page`, :ref:`P150_page`, :ref:`P151_page`, :ref:`P153_page`, :ref:`P154_page`, :ref:`P163_page`, :ref:`P167_page`, :ref:`P169_page`, :ref:`P172_page`, :ref:`P173_page`, :ref:`P177_page`
.. |Plugin_Extra_IO| replace:: :ref:`P011_page`, :ref:`P022_page`, :ref:`P178_page`
@@ -24,18 +24,18 @@
.. |P101_compileinfo| replace:: `.`
.. |P101_usedlibraries| replace:: https://github.com/a7md0/WakeOnLan
.. |P102_name| replace:: :cyan:`PZEM-004Tv30-Multiple`
.. |P102_type| replace:: :cyan:`Energy (AC)`
.. |P102_typename| replace:: :cyan:`Energy (AC) - PZEM-004Tv30-Multiple`
.. |P102_name| replace:: :cyan:`PZEM-004Tv30 / PZEM-017v1`
.. |P102_type| replace:: :cyan:`Energy`
.. |P102_typename| replace:: :cyan:`Energy - PZEM-004T (AC) / PZEM-017 (DC)`
.. |P102_porttype| replace:: `.`
.. |P102_status| replace:: :yellow:`ENERGY`
.. |P102_github| replace:: P102_PZEM004Tv3.ino
.. _P102_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P102_PZEM004Tv3.ino
.. |P102_usedby| replace:: `.`
.. |P102_shortinfo| replace:: `.`
.. |P102_maintainer| replace:: TD-er
.. |P102_maintainer| replace:: TD-er tonhuisman
.. |P102_compileinfo| replace:: `.`
.. |P102_usedlibraries| replace:: https://github.com/olehs/PZEM004T
.. |P102_usedlibraries| replace:: https://github.com/olehs/PZEM004T https://github.com/mandulaj/PZEM-004T-v30
.. |P103_name| replace:: :cyan:`Atlas Scientific EZO pH ORP EC DO HUM`
.. |P103_type| replace:: :cyan:`Environment`
+158 -42
View File
@@ -14,6 +14,10 @@
#define REG_PF 0x0008
#define REG_ALARM 0x0009
#define REG_CURRENT 0x0001
#define REG_HVALARM 0x0006
#define REG_LVALARM 0x0007
#define CMD_RHR 0x03
#define CMD_RIR 0X04
#define CMD_WSR 0x06
@@ -24,6 +28,9 @@
#define WREG_ALARM_THR 0x0001
#define WREG_ADDR 0x0002
#define WREG_HV_ALARM_THR 0x0000
#define WREG_LV_ALARM_THR 0x0001
#define UPDATE_TIME 200
#define RESPONSE_SIZE 32
@@ -84,6 +91,23 @@ PZEM004Tv30::~PZEM004Tv30()
// delete this->_serial;
}
void PZEM004Tv30::setModel(PZEM_model model) {
if (model == _model) {
return;
}
_model = model;
switch (_model) {
case PZEM_model::PZEM004Tv30:
_expectedResponse = PZEM004Tv30_EXPECTED_RESPONSE;
_requestRegisters = PZEM004Tv30_REQUEST_REGISTERS;
break;
case PZEM_model::PZEM017Tv1:
_expectedResponse = PZEM017v1_EXPECTED_RESPONSE;
_requestRegisters = PZEM017v1_REQUEST_REGISTERS;
break;
}
}
/*! * PZEM004Tv30::voltage *
* Get line voltage in Volts *
* @return current L-N volage*/
@@ -222,28 +246,97 @@ uint8_t PZEM004Tv30::getAddress()
* Set power alarm threshold in watts *
* @param[in] watts Alamr theshold *
* @return success*/
bool PZEM004Tv30::setPowerAlarm(uint16_t watts)
{
if (watts > 25000){ // Sanitych check
watts = 25000;
}
// bool PZEM004Tv30::setPowerAlarm(uint16_t watts)
// {
// if (watts > 25000){ // Sanitych check
// watts = 25000;
// }
// Write the watts threshold to the Alarm register
if(!sendCmd8(CMD_WSR, WREG_ALARM_THR, watts, true))
return false;
return true;
}
// // Write the watts threshold to the Alarm register
// if(!sendCmd8(CMD_WSR, WREG_ALARM_THR, watts, true))
// return false;
// return true;
// }
/*! * PZEM004Tv30::getPowerAlarm *
* Is the power alarm set * *
* @return arlam triggerd*/
bool PZEM004Tv30::getPowerAlarm()
{
if(!updateValues()) // Update vales if necessary
return NAN; // Update did not work, return NAN
// bool PZEM004Tv30::getPowerAlarm()
// {
// if(!updateValues()) // Update vales if necessary
// return NAN; // Update did not work, return NAN
// return _currentValues.alarms != 0x0000;
// }
/*!
* PZEM004Tv30::setHighVoltAlarm
* Set HV alarm threshold in volts
* @param[in] volt Alarm theshold
* @return success */
// bool PZEM004Tv30::setHighvoltAlarm(uint16_t volts)
// {
// if (volts < 500){ // Sanity check
// volts = 500;
// }
// if (volts > 34999){ // Sanity check
// volts = 34999;
// }
// // Write the volts threshold to the alarm register
// if(!sendCmd8(CMD_WSR, WREG_HV_ALARM_THR, volts, true))
// return false;
// return true;
// }
/*!
* PZEM004Tv30::setLowVoltAlarm
* Set LV alarm threshold in volts
* @param[in] volt Alarm theshold
* @return success */
// bool PZEM004Tv30::setLowvoltAlarm(uint16_t volts)
// {
// if (volts < 100){ // Sanity check
// volts = 100;
// }
// if (volts > 34999){ // Sanity check
// volts = 34999;
// }
// // Write the volts threshold to the alarm register
// if(!sendCmd8(CMD_WSR, WREG_LV_ALARM_THR, volts, true))
// return false;
// return true;
// }
/*!
* PZEM004Tv30::isHighVoltAlarmOn GET
* Is the HV alarm set
* @return alarm triggerd*/
// bool PZEM004Tv30::isHighvoltAlarmOn()
// {
// if(!updateValues()) // Update vales if necessary
// return NAN; // Update did not work, return NAN
// return _currentValues.HVAlarms != 0x0000;
// }
/*!
* PZEM004Tv30::isLowVoltAlarmOn GET
* Is the LV alarm set
* @return alarm triggerd*/
// bool PZEM004Tv30::isLowvoltAlarmOn()
// {
// if(!updateValues()) // Update vales if necessary
// return NAN; // Update did not work, return NAN
// return _currentValues.LVAlarms != 0x0000;
// }
return _currentValues.alarms != 0x0000;
}
/*! * PZEM004Tv30::init *
* initialization common to all consturctors *
@@ -266,52 +359,75 @@ void PZEM004Tv30::init(uint8_t addr){
* @return success*/
bool PZEM004Tv30::updateValues()
{
//static uint8_t buffer[] = {0x00, CMD_RIR, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00};
static uint8_t response[25];
// If we read before the update time limit, do not update
if(_lastRead + UPDATE_TIME > millis()){
return true;
}
// Read 10 registers starting at 0x00 (no check)
sendCmd8(CMD_RIR, 0x00, 0x0A, false);
// Read <model> registers starting at 0x00 (no check)
sendCmd8(CMD_RIR, 0x00, _requestRegisters, false);
if(recieve(response, 25) != 25){ // Something went wrong
if(recieve(_response, _expectedResponse) != _expectedResponse){ // Something went wrong
return false;
}
// Update the current values
_currentValues.voltage = ((uint32_t)response[3] << 8 | // Raw voltage in 0.1V
(uint32_t)response[4])/10.0;
switch(_model) {
case PZEM_model::PZEM004Tv30:
_currentValues.voltage = ((uint32_t)_response[3] << 8 | // Raw voltage in 0.1V
(uint32_t)_response[4]) / 10.0;
_currentValues.current = ((uint32_t)response[5] << 8 | // Raw current in 0.001A
(uint32_t)response[6] |
(uint32_t)response[7] << 24 |
(uint32_t)response[8] << 16) / 1000.0;
_currentValues.current = ((uint32_t)_response[5] << 8 | // Raw current in 0.001A
(uint32_t)_response[6] |
(uint32_t)_response[7] << 24 |
(uint32_t)_response[8] << 16) / 1000.0;
_currentValues.power = ((uint32_t)response[9] << 8 | // Raw power in 0.1W
(uint32_t)response[10] |
(uint32_t)response[11] << 24 |
(uint32_t)response[12] << 16) / 10.0;
_currentValues.power = ((uint32_t)_response[9] << 8 | // Raw power in 0.1W
(uint32_t)_response[10] |
(uint32_t)_response[11] << 24 |
(uint32_t)_response[12] << 16) / 10.0;
_currentValues.energy = ((uint32_t)response[13] << 8 | // Raw Energy in 1Wh
(uint32_t)response[14] |
(uint32_t)response[15] << 24 |
(uint32_t)response[16] << 16) / 1000.0;
_currentValues.energy = ((uint32_t)_response[13] << 8 | // Raw Energy in 1Wh
(uint32_t)_response[14] |
(uint32_t)_response[15] << 24 |
(uint32_t)_response[16] << 16) / 1000.0;
_currentValues.frequeny =((uint32_t)response[17] << 8 | // Raw Frequency in 0.1Hz
(uint32_t)response[18]) / 10.0;
_currentValues.frequeny =((uint32_t)_response[17] << 8 | // Raw Frequency in 0.1Hz
(uint32_t)_response[18]) / 10.0;
_currentValues.pf = ((uint32_t)response[19] << 8 | // Raw pf in 0.01
(uint32_t)response[20])/100.0;
_currentValues.pf = ((uint32_t)_response[19] << 8 | // Raw pf in 0.01
(uint32_t)_response[20]) / 100.0;
_currentValues.alarms = ((uint32_t)response[21] << 8 | // Raw alarm value
(uint32_t)response[22]);
_currentValues.alarms = ((uint32_t)_response[21] << 8 | // Raw alarm value
(uint32_t)_response[22]);
break;
case PZEM_model::PZEM017Tv1:
_currentValues.voltage = ((uint32_t)_response[3] << 8 | // Raw voltage in 0.01V
(uint32_t)_response[4]) / 100.0;
_currentValues.current = ((uint32_t)_response[5] << 8 | // Raw voltage in 0.01A
(uint32_t)_response[6]) / 100.0;
_currentValues.power = ((uint32_t)_response[7] << 8 | // Raw power in 0.1W
(uint32_t)_response[8] |
(uint32_t)_response[9] << 24 |
(uint32_t)_response[10] << 16) / 10.0;
_currentValues.energy = ((uint32_t)_response[11] << 8 | // Raw Energy in 1Wh
(uint32_t)_response[12] |
(uint32_t)_response[13] << 24 |
(uint32_t)_response[14] << 16) / 1000.0;
_currentValues.HVAlarms = ((uint32_t)_response[15] << 8 | // Raw alarm value
(uint32_t)_response[16]);
_currentValues.LVAlarms = ((uint32_t)_response[17] << 8 | // Raw alarm value
(uint32_t)_response[18]);
break;
}
// Record current time as _lastRead
_lastRead = millis();
+30 -5
View File
@@ -28,9 +28,20 @@
#include <ESPeasySerial.h>
#endif
#define PZEM004Tv30_EXPECTED_RESPONSE 25
#define PZEM004Tv30_REQUEST_REGISTERS 10
#define PZEM017v1_EXPECTED_RESPONSE 21
#define PZEM017v1_REQUEST_REGISTERS 8
#define PZEM_DEFAULT_ADDR 0xF8
#define PZEM_MAX_EXPECTED_RESPONSE 25 // PZEM004v30 needs this buffer size, adjust for models that need a bigger buffer
enum class PZEM_model : uint8_t {
PZEM004Tv30 = 0, // Buffer size 25, request 10 registers
PZEM017Tv1 = 1, // Buffer size 21, request 8 registers
};
class PZEM004Tv30
{
@@ -40,6 +51,7 @@ public:
#endif
~PZEM004Tv30();
void setModel(PZEM_model model); // Sets some communication buffer params
float voltage();
float current();
@@ -52,8 +64,14 @@ public:
bool setAddress(uint8_t addr);
uint8_t getAddress();
bool setPowerAlarm(uint16_t watts);
bool getPowerAlarm();
// Unused methods:
// bool setPowerAlarm(uint16_t watts);
// bool getPowerAlarm();
// bool setHighvoltAlarm(uint16_t volts); //moja uprava
// bool setLowvoltAlarm(uint16_t volts); //moja uprava
// bool isHighvoltAlarmOn(); //upravil som
// bool isLowvoltAlarmOn(); //upravil som
bool resetEnergy();
void init(uint8_t addr); // Init common to all constructors
@@ -64,15 +82,22 @@ private:
bool _isSoft; // Is serial interface software
uint8_t _addr; // Device address
PZEM_model _model = PZEM_model::PZEM004Tv30; // Current default
uint8_t _response[PZEM_MAX_EXPECTED_RESPONSE];
uint8_t _expectedResponse = PZEM004Tv30_EXPECTED_RESPONSE;
uint8_t _requestRegisters = PZEM004Tv30_REQUEST_REGISTERS;
struct {
float voltage;
float current;
float power;
float energy;
float frequeny;
float pf;
uint16_t alarms;
float frequeny; // PZEM004
float pf; // PZEM004
uint16_t alarms; // PZEM004
uint16_t HVAlarms; //upravil som PZEM017
uint16_t LVAlarms; //upravil sOM PZEM017
} _currentValues; // Measured values
uint64_t _lastRead; // Last time values were updated
+78 -38
View File
@@ -3,11 +3,15 @@
#ifdef USES_P102
// #######################################################################################################
// #################################### Plugin 102: PZEM004T v30 with modbus mgt##########################
// ######################## Plugin 102: PZEM004Tv30 / PZEM-017v1 with modbus mgt ########################
// #######################################################################################################
//
/** Changelog:
* 2025-09-14 tonhuisman: Add extra instruction to submit page once more after 'Reset energy' is used, to save 'Read energy' default Mode.
* 2025-08-06 tonhuisman: Change plugin name to "Energy - PZEM-004T (AC) / PZEM-017 (DC)"
* 2025-08-05 tonhuisman: Add support for PZEM-017v1 from a forum suggestion:
* https://www.letscontrolit.com/forum/viewtopic.php?p=74069#p74064
* 2025-01-17 tonhuisman: Implement support for MQTT AutoDiscovery (partially)
* 2025-01-12 tonhuisman: Add support for MQTT AutoDiscovery (not supported yet for PZEM00x)
*/
@@ -21,7 +25,7 @@
# define PLUGIN_102
# define PLUGIN_ID_102 102
# define PLUGIN_102_DEBUG true // activate extra log info in the debug
# define PLUGIN_NAME_102 "Energy (AC) - PZEM-004Tv30-Multiple"
# define PLUGIN_NAME_102 "Energy - PZEM-004T (AC) / PZEM-017 (DC)"
# define P102_PZEM_mode PCONFIG(1) // 0=read value ; 1=reset energy; 2=programm address
# define P102_PZEM_ADDR PCONFIG(2)
@@ -32,6 +36,7 @@
# define P102_QUERY4 PCONFIG(6)
# define P102_PZEM_FIRST PCONFIG(7)
# define P102_PZEM_ATTEMPT PCONFIG_LONG(1)
# define P102_PZEM_TYPE PCONFIG_LONG(0)
# define P102_PZEM_mode_DFLT 0 // Read value
# define P102_QUERY1_DFLT 0 // Voltage (V)
@@ -44,6 +49,10 @@
# define P102_PZEM_MAX_ATTEMPT 3 // Number of tentative before declaring NAN value
# define P102_PZEM004_VALUE_COUNT 6
# define P102_PZEM017_VALUE_COUNT 4
PZEM004Tv30 * P102_PZEM_sensor = nullptr;
boolean Plugin_102_init = false;
@@ -56,7 +65,7 @@ const __FlashStringHelper* p102_getQueryString(uint8_t query);
boolean Plugin_102(uint8_t function, struct EventStruct *event, String& string)
{
boolean success = false;
bool success = false;
switch (function)
{
@@ -127,6 +136,7 @@ boolean Plugin_102(uint8_t function, struct EventStruct *even
case PLUGIN_WEBFORM_SHOW_CONFIG:
{
string += serialHelper_getSerialTypeLabel(event);
string += strformat(F("<BR>addr: 0x%02x"), P102_PZEM_ADDR); // Show modbus address
success = true;
break;
}
@@ -135,14 +145,17 @@ boolean Plugin_102(uint8_t function, struct EventStruct *even
{
// To select the data in the 4 fields.
const __FlashStringHelper *options[P102_NR_OUTPUT_OPTIONS];
const uint8_t nrOptions = PZEM_model::PZEM004Tv30 == static_cast<PZEM_model>(P102_PZEM_TYPE)
? P102_PZEM004_VALUE_COUNT
: P102_PZEM017_VALUE_COUNT;
for (uint8_t i = 0; i < P102_NR_OUTPUT_OPTIONS; ++i) {
for (uint8_t i = 0; i < nrOptions; ++i) {
options[i] = p102_getQueryString(i);
}
for (uint8_t i = 0; i < P102_NR_OUTPUT_VALUES; ++i) {
const uint8_t pconfigIndex = i + P102_QUERY1_CONFIG_POS;
sensorTypeHelper_loadOutputSelector(event, pconfigIndex, i, P102_NR_OUTPUT_OPTIONS, options);
sensorTypeHelper_loadOutputSelector(event, pconfigIndex, i, nrOptions, options);
}
success = true;
@@ -150,7 +163,19 @@ boolean Plugin_102(uint8_t function, struct EventStruct *even
break;
}
case PLUGIN_WEBFORM_LOAD: {
case PLUGIN_WEBFORM_LOAD:
{
{
const __FlashStringHelper*pzemModels[] = {
F("PZEM-004Tv30 (AC)"),
F("PZEM-017v1 (DC)"),
};
constexpr int pzemCount = NR_ELEMENTS(pzemModels);
FormSelectorOptions selector(pzemCount, pzemModels);
selector.reloadonchange = true;
selector.addFormSelector(F("PZEM Model"), F("pztype"), P102_PZEM_TYPE);
}
if (P102_PZEM_sensor == nullptr) { P102_PZEM_FIRST = event->TaskIndex; // To detect if first PZEM or not
}
@@ -161,7 +186,7 @@ boolean Plugin_102(uint8_t function, struct EventStruct *even
"don't use HW serial (or invert Tx and Rx to configure as SW serial).</B></span>"));
addFormSubHeader(F("PZEM actions"));
{
const __FlashStringHelper *options_model[] = { F("Read_value"), F("Reset_Energy"), F("Program_adress") };
const __FlashStringHelper *options_model[] = { F("Read value"), F("Reset Energy"), F("Program address") };
constexpr size_t optionCount = NR_ELEMENTS(options_model);
const FormSelectorOptions selector(optionCount, options_model);
selector.addFormSelector(F("PZEM Mode"), F("PZEM_mode"), P102_PZEM_mode);
@@ -169,8 +194,8 @@ boolean Plugin_102(uint8_t function, struct EventStruct *even
if (P102_PZEM_mode == 2)
{
addHtml(F("<span style=\"color:red\"> <br>When programming an address, only one PZEMv30 must be connected. "
"Otherwise, all connected PZEMv30s will get the same address, which would cause a conflict during reading.</span>"));
addHtml(F("<span style=\"color:red\"> <br>When programming an address, only one PZEM must be connected. "
"Otherwise, all connected PZEMs will get the same address, which would cause a conflict during reading.</span>"));
{
const __FlashStringHelper *options_confirm[] = { F("NO"), F("YES") };
constexpr size_t optionCount = NR_ELEMENTS(options_confirm);
@@ -182,13 +207,13 @@ boolean Plugin_102(uint8_t function, struct EventStruct *even
}
else
{
addFormNumericBox(F("Address of PZEM"), F("PZEM_addr"), P102_PZEM_ADDR, 0, 247);
addHtml(F(" Address 0 allows to communicate with any <B>single</B> PZEMv30 whatever its address"));
addFormNumericBox(F("Address of PZEM"), F("PZEM_addr"), P102_PZEM_ADDR, 0x00, 0xF7);
addHtml(F("&nbsp;Address 0 allows to communicate with any <B>single</B> PZEM whatever its address"));
}
if (P102_PZEM_ADDR_SET == 3) // If address programming done
{
addHtml(F("<span style=\"color:green\"> <br><B>Address programming done ! </B></span>"));
addHtml(F("<span style=\"color:green\"><br><B>Address programming done ! </B></span>"));
P102_PZEM_ADDR_SET = 0; // Reset programming confirmation
}
}
@@ -196,28 +221,30 @@ boolean Plugin_102(uint8_t function, struct EventStruct *even
{
addFormSubHeader(F("PZEM actions"));
{
const __FlashStringHelper *options_model[] = { F("Read_value"), F("Reset_Energy") };
const __FlashStringHelper *options_model[] = { F("Read value"), F("Reset Energy") };
constexpr size_t optionCount = NR_ELEMENTS(options_model);
const FormSelectorOptions selector(optionCount, options_model);
selector.addFormSelector(F("PZEM Mode"), F("PZEM_mode"), P102_PZEM_mode);
}
addHtml(F(" Tx/Rx Pins config disabled: Configuration is available in the first PZEM plugin.<br>"));
addFormNumericBox(F("Address of PZEM"), F("PZEM_addr"), P102_PZEM_ADDR, 1, 247);
addHtml(F("&nbsp;Tx/Rx Pins config ignored: Configuration is available in the first PZEM plugin.<br>"));
addFormNumericBox(F("Address of PZEM"), F("PZEM_addr"), P102_PZEM_ADDR, 0x01, 0xF7);
}
addHtml(F("<br><br> Reset energy can be done also by: http://*espeasyip*/control?cmd=resetenergy,*PZEM address*"));
if (P102_PZEM_ADDR_SET == 4)
{
addHtml(F("<span style=\"color:blue\"> <br><B>Energy reset on current PZEM ! </B></span>"));
addHtml(F("<span style=\"color:red\"> <br>Click <B>Submit</B> button once more to save 'Read value' PZEM Mode !</span>"));
P102_PZEM_ADDR_SET = 0; // Reset programming confirmation
}
addFormNote(F("Reset Energy can also be done by: http://*espeasyip*/control?cmd=resetenergy,*PZEM address*"));
success = true;
break;
}
case PLUGIN_WEBFORM_SAVE: {
case PLUGIN_WEBFORM_SAVE:
{
serialHelper_webformSave(event);
// Save output selector parameters.
@@ -226,6 +253,7 @@ boolean Plugin_102(uint8_t function, struct EventStruct *even
const uint8_t choice = PCONFIG(pconfigIndex);
sensorTypeHelper_saveOutputSelector(event, pconfigIndex, i, p102_getQueryString(choice));
}
P102_PZEM_TYPE = getFormItemInt(F("pztype"));
P102_PZEM_mode = getFormItemInt(F("PZEM_mode"));
P102_PZEM_ADDR = getFormItemInt(F("PZEM_addr"));
P102_PZEM_ADDR_SET = getFormItemInt(F("PZEM_addr_set"));
@@ -263,6 +291,7 @@ boolean Plugin_102(uint8_t function, struct EventStruct *even
if (P102_PZEM_sensor != nullptr) {
P102_PZEM_sensor->init(P102_PZEM_ADDR);
P102_PZEM_sensor->setModel(static_cast<PZEM_model>(P102_PZEM_TYPE));
// Sequence for reseting PZEM energy
if (P102_PZEM_mode == 1)
@@ -297,6 +326,7 @@ boolean Plugin_102(uint8_t function, struct EventStruct *even
{
// When new data is available, return true
P102_PZEM_sensor->init(P102_PZEM_ADDR);
P102_PZEM_sensor->setModel(static_cast<PZEM_model>(P102_PZEM_TYPE));
float PZEM[6];
PZEM[0] = P102_PZEM_sensor->voltage();
@@ -306,12 +336,17 @@ boolean Plugin_102(uint8_t function, struct EventStruct *even
PZEM[4] = P102_PZEM_sensor->pf();
PZEM[5] = P102_PZEM_sensor->frequency();
for (uint8_t i = 0; i < 6; i++) // Check each PZEM field
// Skip non-available values
const uint8_t toCheckMax = PZEM_model::PZEM004Tv30 == static_cast<PZEM_model>(P102_PZEM_TYPE)
? P102_PZEM004_VALUE_COUNT
: P102_PZEM017_VALUE_COUNT;
for (uint8_t i = 0; i < toCheckMax; i++) // Check each PZEM field
{
if (PZEM[i] != PZEM[i]) // Check if NAN
if (PZEM[i] != PZEM[i]) // Check if NAN
{
P102_PZEM_ATTEMPT == P102_PZEM_MAX_ATTEMPT ? P102_PZEM_ATTEMPT = 0 : P102_PZEM_ATTEMPT++;
break; // if one is Not A Number, break
break; // if one is Not A Number, break
}
P102_PZEM_ATTEMPT = 0;
}
@@ -333,10 +368,14 @@ boolean Plugin_102(uint8_t function, struct EventStruct *even
# if FEATURE_PACKED_RAW_DATA
case PLUGIN_GET_PACKED_RAW_DATA:
{
// Matching JS code:
// Matching JS code for PZEM-004T:
// return decode(bytes, [header, int16_1e1, int32_1e3, int32_1e1, int32_1e1, uint16_1e2, uint8_1e1],
// ['header', 'voltage', 'current', 'power', 'energy', 'powerfactor', 'frequency']);
//
// Matching JS code for PZEM-017:
// return decode(bytes, [header, int16_1e1, int32_1e3, int32_1e1, int32_1e1],
// ['header', 'voltage', 'current', 'power', 'energy']);
//
// Resolutions:
// Voltage: 0.1V => int16_1e1 (range 80-260V)
// Current: 0.001A => int32_1e3
@@ -347,13 +386,18 @@ boolean Plugin_102(uint8_t function, struct EventStruct *even
// FIXME TD-er: Calling these functions is probably done within the 200 msec timeout used in the library.
// If not, this should be cached in a task data struct.
string += LoRa_addFloat(P102_PZEM_sensor->voltage(), PackedData_int16_1e1);
string += LoRa_addFloat(P102_PZEM_sensor->current(), PackedData_int32_1e3);
string += LoRa_addFloat(P102_PZEM_sensor->power(), PackedData_int32_1e1);
string += LoRa_addFloat(P102_PZEM_sensor->energy(), PackedData_int32_1e1);
string += LoRa_addFloat(P102_PZEM_sensor->pf(), PackedData_uint16_1e2);
string += LoRa_addFloat(P102_PZEM_sensor->frequency() - 40, PackedData_uint8_1e1);
event->Par1 = 6; // valuecount
string += LoRa_addFloat(P102_PZEM_sensor->voltage(), PackedData_int16_1e1);
string += LoRa_addFloat(P102_PZEM_sensor->current(), PackedData_int32_1e3);
string += LoRa_addFloat(P102_PZEM_sensor->power(), PackedData_int32_1e1);
string += LoRa_addFloat(P102_PZEM_sensor->energy(), PackedData_int32_1e1);
if (PZEM_model::PZEM004Tv30 == static_cast<PZEM_model>(P102_PZEM_TYPE)) {
string += LoRa_addFloat(P102_PZEM_sensor->pf(), PackedData_uint16_1e2);
string += LoRa_addFloat(P102_PZEM_sensor->frequency() - 40, PackedData_uint8_1e1);
event->Par1 = P102_PZEM004_VALUE_COUNT; // valuecount
} else {
event->Par1 = P102_PZEM017_VALUE_COUNT; // valuecount
}
success = true;
break;
@@ -367,14 +411,10 @@ boolean Plugin_102(uint8_t function, struct EventStruct *even
{
const String command = parseString(string, 1);
if ((equals(command, F("resetenergy"))) && (P102_PZEM_FIRST == event->TaskIndex))
{
if ((event->Par1 >= 0) && (event->Par1 <= 247))
{
P102_PZEM_sensor->init(event->Par1);
P102_PZEM_sensor->resetEnergy();
success = true;
}
if (equals(command, F("resetenergy")) && (event->Par1 >= 0) && (event->Par1 <= 247)) {
P102_PZEM_sensor->init(event->Par1);
P102_PZEM_sensor->resetEnergy();
success = true;
}
}
@@ -392,7 +432,7 @@ const __FlashStringHelper* p102_getQueryString(uint8_t query) {
case 2: return F("Power_W");
case 3: return F("Energy_kWh");
case 4: return F("Power_Factor_cosphi");
case 5: return F("Frequency Hz");
case 5: return F("Frequency_Hz");
}
return F("");
}