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:

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();
}
code/powermeter.c.txt · Last modified: 2022-12-21 19:53 by keith
CC Attribution-Noncommercial-Share Alike 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0