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 );
}