PZEM-016 single phase modbus energy meter

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.