====== 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: * My [[code:libax|libax library]] * 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 #include #include #include #include // #include // #include // #include #include #include #include #include #include #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(); }