PZEM-016 single phase modbus energy meter

Python V3?

Not sure what you mean here…

@NA7KR
Have you searched here for just “PZEM”?

I added PZEM-016 support to RS485modbus. The device seems to be accurate, I tested it with a kmart 40cm pedestal fan.

pzem

I didn’t think of optoisolation issues like dBC did, good point, there is an isolated RS485 to USB available. You just need to read 25 bytes from the PZEM-016 and transcribe 6 of those bytes into 3 hex values into values.

The Windows source code is available on sourceforge. This is an open source hobby of mine. You’ll need to customize this code for whatever purpose you have in mind. The Chinese make all these RS485 gadgets, but no one makes software to go with it. If you wanna help, email me and confirm the software works.

PB66 and I noticed the same thing. NOT bad for a “10 buck” device!

I had to get 3 of them, cos we have 3 phase. It’s useful to know how much power you’.re using. The manual says it’s optoisolated.

Some performance data comparing a PZEM-016 to an Elkor WattsOn:

Hi, my name is daniel, someone knows how to reset the energy counter using modbus commands, i saw in the second page instructions and try to use it, but i cant do it, i dont know if it have a especial notation or i cant interpret it

Welcome to OEM Dan,

Here’s a Python script I use to read my PZEM-016.
One section of that script contains a definition of the reset command.
The script uses a Python module called minimalmodbus as well as a few others.
Depending on what you want to do, you may or may not need anything other than minimalmodbus.

pzem-016.zip (874 Bytes)


Here’s another script that contains only the code that performs the reset.
Like the first script, this one also uses minimalmodbus.

pzreset.zip (287 Bytes)


I use an RS-485 to USB adapter to read the PZEM-016. One similar to this:
https://www.ebay.com/itm/Adapter-Module-CH340-USB-to-RS485-For-Win7-Linux-Vista-XP-Converter-Raspberry-c/323980605150?epid=5027448925&hash=item4b6ebfb6de:g:~JoAAOSwbYZXciE9

Hi Bill, thankyou for your answer, i saw your code but I didn’t run it, because i cant undestand how the reset energy is activated, al commands in code are only read_registers, and if we want to reset the energy counter , its not could be a write command?

I am grateful with you for your help, if you need someting you can ask me, greetings from colombia

I share you the code of file that you share me and a photo of instruction manual that i cannot be solve, the section is 2.5 reset energy:

and code that you send me:

import logging, minimalmodbus
from apscheduler.scheduler import Scheduler #Advanced Python Scheduler v2.12
from time import sleep

pz = minimalmodbus.Instrument("/dev/ttyUSB0", 1)
pz.serial.timeout = 0.1 # had to bump this up from the default of 0.05
pz.serial.baudrate = 9600
logging.basicConfig()
sched = Scheduler()
sched.start()

def read_meter():
    VOLT = pz.read_register(0, 0, 4)
    AMPS = pz.read_register(1, 0, 4)
    WATT = pz.read_register(3, 0, 4)
    WHRS = pz.read_register(5, 0, 4)
    FREQ = pz.read_register(7, 0, 4)
    PWRF = pz.read_register(8, 0, 4)
    print VOLT * 0.1
    print AMPS * 0.001
    print WATT * 0.1
    print WHRS
    print FREQ * 0.1
    print PWRF * 0.01
    print

def main():
    sched.add_cron_job(read_meter, second='*/1')

    while True:
       sleep(1)

if __name__ == "__main__":
    main()

I’m not sure what happened, but the code in your post doesn’t look like the code
in the zip file I attached to my post.

Here’s a code snippet that performs only the reset function:

#!/usr/bin/python

import minimalmodbus

pz = minimalmodbus.Instrument('/dev/ttyUSB0', 1)
pz.serial.baudrate = 9600

pz._performCommand(66, '')

You’ll need Python, the minimalmodbus module, and an RS-485 interface to enable your computer to talk to your PZEM-016.

The RS-485 interface I use is a USB device like the one in the link two posts above this one.
More info on minimalmodbus here: Installation — MinimalModbus 2.0.1 documentation

I finally got a chance to play around with my PZEM-016. FYI - I am getting a fairly consistent 20% higher current reading compared with my other meters. UNI-T UT804 (direct measurement through leads), UT210E (clamp meter), Fluke T5-600 (jaw clamp meter). I might have mixed up the CT from some similar toys that came from Peacefair.
Has anyone else checked the current to see how accurate it is?
Since it talks over modbus obviously I could apply my own scaling factor in software instead of dealing with the calibration process so it’s not the end of the world to me.
Surprisingly it was able to measure down to the lowest current that I tried at 3mA.

Check post 133 in this thread. I posted a graph comparing my PZEM-016 to an Elkor WattsOn
Watthour meter. You have to look closely, as the green trace (PZEM) overlays the red trace
(WattsOn) almost to the point of hiding it completely.

The 016 is quite accurate considering the cost. I’ve had one for a bit over a year
and find that it tracks my WattsOn as well as a Continental Control Systems WattNode kWh meter
quite closely.

For a different way of communicating with the PZEM-016, here’s how to do it from C.
First, install libmodbus:

sudo apt-get install libmodbus5
sudo apt-get install libmodbus-dev

The following will read the PZEM data using libmodbus:

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <modbus.h>

#define DOUBLEWORD	0x01
#define DIV10		0x02
#define DIV100		0x04
#define DIV1000		0x08
#define END			0xFF

int main( void )
	{
	static const char *configDeviceName = "/dev/ttyUSB0";
	static const int configBaudRate = 9600;
	static const int configSlaveAddress = 1;
	static const int modbusResultInfo[] = {
		DIV10,					/* Voltage in 0.1V */
		DOUBLEWORD | DIV1000,	/* Current in 0.001A */
		DOUBLEWORD | DIV10,		/* Power in 0.1W */
		DOUBLEWORD,				/* Energy in 1Wh */
		DIV10,					/* Frequency in 0.1Hz */
		DIV100,					/* Power factor in 0.01 */
		END
		};
	modbus_t *modbusCTX;
	uint16_t modbusResults[ 9 ];
	int i, resultIndex = 0, status;

	/* Create a libmodbus context and connect to the remote device */
	modbusCTX = modbus_new_rtu( configDeviceName, configBaudRate,
								'N', 8, 1 );
	if( modbusCTX == NULL )
		{
	    fprintf( stderr, "Couldn't create libmodbus context for %s.\n",
	    		 configDeviceName );
	    return( EXIT_FAILURE );
		}
	if( modbus_connect( modbusCTX ) == -1 )
		{
		fprintf( stderr, "modbus connect failed for %s: %s.\n",
				 configDeviceName, modbus_strerror( errno ) );
		modbus_free( modbusCTX );
		return( EXIT_FAILURE );
		}

	/* Set the slave ID of the device to connect to.  A value of 0x01 seems
	   to work, there's a command to set it to a value 0x01...0xF7 but we
	   don't touch that */
	modbus_set_slave( modbusCTX, configSlaveAddress );

	/* Let the user know that we've established a connection */
	printf( "Connected to %s at %d 8N1, slave ID %d.\n", configDeviceName,
			configBaudRate, configSlaveAddress );

	/* Try and read the first 9 registers, which is all the PZEM data
	   except the alarm status */
	if( modbus_read_input_registers( modbusCTX, 0, 9, modbusResults ) == -1 )
		{
		fprintf( stderr, "modbus register read failed for slave ID %d: %s.\n",
				 configSlaveAddress, modbus_strerror( errno ) );
		modbus_free( modbusCTX );
		return( EXIT_FAILURE );
		}

	/* Display the results */
	for( i = 0; modbusResultInfo[ i ] != END; i++ )
		{
		const int modbusInfo = modbusResultInfo[ i ];
		uint32_t value = modbusResults[ resultIndex++ ];
		float result, divisor;

		/* Get the high word if there is one */
		if( modbusInfo & DOUBLEWORD )
			value |= ( uint32_t ) modbusResults[ resultIndex ++ ] << 16;

		/* Scale the value to get the actual measurement */
		divisor = ( modbusInfo & DIV1000 ) ? 1000 : \
				  ( modbusInfo & DIV100 ) ? 100 : 10;
		result = value / divisor;

		printf( "%d %.2f\n", value, result );
		}
	printf( "\n" );

	modbus_free( modbusCTX );

	return( EXIT_SUCCESS );
	}

Finally, to build it:

cc pzem16.c -o pzem16 pkg-config --cflags --libs libmodbus`

That’s just some test code to dump the output, depending on how useful people find it it can be adapted to take command-line args for e.g. the port to use, and something for output formatting, I’m going to be feeding it to Thingspeak so I’ll probably add something to handle that.

Slight update, this is a bit of an improved version that outputs in a format where it can be submitted directly to Thingspeak via wget, so just:

wget -q "https://api.thingspeak.com/update?api_key=XXXXX./pzem16"

All the instructions are now present as code comments. Next update will probably be to add output for IFTTT, since Thingspeak’s options for handling alarm conditions are pretty awkward to deal with.

/* PZEM-16 read code, currently set up to output for Thingspeak use.  You
   can use this code in whatever way you want, as long as you don't try to
   claim you wrote it.

   To install libmodbus:

	sudo apt-get install libmodbus5
	sudo apt-get install libmodbus-dev

   To build the code:

	gcc pzem16.c -o pzem16 `pkg-config --cflags --libs libmodbus`

   To post a single set of results to Thingspeak:

	wget -q "https://api.thingspeak.com/update?api_key=XXXXX`./pzem16`"

   To post on an ongoing basis, once a minute:

	#!/bin/sh

	while sleep 59 ;
	do
		wget -q "https://api.thingspeak.com/update?api_key=XXXXX`./pzem16`"
	done

   Command-line options:

	-d			Print debug output instead of Thingspeak output
	-u<number>	Set USB device to /dev/ttyUSBx, default is 0 */

#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include <modbus.h>

/* Useful defines */

#ifndef TRUE
  #define FALSE	0
  #define TRUE	( !FALSE )
#endif /* TRUE */

/* Flags that control how to interpret the modbus read results */

#define DOUBLEWORD	0x01
#define DIV10		0x02
#define DIV100		0x04
#define DIV1000		0x08
#define END			0xFF

int main( int argc, char *argv[] )
	{
	static const char *configDeviceNameTemplate = "/dev/ttyUSB%d";
	static const int configBaudRate = 9600;
	static const int configSlaveAddress = 1;
	static const int modbusResultInfo[] = {
		DIV10,					/* Voltage in 0.1V */
		DOUBLEWORD | DIV1000,	/* Current in 0.001A */
		DOUBLEWORD | DIV10,		/* Power in 0.1W */
		DOUBLEWORD,				/* Energy in 1Wh */
		DIV10,					/* Frequency in 0.1Hz */
		DIV100,					/* Power factor in 0.01 */
		END
		};
	static const char *thingspeakFieldNames[] = {
		"field1", NULL, NULL, NULL, "field2", NULL, NULL
		};
	modbus_t *modbusCTX;
	uint16_t modbusResults[ 9 ];
	char deviceName[ 64 ];
	int moreArgs = TRUE, debugOutput = FALSE, usbDeviceNo = 0;
	int i, resultIndex = 0, status;

	/* Skip the program name */
	argv++; argc--;

	/* Check for arguments */
	while( argc && *argv[ 0 ] == '-' && moreArgs )
		{
		char *argPtr = argv[ 0 ] + 1;

		while( *argPtr )
			{
			switch( toupper( *argPtr ) )
				{
				case '-':
					moreArgs = FALSE;	/* GNU-style end-of-args flag */
					break;

				case 'D':
					debugOutput = TRUE;
					break;

				case 'U':
					usbDeviceNo = atoi( argPtr + 1 );
					if( usbDeviceNo < 0 || usbDeviceNo > 20 )
						{
						fprintf( stderr, "Invalid USB device "
								 "number.\n" );
						exit( EXIT_FAILURE );
						}
					while( argPtr[ 1 ] )
						argPtr++;	/* Skip rest of arg */
					break;

				default:
					fprintf( stderr, "Unknown argument '%c'.\n", *argPtr );
					return( EXIT_FAILURE );
				}
			argPtr++;
			}

		argv++;
		argc--;
		}
	sprintf( deviceName, configDeviceNameTemplate, usbDeviceNo );

	/* Create a libmodbus context and connect to the remote device */
	modbusCTX = modbus_new_rtu( deviceName, configBaudRate, 'N', 8, 1 );
	if( modbusCTX == NULL )
		{
	    fprintf( stderr, "Couldn't create libmodbus context for %s.\n",
	    		 deviceName );
	    return( EXIT_FAILURE );
		}
	if( modbus_connect( modbusCTX ) == -1 )
		{
		fprintf( stderr, "modbus connect failed for %s: %s.\n",
				 deviceName, modbus_strerror( errno ) );
		modbus_free( modbusCTX );
		return( EXIT_FAILURE );
		}

	/* Set the slave ID of the device to connect to.  A value of 0x01 seems
	   to work, there's a PZEM command to change it to a value 0x01...0xF7
	   but we don't touch that */
	modbus_set_slave( modbusCTX, configSlaveAddress );

	/* If we're providing debug output, let the user know that we've
	   established a connection */
	if( debugOutput )
		{
		printf( "Connected to %s at %d 8N1, slave ID %d.\n",
				deviceName, configBaudRate, configSlaveAddress );
		}

	/* Try and read the first 9 registers, which is all the PZEM data
	   except the alarm status */
	if( modbus_read_input_registers( modbusCTX, 0, 9, modbusResults ) == -1 )
		{
		fprintf( stderr, "modbus register read failed for slave ID %d: %s.\n",
				 configSlaveAddress, modbus_strerror( errno ) );
		modbus_free( modbusCTX );
		return( EXIT_FAILURE );
		}

	/* Output the results */
	for( i = 0; modbusResultInfo[ i ] != END; i++ )
		{
		const int modbusInfo = modbusResultInfo[ i ];
		uint32_t value = modbusResults[ resultIndex++ ];
		float result, divisor;

		/* Get the high word if there is one */
		if( modbusInfo & DOUBLEWORD )
			value |= ( uint32_t ) modbusResults[ resultIndex ++ ] << 16;

		/* Scale the value to get the actual measurement */
		divisor = ( modbusInfo & DIV1000 ) ? 1000 : \
				  ( modbusInfo & DIV100 ) ? 100 : 10;
		result = value / divisor;

		if( debugOutput )
			printf( "%d %.2f\n", value, result );
		else
			{
			if( thingspeakFieldNames[ i ] != NULL )
				printf( "&%s=%.2f", thingspeakFieldNames[ i ], result );
			}
		}

	modbus_free( modbusCTX );

	return( EXIT_SUCCESS );
	}

Final version, now with IFTTT support for processing over/undervoltage alarms, see the shell script in the comment at the start.

/* PZEM-16 read code, currently set up to output for Thingspeak use.  You
   can use this code in whatever way you want, as long as you don't try to
   claim you wrote it.

   To install libmodbus:

	sudo apt-get install libmodbus5
	sudo apt-get install libmodbus-dev

   To build the code:

	gcc pzem16.c -o pzem16 `pkg-config --cflags --libs libmodbus`

   To post a single set of results to Thingspeak:

	wget -O/dev/null -q "https://api.thingspeak.com/update?api_key=XXXXX`./pzem16`"

   To trigger an IFTTT event, in this case based on voltage:

	wget -O/dev/null -q "https://maker.ifttt.com/trigger/voltage_alarm/with/key/XXXX?value1='./pzem16 -v'"

   To post on an ongoing basis, once a minute, as well as checking for over/
   undervoltages:

	#!/bin/sh
	#
	# Usage: pzem16 thingspeak_key ifttt_key

	if [ $# -ne 2 ] ; then
		echo "Usage: $0 thingspeak_key ifttt_key" >&2 ;
		exit 1 ;
	fi

	THINGSPEAK_KEY=$1
	IFTTT_KEY=$2
	MIN_VOLTAGE=230
	MAX_VOLTAGE=250

	while sleep 59 ;
	do
		OUTPUT=$(./pzem16)
		if [ $? -ne 0 ] ; then
			continue
		fi
		wget -O/dev/null -q "https://api.thingspeak.com/update?api_key=$THINGSPEAK_KEY$OUTPUT"
		OUTPUT=$(./pzem16 -v)
		if [ $OUTPUT -lt $MIN_VOLTAGE ] || [ $OUTPUT -gt $MAX_VOLTAGE ] ; then
			wget -O/dev/null -q "https://maker.ifttt.com/trigger/voltage_alarm/with/key/$IFTTT_KEY?value1=$OUTPUT"
		fi
	done

   Note that this has no rate-limiting, it'll keep triggering alerts for as
   long as the alarm condition is present.

   Command-line options:

	-d			Print debug output instead of Thingspeak output
	-u<number>	Set USB device to /dev/ttyUSBx, default is 0
	-v			Print voltage as an integer value */

#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include <modbus.h>

/* Useful defines */

#ifndef TRUE
  #define FALSE	0
  #define TRUE	( !FALSE )
#endif /* TRUE */

/* Flags that control how to interpret the modbus read results */

#define DOUBLEWORD	0x01
#define DIV10		0x02
#define DIV100		0x04
#define DIV1000		0x08
#define END			0xFF

int main( int argc, char *argv[] )
	{
	static const char *configDeviceNameTemplate = "/dev/ttyUSB%d";
	static const int configBaudRate = 9600;
	static const int configSlaveAddress = 1;
	static const int modbusResultInfo[] = {
		DIV10,					/* Voltage in 0.1V */
		DOUBLEWORD | DIV1000,	/* Current in 0.001A */
		DOUBLEWORD | DIV10,		/* Power in 0.1W */
		DOUBLEWORD,				/* Energy in 1Wh */
		DIV10,					/* Frequency in 0.1Hz */
		DIV100,					/* Power factor in 0.01 */
		END
		};
	static const char *thingspeakFieldNames[] = {
		"field1", NULL, NULL, NULL, "field2", NULL, NULL
		};
	modbus_t *modbusCTX;
	uint16_t modbusResults[ 9 ];
	char deviceName[ 64 ];
	int moreArgs = TRUE, debugOutput = FALSE, showVoltage = FALSE;
	int usbDeviceNo = 0, i, resultIndex = 0, status;

	/* Skip the program name */
	argv++; argc--;

	/* Check for arguments */
	while( argc && *argv[ 0 ] == '-' && moreArgs )
		{
		char *argPtr = argv[ 0 ] + 1;

		while( *argPtr )
			{
			switch( toupper( *argPtr ) )
				{
				case '-':
					moreArgs = FALSE;	/* GNU-style end-of-args flag */
					break;

				case 'D':
					debugOutput = TRUE;
					break;

				case 'U':
					usbDeviceNo = atoi( argPtr + 1 );
					if( usbDeviceNo < 0 || usbDeviceNo > 20 )
						{
						fprintf( stderr, "Invalid USB device "
								 "number.\n" );
						exit( EXIT_FAILURE );
						}
					while( argPtr[ 1 ] )
						argPtr++;	/* Skip rest of arg */
					break;

				case 'V':
					showVoltage = TRUE;
					break;

				default:
					fprintf( stderr, "Unknown argument '%c'.\n", *argPtr );
					return( EXIT_FAILURE );
				}
			argPtr++;
			}

		argv++;
		argc--;
		}
	sprintf( deviceName, configDeviceNameTemplate, usbDeviceNo );

	/* Create a libmodbus context and connect to the remote device */
	modbusCTX = modbus_new_rtu( deviceName, configBaudRate, 'N', 8, 1 );
	if( modbusCTX == NULL )
		{
	    fprintf( stderr, "Couldn't create libmodbus context for %s.\n",
	    		 deviceName );
	    return( EXIT_FAILURE );
		}
	if( modbus_connect( modbusCTX ) == -1 )
		{
		fprintf( stderr, "modbus connect failed for %s: %s.\n",
				 deviceName, modbus_strerror( errno ) );
		modbus_free( modbusCTX );
		return( EXIT_FAILURE );
		}

	/* Set the slave ID of the device to connect to.  A value of 0x01 seems
	   to work, there's a PZEM command to change it to a value 0x01...0xF7
	   but we don't touch that */
	modbus_set_slave( modbusCTX, configSlaveAddress );

	/* If we're providing debug output, let the user know that we've
	   established a connection */
	if( debugOutput )
		{
		printf( "Connected to %s at %d 8N1, slave ID %d.\n",
				deviceName, configBaudRate, configSlaveAddress );
		}

	/* Try and read the first 9 registers, which is all the PZEM data
	   except the alarm status */
	if( modbus_read_input_registers( modbusCTX, 0, 9, modbusResults ) == -1 )
		{
		fprintf( stderr, "modbus register read failed for slave ID %d: %s.\n",
				 configSlaveAddress, modbus_strerror( errno ) );
		modbus_free( modbusCTX );
		return( EXIT_FAILURE );
		}

	/* Output the results */
	for( i = 0; modbusResultInfo[ i ] != END; i++ )
		{
		const int modbusInfo = modbusResultInfo[ i ];
		uint32_t value = modbusResults[ resultIndex++ ];
		float result, divisor;

		/* Get the high word if there is one */
		if( modbusInfo & DOUBLEWORD )
			value |= ( uint32_t ) modbusResults[ resultIndex ++ ] << 16;

		/* Scale the value to get the actual measurement */
		divisor = ( modbusInfo & DIV1000 ) ? 1000 : \
				  ( modbusInfo & DIV100 ) ? 100 : 10;
		result = value / divisor;

		/* If we're displaying the voltage only, print it and exit.  We
		   output it as an integer since it's going to be used by the
		   shell */
		if( showVoltage )
			{
			printf( "%d\n", ( int ) result );
			break;
			}

		if( debugOutput )
			printf( "%d %.2f\n", value, result );
		else
			{
			if( thingspeakFieldNames[ i ] != NULL )
				printf( "&%s=%.2f", thingspeakFieldNames[ i ], result );
			}
		}

	modbus_free( modbusCTX );

	return( EXIT_SUCCESS );
	}

I’ve now updated this to also talk to Adafruit IO alongside Thingspeak and IFTTT, as well as make the code a bit more general-purpose, if anyone wants the updated code let me know.

hi,

Any guidance on how to set address for each pzem using python?

Thanks,
I was able to figure this out from the code snippets above. Not a programmer, so wasn’t able to figure out how to get stuff working earlier but, figured out eventually. Will post some updates later in a searchengine friendly manner so that it is useful for others looking for similar stuff.

thumbs_up thumbsup

As Promised above:

Read Multiple PZEM-004t V3 Energy Sensors using Python (Raspberry PI)

All credits to PB66 and Bill.Thomson who discussed and posted the code somewhere above on this thread. I with my very basic programming knowledge and some google search just added a few lines to convert the output to JSON and publish to a mqtt topic for easy consumption on Nodered or Home assistant etc via mqtt.

Posted code in the below git repo with some basic instructions and image. Feedback for improvements is welcome.