/*
Reading electricity meter Landis+Gyr ZMF 120AC impulses for RRDtool logging
Branje impulzov iz LandisGyr stevca ZMF 120AC po RS232 protokolu.
Konstanta stevca 500 imp/kWh. Dolzina impulza 40ms
Leon Kos, 2011
RS232 adapter with 20 meters of phone cable between meter and Linux server
requires two components: R1 provides pulldown. D1 reverse voltage protection.
DB9 RS232 Connector
___
TxD | 3 |---+
| | |
| | | | R1
| | | | 1K5
| | | |
| | | blue/white
RxD | 2 |---+----------------------+
| | | ====|====
| | --- D1 |1o o3 o4| 6 pin connector in ZMF120AC meter
| | \ / BAT85 | | to four optocouplers SFH6156
| | ===== |2o o4 o5|
| | | blue ====|====
DTR | 4 |---+----------------------+
---
Electricity meter ZMF 120AC pin descriptions:
Pin 1: Input 1 Anode
Pin 2: Input 1 Catode
Pin 3: Signal ground = common Emitor and Catode for optocouplers 2,3,4
Pin 4: Output 2 Collector
Pin 5: Input 3 Anode
Pin 6: Output 4 Collector
Open collector pulses between pins 4 and 3:
T=variable pulse frequency with meter constant C=500imp/kWh
__ _______________ _______________ _______________ ____
| | | | | | | |
| | | | | | | |
+-+ +-+ +-+ +-+
T_pulse=40ms
Absolute maximum ratings for optocouplers SFH6156:
Diode: I_F=60mA, V_reverse = 6V
transistor: V_CE=70V, V_CEO=7V, I_C=50mA
Daemon description:
This is standard linux daemon that reads impulses as it were RS232 chars.
Every impulse is read and added to counter that is split into two tariffs
for better accounting of total energy consumption. As T_pulse is constant
characters are read at 150baud giving 0x60 reads with 7bit char length.
When enough time is passes (5 minutes) counters reading is send to the
remote RRDcollect daemon that stores reading into RRD two databases
created with:
# Measuring electric power in kW
# Create 5 min samples for 8 days, hourly samples for 32 days
# and dayly samples for 5 years
rrdtool create --no-overwrite data/electricity-meter-counter.rrd \
DS:PWR:COUNTER:900:0:4294967295 \
RRA:AVERAGE:0.5:1:2304 \
RRA:AVERAGE:0.5:12:768 \
RRA:AVERAGE:0.5:288:1890 \
RRA:MAX:0.5:12:768 \
RRA:MAX:0.5:288:1890 \
RRA:MIN:0.5:12:768 \
RRA:MIN:0.5:288:1890
# Recording energy in kWh
# Create hourly samples for one month and daily samples for 5 years
rrdtool create --no-overwrite data/electricity-meter-gauge.rrd \
DS:NT:GAUGE:900:0:4294967295 \
DS:VT:GAUGE:900:0:4294967295 \
RRA:MAX:0.5:12:744 \
RRA:MAX:0.5:288:1890
Reading are loged into syslog for backup purposes and for restoring
counter values between server downtime. New counter values at reboot
are estimated from past two syslog entries.
There are no arguments required as all parameters are baked in.
Except optional initialization consumption values that is used to
synchronize meter readings at first run or possible correction.
Signal SIGUSR1 forces current meter readings to syslog output.
Compile the daemon with:
cc -Wall -lrrd electricity-meter.c -o electricity-meter
For RRD database monitor visit
http://mon.hpc.fs.uni-lj.si
References:
- Meter "technical data":
http://www.landisgyr.eu/apps/products/data/pdf1/LandisGyr_ZMF100AC_TechD...
- RRDcollect usage:
http://www.netways.de/uploads/media/Sebastian_Harl_How_to_Escape_the_IO_...
- Daemon writing:
http://www.netzmafia.de/skripten/unix/linux-daemon-howto.html
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define HAVE_STDINT_H 1
#include
#include
#define SERIAL_DEVICE "/dev/ttyS0" /* RS232 port */
#define BUFFER_SIZE 10 /* Size of read buffer */
#define RRD_INTERVAL 300 /* RRD logging interval in seconds */
#define SYSLOG_FILE "/var/log/messages" /* for counter restore */
#define SYSLOG_IDENT "electricity-meter" /* facility name */
#define SYSLOG_INTERVAL 3600 /* Log interval into syslog */
#define METER_CONSTANT 500 /* Energy meter frequency impulses/kWh */
#define RRDCACHED_ADDRESS "10.0.2.1"
typedef uint32_t counter;
static counter tariff_counter[2]; /* Pulse counter */
static sig_atomic_t terminate_signal_received; /* Loop terminate */
static sig_atomic_t log_counter_requested;
static void signal_handler( int signal )
{
switch( signal )
{
case SIGUSR1: /* SGE send this begore SIGSTOP */
log_counter_requested = 1;
break;
case SIGHUP:
case SIGTERM:
case SIGINT:
terminate_signal_received = 1;
log_counter_requested = 1;
// fprintf(stderr, "Terminating...\n");
break;
}
}
/* Local policy on tariffs (low and high).
*/
static int current_tariff()
{
struct tm *tm;
time_t now = time(0);
tm = localtime(&now);
if (tm->tm_wday == 0 || tm->tm_wday == 6 /*Saturday or Sunday*/
|| tm->tm_hour >= 22 || tm->tm_hour < 6) /* From 22 to 6 */
return 0;
return 1;
}
/* If constant load is assumed then one week contains 2*24+5*8=88 hours
of Tariff 0 (NT) and 5*16=80 hours of Tarrif 1 (VT) */
static void restore_tariffs_approximately(double kWh)
{
counter counter;
tariff_counter[0] = 0.5238*kWh*METER_CONSTANT;
tariff_counter[1] = 0.4762*kWh*METER_CONSTANT;
counter = tariff_counter[0] + tariff_counter[1];
syslog(LOG_INFO, "Tariffs approximated using %.1Lf kWh",
(long double)counter/METER_CONSTANT);
}
/* Last two readings are used for restoring counter from syslog */
static int restore_counter_from_syslog()
{
# define MAX_LENGTH 80
time_t prev_seconds=0, last_seconds=0;
counter prev_counter[2], last_counter[2];
char line[MAX_LENGTH];
FILE *f;
f = fopen(SYSLOG_FILE, "r");
if (f == NULL)
{
perror("Error: reading " SYSLOG_FILE);
return errno;
}
while(fgets(line, MAX_LENGTH, f))
{
char *facility;
if((facility=strstr(line, SYSLOG_IDENT))
&& (strstr(line, "imp ==")) )
{
int i;
prev_seconds = last_seconds;
prev_counter[0] = last_counter[0];
prev_counter[1] = last_counter[1];
for(i = 0; i < facility-line; i++)
if (line[i] == ':' && line[i+3] == ':')
{
last_seconds = 3600*atoi(&line[i-2])
+ 60*atoi(&line[i+1])+atoi(&line[i+4]);
sscanf(facility+sizeof(SYSLOG_IDENT), "%u+%u",
&last_counter[0], &last_counter[1]);
// printf("%lu+%lu\n",last_counter[0], last_counter[1]);
break;
}
}
}
fclose(f);
if (last_seconds > prev_seconds) /* two readings in correct order */
{
float power;
double total;
struct tm *tm;
int tariff;
time_t now = time(0);
tm = localtime(&now);
now = (3600*tm->tm_hour+60*tm->tm_min+tm->tm_sec)%(24*3600);
assert(now > last_seconds);
power = (float)(last_counter[0]+last_counter[1]
-prev_counter[0]-prev_counter[1])
/(last_seconds-prev_seconds);
tariff = current_tariff();
tariff_counter[tariff] = last_counter[tariff] + power*(now-last_seconds);
if (tariff == 0)
tariff_counter[1] = last_counter[1];
else
tariff_counter[0] = last_counter[0];
total = (tariff_counter[0] + tariff_counter[1])
/(double)METER_CONSTANT;
syslog(LOG_INFO, "Estimated %.1lf kWh from " SYSLOG_FILE, total);
return 0;
}log_counter_requested = 1;
return -1;
}
int main(int argc, char *argv[])
{
struct termios t, origterm; // see man termios - declared as above
int rc;
int fd;
time_t last_syslog_time, last_rrd_update_time, now;
/* Our process ID and Session ID */
pid_t pid, sid;
/* Fork off the parent process */
pid = fork();
if (pid < 0) {
exit(EXIT_FAILURE);
}
/* If we got a good PID, then
we can exit the parent process. */
if (pid > 0) {
exit(EXIT_SUCCESS);
}
/* Change the file mode mask */
umask(0);
/* Create a new SID for the child process */
sid = setsid();
if (sid < 0) {
/* Log the failure */
exit(EXIT_FAILURE);
}
/* Change the current working directory */
if ((chdir("/")) < 0) {
/* Log the failure */
exit(EXIT_FAILURE);
}
/* Close out the standard file descriptors */
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
if (argc > 1)
restore_tariffs_approximately(atoi(argv[1]));
else
restore_counter_from_syslog();
// return 0;
fd = open(SERIAL_DEVICE, O_RDONLY | O_NOCTTY);
rc = tcgetattr (fd, &t);
if (rc < 0)
{
int tmp;
tmp = errno;
close(fd);
errno = tmp;
perror("Error opening " SERIAL_DEVICE);
return errno;
}
cfsetospeed(&t, B150);
cfsetispeed(&t, B150);
tcgetattr(fd,&t); // Get terminal parameters.
origterm = t; // Save original settings.
// Set to non-canonical mode, and no RTS/CTS handshaking
t.c_iflag &= ~(BRKINT|ICRNL|IGNCR|INLCR|INPCK|ISTRIP|IXON|IXOFF|PARMRK);
t.c_iflag |= IGNBRK|IGNPAR;
t.c_oflag &= ~(OPOST);
t.c_cflag &= ~(CRTSCTS|CSIZE|PARENB);
t.c_lflag &= ~(ECHO|ECHOE|ECHOK|ECHONL|ICANON|IEXTEN|ISIG);
/* Read some reasonable number bytes at a time, no timeout */
t.c_cc[VMIN] = BUFFER_SIZE;
t.c_cc[VTIME] = 0;
/* 7 data bits, Receiver enabled, Hangup, Dont change "owner" */
t.c_cflag |= CS7 | CREAD | HUPCL | CLOCAL;
rc = tcsetattr(fd, TCSAFLUSH, &t);
/* Flush the input and output buffers */
tcflush(fd,TCIOFLUSH);
if (rc < 0)
{
int tmp;
tmp = errno;
close(fd);
errno = tmp;
perror("Nastavljanje serijskega porta");
return errno;
}
fcntl(fd, F_SETFL, 0); /* Normal (blocking) behavior with read() */
signal( SIGUSR1, signal_handler );
signal( SIGHUP, signal_handler );
signal( SIGTERM, signal_handler );
signal( SIGINT, signal_handler );
openlog(SYSLOG_IDENT, LOG_CONS, LOG_NEWS);
syslog(LOG_WARNING, "Counting energy with %d imp/kWh", METER_CONSTANT);
terminate_signal_received = 0;
log_counter_requested = 1;
now = time(0);
last_syslog_time = now;
last_rrd_update_time = now - RRD_INTERVAL;
while (!terminate_signal_received) /* nearly infinite loop */
{
int rc;
unsigned char buffer[BUFFER_SIZE];
char update[25];
const char *values[1] = {update};
tariff_counter[current_tariff()] += read(fd, buffer, BUFFER_SIZE);
now = time(0);
if (now >= last_syslog_time + SYSLOG_INTERVAL
|| log_counter_requested)
{
double total = (tariff_counter[0] + tariff_counter[1])
/(double)METER_CONSTANT;
syslog(LOG_INFO, "%u+%u imp == %.1lf kWh",
tariff_counter[0], tariff_counter[1], total);
last_syslog_time = now;
log_counter_requested = 0;
}
if (now >= RRD_INTERVAL + last_rrd_update_time)
{
if ((rc = rrdc_connect(RRDCACHED_ADDRESS)) == 0)
{
snprintf(update, sizeof(update), "N:%.3f:%.3f",
(double)tariff_counter[0]/METER_CONSTANT,
(double)tariff_counter[1]/METER_CONSTANT);
rc = rrdc_update("electricity-meter-gauge.rrd", 1, values);
if (rc)
{
char buffer[100];
snprintf(buffer, sizeof(buffer), "Error updating: %s",
rrd_get_error());
syslog(LOG_WARNING, buffer);
}
snprintf(update, sizeof(update), "N:%u",
tariff_counter[0] + tariff_counter[1]);
rc = rrdc_update("electricity-meter-counter.rrd", 1, values);
if (rc)
{
char buffer[100];
snprintf(buffer, sizeof(buffer), "Error updating: %s",
rrd_get_error());
syslog(LOG_WARNING, buffer);
}
rrdc_disconnect ();
}
else
{
char buffer[120];
snprintf(buffer, sizeof(buffer), "Error: %s @" RRDCACHED_ADDRESS,
rrd_get_error());
syslog(LOG_WARNING, buffer);
}
last_rrd_update_time = now;
}
}
closelog();
// Closes the connection to the port
tcsetattr(fd, TCSAFLUSH, &origterm); // restore tty settings
tcflush(fd, TCIOFLUSH);
close(fd);
return 0;
}