powermeter.c
There is still a bunch of hard-coded cruft in here. Assuming sensors have id's 11 and 12 (the default is 1). This is currently running on my control server.
This is growing. It is dependent on:
- libmodbus
Next go-round I will put the dev setup in a zip file. If I get real motivated I will add it to my fossil repo.
//
// Source: powermeter.c
// Created: Tue Dec 20 16:02:25 2022
// By: Keith Edwin Smith
// Copyright: (c)2022 Keith Edwin Smith
// Title: Management/Polling/Logging for Energy Sensor
//
// Last Modified: 2022-12-20 16:20:09
//
// TODO: CSV log, Configuration file, sensor list from db and/or config
//
#include <local.h>
#include <modbus/modbus.h>
#include <postgresql/libpq-fe.h>
#include <time.h>
#include <string.h>
// #include <sys/types.h>
// #include <sys/stat.h>
// #include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#define RTU_DEVICE "/dev/ttyUSB0"
#define DEFAULT_INTERVAL 5
// Back to back polls will fail without some wait time
#define WAIT_NEXT_USEC 400000
#define REG_VOLTAGE 0x0000
#define REG_CURRENT_LOW 0x0001
#define REG_CURRENT_HIGH 0x0002
#define REG_POWER_LOW 0x0003
#define REG_POWER_HIGH 0x0004
#define REG_ENERGY_LOW 0x0005
#define REG_ENERGY_HIGH 0x0006
#define REG_FREQUENCY 0x0007
#define REG_POWER_FACTOR 0x0008
#define REG_ALARM 0x0009
// Local constructs
uint16_t buf[64];
struct sensor_values {
int sensor_id;
char stamp[32]; // YYYY-MM-DD HH:MM:SS.uuuuuu-0000
double volts;
double amps;
double watts;
unsigned long energy;
double frequency;
double power_factor;
int alarm;
} sv;
// ==== Global variable structure ====
struct globs {
PGconn *dbh;
modbus_t *psensor;
int verbose;
int action;
int energy_reset_month;
int energy_reset_day;
int sensor_count;
int sensor_list[256];
int current_id;
int new_id;
int interval;
} g;
// Program Arguments
char *argvlist[] = {
"-d"
,"--daemon"
,"--help"
,"--id"
,"--interval"
,"--once"
,"--reset"
,"--reset-day"
,"--set-id"
,"-v"
,"--verbose"
,NULL
};
#define ARGV_D 0
#define ARGV_DAEMON 1
#define ARGV_HELP 2
#define ARGV_ID 3
#define ARGV_INTERVAL 4
#define ARGV_ONCE 5
#define ARGV_RESET 6
#define ARGV_RESET_DAY 7
#define ARGV_SET_ID 8
#define ARGV_V 9
#define ARGV_VERBOSE 10
#define ARGV_END 11
// Script actions
#define ACTION_DEFAULT 1
#define ACTION_POLL_LOOP 0
#define ACTION_POLL_ONCE 1
#define ACTION_RESET 2
#define ACTION_SET_ID 3
// Function Prototypes
#ifndef NO_PROTOTYPE
int add_table_entry(PGconn *myhouse, struct sensor_values *sv_p);
PGconn *db_open();
int sensor_poll_all(modbus_t *psensor, PGconn *dbh, struct tm *now);
int sensor_poll_id(modbus_t *psensor, int sensor_id, char *stamp);
int sensor_reset_energy(modbus_t *psensor, int sensor_id);
#endif
//----------------------------------------------------------------------
// ADD_TABLE_ENTRY
//----------------------------------------------------------------------
int add_table_entry(PGconn *myhouse, struct sensor_values *sv_p) {
char sql[2048];
PGresult *res;
sprintf(sql,"INSERT INTO power_monitor\n"
" (\n"
" pwm_sensor,pwm_stamp\n"
" ,pwm_volts,pwm_amps,pwm_watts,pwm_energy\n"
" ,pwm_frequency,pwm_power_factor,pwm_alarm\n"
" ) VALUES (\n"
" %d,'%s'\n"
" ,%0.1f,%0.3f,%0.1f,%ld\n"
" ,%0.1f,%0.2f,%d\n"
" )\n"
,sv_p->sensor_id,sv_p->stamp
,sv_p->volts,sv_p->amps,sv_p->watts,sv_p->energy
,sv_p->frequency,sv_p->power_factor,sv_p->alarm
);
if(g.verbose) {
fprintf(stdout,"--\n");
fprintf(stdout,"%s",sql);
fprintf(stdout,"--\n");
}
res = PQexec(myhouse, sql);
if (PQresultStatus(res) != PGRES_COMMAND_OK) {
fprintf(stderr,"Insert Failed %s\n",
PQerrorMessage(myhouse)
);
}
PQclear(res);
return(0);
}
//----------------------------------------------------------------------
// DB_OPEN
// Open our database
// FIXME: Hardcoded DB parms
//----------------------------------------------------------------------
PGconn *db_open() {
PGconn *dbh;
// Connect to the database
dbh = PQconnectdb("host=127.0.0.1 user=myhouse dbname=myhouse password=myhouse");
if (PQstatus(dbh) == CONNECTION_BAD) {
fprintf(stderr, "connection to database failed: %s\n",
PQerrorMessage(dbh));
exit(-1);
}
if(g.verbose) {
int ver = PQserverVersion(dbh);
printf("Server version: %d\n", ver);
}
return(dbh);
}
//----------------------------------------------------------------------
// END_PROGRAM
//----------------------------------------------------------------------
int end_program() {
modbus_free(g.psensor);
PQfinish(g.dbh);
exit(0);
}
//----------------------------------------------------------------------
// CURRENT_TIMESTAMPTZ
//----------------------------------------------------------------------
struct tm *current_timestamptz(char *buf, int buflen) {
time_t t;
struct tm *tmp;
t = time(NULL);
tmp = localtime(&t);
if (tmp == NULL) {
perror("localtime");
exit(255);
}
if (strftime(buf,32,"%Y-%m-%d %H:%M:%S-0700", tmp) == 0) {
fprintf(stderr, "strftime returned 0");
exit(255);
}
return(tmp);
}
//----------------------------------------------------------------------
// LOAD_CONFIG
//----------------------------------------------------------------------
int load_config() {
FILE *f;
f = fopen("powermeter.cfg","r");
if(f == NULL) {
return 0;
}
fclose(f);
return 0;
}
//----------------------------------------------------------------------
// RESET_CHECK
//----------------------------------------------------------------------
int reset_check(struct tm *now) {
return(g.energy_reset_month == now->tm_mon && g.energy_reset_day == now->tm_mday);
}
//----------------------------------------------------------------------
// SENSOR_OPEN
//----------------------------------------------------------------------
int sensor_open(modbus_t *psensor, int sensor_id) {
if (modbus_connect(psensor) == -1) {
fprintf(stderr, "Connection failed: %s\n", modbus_strerror(errno));
return -1;
}
modbus_set_slave(psensor, sensor_id);
modbus_set_response_timeout(psensor, 5, 0000000);
return(1);
}
//----------------------------------------------------------------------
// SENSOR_POLL_ALL
//----------------------------------------------------------------------
int sensor_poll_all(modbus_t *psensor, PGconn *dbh, struct tm *now) {
int ix;
char stamp[32];
now = current_timestamptz(stamp,32);
for(ix = 0; g.sensor_list[ix] != 0; ix++) {
if(reset_check(now)) {
sensor_reset_energy(psensor,g.sensor_list[ix]);
}
sensor_poll_id(psensor,g.sensor_list[ix], stamp);
if(dbh != NULL) {
add_table_entry(dbh,&sv);
}
usleep(WAIT_NEXT_USEC); // Too fast will hang
}
// Update to next month if triggered
if(reset_check(now)) {
g.energy_reset_month = ( g.energy_reset_month + 1 ) % 12;
}
}
//----------------------------------------------------------------------
// SENSOR_POLL_ID
// Poll sensor sensor_id for data
//----------------------------------------------------------------------
int sensor_poll_id(modbus_t *psensor, int sensor_id, char *stamp) {
int x,i;
if(g.verbose) {
fprintf(stdout,"Polling: %d\n",sensor_id);
}
sensor_open(psensor,sensor_id);
x = modbus_read_input_registers(psensor, 0, 10, buf);
if (x == -1) {
fprintf(stderr, "%s\n", modbus_strerror(errno));
return -1;
}
//for (i=0; i < x; i++) {
// printf("reg[%d]=%d (0x%X)\n", i, buf[i], buf[i]);
//}
sv.sensor_id = sensor_id;
strcpy(sv.stamp,stamp);
sv.volts = buf[REG_VOLTAGE] / 10.0;
sv.amps = ((int)(buf[REG_CURRENT_HIGH] << 16) + buf[REG_CURRENT_LOW]) / 1000.0;
sv.watts = ((int)(buf[REG_POWER_HIGH] << 16) + buf[REG_POWER_LOW]) / 10.0;
sv.energy = ((int)(buf[REG_ENERGY_HIGH] << 16) + buf[REG_ENERGY_LOW]);
sv.frequency = buf[REG_FREQUENCY] / 10.0;
sv.power_factor = buf[REG_POWER_FACTOR] / 100.0;
sv.alarm = buf[REG_ALARM] > 0 ? 1 : 0;
modbus_close(psensor);
}
//----------------------------------------------------------------------
// SENSOR_RESET_ENERGY
//----------------------------------------------------------------------
int sensor_reset_energy(modbus_t *psensor, int sensor_id) {
unsigned char request[8];
int length,ix;
uint8_t rsp[MODBUS_TCP_MAX_ADU_LENGTH];
if(g.verbose) {
fprintf(stderr,"Resetting Energy Counter\n");
}
sensor_open(psensor,sensor_id);
request[0] = sensor_id;
request[1] = 0x42;
length = modbus_send_raw_request(psensor, request, 2);
modbus_receive_confirmation(psensor, rsp);
if(g.verbose) {
fprintf(stderr,"request returns = %d\n",length);
// fprintf(stderr,"length = %d Respose:\n",length);
// for(ix = 0; ix < MODBUS_TCP_MAX_ADU_LENGTH; ix++) {
// fprintf(stderr,"%02x ",rsp[ix]);
// }
// fprintf(stderr,"\n");
}
modbus_close(psensor);
}
//----------------------------------------------------------------------
// SENSOR_SET_ID
// This should probably wait a few seconds and poll the new id
//----------------------------------------------------------------------
int sensor_set_id(modbus_t *psensor, int current_id, int new_id) {
int x;
sensor_open(psensor,current_id);
x = modbus_write_register(psensor, 0, new_id);
if (x == -1) {
fprintf(stderr, "%s\n", modbus_strerror(errno));
return -1;
}
modbus_close(psensor);
}
//----------------------------------------------------------------------
// USAGE
//----------------------------------------------------------------------
int usage(char *pgm) {
fprintf(stderr
,"\n"
"Usage: %s [flags]\n"
" Flags:\n"
" -d|--daemon Start a polling loop\n"
" --help This help text\n"
" --id {n} No default, pull from sensor {n}\n"
" --interval {n} Polling interval in seconds\n"
" --once Poll sensors once\n"
" --reset Reset Energy\n"
" --reset-day Day of the month for energy reset\n"
" --set-id {current} {new}\n"
" Change ID of sensor {current} to {new}\n"
" -v|--verbose Make Noise\n"
"\n"
,pgm
);
exit(0);
}
//----------------------------------------------------------------------
// PARSE_ARGS
//----------------------------------------------------------------------
int parse_args(int argc, char *argv[]) {
int arg,ix;
// Set these to make the sensor reset energy periodically
// on (maybe) the first day of the month
memset(&g,0,sizeof(g));
g.action = ACTION_DEFAULT;
g.verbose = 0;
g.energy_reset_month = -1;
g.energy_reset_day = 1;
g.interval = DEFAULT_INTERVAL;
// Parse the arguments
for(ix = 1; ix < argc; ix++) {
arg = argvindex(argvlist,argv[ix],strncmp);
switch(arg) {
case ARGV_D:
case ARGV_DAEMON:
g.action = ACTION_POLL_LOOP;
break;
case ARGV_HELP:
usage(argv[0]);
break; // NOTREACHED
case ARGV_ID:
// Need to parse the string instead, i.e. 1,3,5 or 11-14 . . .
// multiple --id's for now will work
g.sensor_list[g.sensor_count++] = strtol(argv[++ix],NULL,0);
break;
case ARGV_INTERVAL:
g.interval = strtol(argv[++ix],NULL,0);
break;
case ARGV_ONCE:
g.action = ACTION_POLL_ONCE;
break;
case ARGV_V:
case ARGV_VERBOSE:
g.verbose = 1;
break;
case ARGV_RESET:
g.action = ACTION_RESET;
g.verbose = 1;
break;
case ARGV_RESET_DAY:
g.energy_reset_day = strtol(argv[++ix],NULL,0);
break;
case ARGV_SET_ID:
if(argc > (ix + 2)) {
g.current_id = strtol(argv[++ix],NULL,0);
g.new_id = strtol(argv[++ix],NULL,0);
} else {
usage(argv[0]);
}
g.action = ACTION_SET_ID;
break;
default:
fprintf(stderr,"Unknown Argument: %s\n",argv[ix]);
}
}
// FIXME: Hardcoded id's
// Defaults should read from config or database.
if(g.sensor_list[0] == 0) {
g.sensor_list[0] = 11;
g.sensor_list[1] = 12;
}
}
//----------------------------------------------------------------------
// MAIN
//----------------------------------------------------------------------
int main(int argc, char *argv[]) {
int ix;
struct tm *now;
parse_args(argc,argv);
g.dbh = db_open();
// Set up an RS485 sensor handle
g.psensor = modbus_new_rtu(RTU_DEVICE,9600,'N',8,1);
if (g.psensor == NULL) {
fprintf(stderr, "Unable to create the libmodbus context\n");
return -1;
}
modbus_rtu_set_serial_mode(g.psensor, MODBUS_RTU_RS485);
switch(g.action) {
case ACTION_POLL_LOOP:
for(;;) {
sensor_poll_all(g.psensor,g.dbh,now);
sleep(g.interval);
}
break;
case ACTION_POLL_ONCE:
sensor_poll_all(g.psensor,g.dbh,now);
break;
case ACTION_RESET:
for(ix = 0; g.sensor_list[ix] != 0; ix++) {
sensor_reset_energy(g.psensor, g.sensor_list[ix]);
}
break;
case ACTION_SET_ID:
sensor_set_id(g.psensor,1,g.new_id);
break;
}
end_program();
}
