Power meter

Power meter

One of the methods to reduce the energy consumption in private homes (and in businesses) is to visualise the consumption. By showing how much energy is being used and when it's being used, you can often easily find ways to reduce the consumption.

To do this in my own apartment, I have built a power meter that monitors the electric power used, and stores the data in a database, for later display in graphs.

The system consists of an Arduino with Ethernet Shield located close to the actual utility power meter, a Perl script (which reads data from the Arduino and stores it in a database) running on my Linux box and a couple of .php pages (which displays the collected data) running on the web-server on the same Linux box. The Arduino part is inspired by the following articles:

Reading pulses from meters with pulse outputs
Arduino Electricity Data Logger

My utility power meter has a LED that flashes 1000 times for each kWh used. On my meter it flashes with visible light, but there are other meters that use infrared light. On the latter it can be difficult to see if the LED on the meter is active. One trick is to use a digital camera (or mobile phone camera) to inspect the meter, since many of these cameras can "see" infrared light.

Utility Power Meter

The Arduino part of the system consists of an Arduino UNO R3 and an Arduino Ethernet Shield mounted in a standard Arduino Box.

Arduino

To detect the light pulses from the utility meter I use a TAOS/ams TSL257 Light-to-Voltage Converter. If the meter is using an infrared LED or shielding from ambient light is more difficult, the use of a sensor that is less sensitive to visible light (such as TSL261) may be preferred.

TSL257

The full installation at the utility meter can be seen below. The only thing that touches the utility meter is the sensor and it's fastened with gaffer tape, so no modifications to the utility meter is needed (the utilities company does not like things screwed onto their meters). Since the TSL257 is a visible light sensor it needs to be protected from the ambient light. Hence the use of several layers of gaffer tape.

Installation

The schematics of the Arduino and Sensor are very simple since the TSL257 can be connected directly to the Arduino without the need for any dropping resistor.

Schematics

And the Sketch for the Arduino can be seen below. This Sketch has the values for my setup (MAC-address, IP address and number of pulses pr. kWh), so remember to change these values if you want to use the code for your own setup. The code counts the number of pulses on the meter. This number can then be read by a remote computer. The code also calculates the current Watt usage, by measuring the time between pulses.

PowerMeter.ino
/*
  Power Meter Led counter
  by Lars Lind Nilsson (http://www.lindnilsson.dk/lars)
*/

#include <SPI.h>
#include <Ethernet.h>

// MAC address of the Ethernet Shield
byte mac[] = { 0x90, 0xA2, 0xDA, 0x0D, 0x24, 0x7A };  
// IP address for the shield:
byte ip[] = { 192, 168, 1, 25 };    

// Telnet defaults to port 23
EthernetServer server = EthernetServer(23);

volatile unsigned long counter = 0;
volatile unsigned long lastImpulse = 0;
volatile unsigned long diffImpulse = 0;
volatile long currentWatt = 0;
boolean inTrace = false;

// the setup routine runs once when you press reset:
void setup() {                
  // Attach an interrupt handler on PIN 3
  attachInterrupt(1, blink, FALLING);
  
  // start ethernet "server"
  Ethernet.begin(mac, ip);
  server.begin();
}

// the loop routine runs over and over again forever:
void loop() {
  char readChar;
  unsigned long tempCounter;
  
  EthernetClient client = server.available();
  if ((client == true) || inTrace) {
    readChar = client.read();
    if ((readChar == -1) && inTrace) {
      delay(1000);
      readChar = 't';
    }
    switch (readChar) {
      case 'p' :
        // the p (peek) command will write back the current impulse counter and current Watt usage
        server.print("p ");
        server.print(counter);
        server.print(" ");
        server.println(currentWatt);
        client.stop();
        break;
      case 'r' :
        // the r (read) command will write back the current impulse counter and current Watt usage, and reset the counter
        tempCounter = counter;
        server.print("r ");
        server.print(tempCounter);
        server.print(" ");
        server.println(currentWatt);
        client.stop();
        counter -= tempCounter;
        break;
      case 't' :
        // the t (trace) command will write back the current impulse counter and current Watt usage every second
        server.print("t ");
        server.print(counter);
        server.print(" ");
        server.println(currentWatt);
        inTrace = true;
        break;
      case 'T' : 
        // the T command will turn off a running trace
        if (inTrace) {
          inTrace = false;
        }
        client.stop();
        break;
      default : 
        client.stop();
    }
  }
}

void blink() {
  unsigned long currentImpulse = millis();
  
  // Increment counter
  counter++;
  
  // Calculate current Watt usage by measuring the time difference between two impulses
  if (lastImpulse != 0) { // we can now calculcate
    if (currentImpulse > lastImpulse) {
      diffImpulse = currentImpulse - lastImpulse;
      currentWatt = long((3600UL * 1000UL) / diffImpulse);
    } else if (lastImpulse > currentImpulse) { // overflow
      diffImpulse = currentImpulse + (4294967295UL - lastImpulse);
      currentWatt = long((3600UL * 1000UL) / diffImpulse);
    } else {
      // We should never get here
      diffImpulse = 0;
      currentWatt = -1;
    }
  }
  
  lastImpulse = currentImpulse;
  
}

On the Linux box there are 2 MySQL tables used by the system:

CREATE TABLE  `powermeter` (
  `currentwatt` int(11) NOT NULL,
  `meterstatus` int(11) NOT NULL,
  `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)

CREATE TABLE `powerlog` (
  `logdate` datetime NOT NULL,
  `impulsecount` int(11) NOT NULL,
  KEY `logdate` (`logdate`)
) 

The following daemon script is running on the Linux box to collect data from the Arduino. It reads the current Watt usage every 30 seconds, and every 5 minutes is reads the number of impulses and stores the number in the database.

powermeter.pl
#! /usr/bin/perl
# This is a daemon that reads power data from a network connected Arduino
# and writes it to a database.
# 
# Written 2012 by Lars Lind Nilsson (http://www.lindnilsson.dk/lars)
#
# You can freely use this code as you wish
#
# If re-distributed with only minor changes, please keep the above attribution
#

use warnings;
use strict;

use threads;
use threads::shared;

use List::Util qw(sum);
use IO::Socket::INET;

use Getopt::Long;
use Proc::Daemon;
use DBI;

use Log::Handler;
use Cwd;
use Proc::Pidfile;

# Program options

my $nodaemon = 0;
my $device = '192.168.1.25';
my $verbose;
my $help;

my $result = GetOptions(
	"nodaemon"       => \$nodaemon,
	"device|d=s"   => \$device,
	"verbose|v"  => \$verbose,
	"help|h"     => \$help,
);

if($help) {
	print "Usage: $0 [--nodaemon]  [{--device|-d} <ip address of device>] [{--verbose|-v}] [{--help|-h}]$/";
	exit 0;
}

# Prepare filenames before daemonizing

my $pidfilename = '/home/myuser' . '/powermeter.pid';
my $logfilename = '/home/myuser' . '/powermeter.log';

# Daemonize

unless($nodaemon) {
	Proc::Daemon::Init;
}

# Log file + PID file

my $pp = Proc::Pidfile->new(pidfile => "$pidfilename");
my $log;
if ($verbose) {
	$log = Log::Handler->new();
	$log->add(file => {filename => "$logfilename", mode => 'append', maxlevel => 7, newline => 1});
} else {
	$log = Log::Handler->new();
	$log->add(file => {filename => "$logfilename", mode => 'append', maxlevel => 6, newline => 1});
}

if ($nodaemon) {
	$log->notice("Power Meter Started (PID: $$)");
} else {
	$log->notice("Power Meter Started as Daemon (PID: $$)");
}

# Log on TERM

$SIG{TERM} = sub { $log->notice("Power Meter Stopped"); undef $pp; exit 0} ;


# Start main procedure

do_loop($device);

$log->error("I should not be here");

# Procedures

sub do_loop {
	my($device) = @_;

	my $socket;
	my $answer;
	my $previous = time;
	my $now;

	# Connect to database

	my $database = DBI->connect("DBI:mysql:database=mydatabase;host=localhost", "myuser", "mypassword");
	$database->do("DELETE FROM powermeter");
	$database->do("INSERT INTO powermeter (currentwatt, meterstatus) VALUES (0,0)");

	while (1)
	{

		$socket = IO::Socket::INET->new(PeerAddr => $device, PeerPort => 23, Proto => "tcp", Type => SOCK_STREAM);

		if ($socket) {

			$now = time;
			if ($now - $previous >= 300) {
				print $socket "r";
				$answer = <$socket>;
				$log->debug($answer);

				if ($answer =~ /r\s(\d*)\s(\d*)/) {
					$database->do("INSERT INTO powerlog (logdate, impulsecount) VALUES (UTC_TIMESTAMP(), " . $1 . ")");
					$database->do("UPDATE powermeter SET currentwatt = " . $2 . ", meterstatus = 1, timestamp = CURRENT_TIMESTAMP");
				$previous = $now;
				}
			} else {
				print $socket "p";
				$answer = <$socket>;
				$log->debug($answer);
				if ($answer =~ /p\s(\d*)\s(\d*)/) {
					$database->do("UPDATE powermeter SET currentwatt = " . $2 . ", meterstatus = 1, timestamp = CURRENT_TIMESTAMP");
				}
	
			}
			close($socket);
		} else {
			# no connection to device	

			$log->notice("No connection to device");			
			$database->do("UPDATE powermeter SET meterstatus = 0, timestamp = CURRENT_TIMESTAMP");
		}
		sleep(30);
	}
	$log->error("What am I doing here?");
}

On the Apache Web server on the Linux box the following two .php pages are used to display the power usage. The first page (power.php) shows the current power usage, and a list of the total consumption for the last 30 days. The second page (powerimage.php) shows a graph of the power usage through the selected day. All data is recorded and displayed based on UTC time (to avoid problems with daylight saving times). If your local time is too far from UTC you may have to make changes to the code.

power.php
<?php
	error_reporting(E_ALL);
	$db=mysql_connect("localhost", "myuser", "mypassword");
	mysql_select_db("mydatabase", $db);
?>
<html>
<head>
<title>Power</title>
</head>
<body>
<h1>Power</h1>
<b><?php
$res = mysql_query("SELECT * FROM powermeter");
	if (mysql_num_rows($res) != 1) {
		echo "Server not running (no record)";
	}
	else
	{
		$temprow = mysql_fetch_array($res);
		mysql_free_result($res);
		if ($temprow["meterstatus"] != 1) {
			echo "No connection to device";
		} else if (strtotime("now") - strtotime($temprow["timestamp"]) > 60) {
			echo "Server not running (stale data)";
		} else {
						
			echo $temprow["currentwatt"] . " W";
		}
	}
?></b><br><br>
<table>
<tr>
<td><b>Day</b></td>
<td><b>Date</b></td>
<td><b>kWh</b></td>
</tr>
<?php
$res = mysql_query("SELECT SUM(impulsecount) AS powerusage, CAST(logdate as DATE) AS powerdate FROM `powerlog` GROUP BY CAST(logdate as DATE) ORDER BY powerdate DESC LIMIT 0, 30");
while ($row = mysql_fetch_assoc($res)) {
	echo "<tr>";
	echo "<td>" . date("D", strtotime($row["powerdate"])) . "</td>";
	echo "<td><a href=\"powerimage.php?date=" . date("Y-m-d", strtotime($row["powerdate"])) . "\">" . date("Y-m-d", strtotime($row["powerdate"])) . "</a></td>";
	echo "<td>" .  $row["powerusage"] / 1000 . "</td>";
	echo "</tr>";
}
mysql_free_result($res);
?>
</table>

</body>
</html>


powerimage.php
<?php
	error_reporting(E_ALL);
	ini_set('display_errors', '1');
	$db=mysql_connect("localhost", "myuser", "mypassword");
	mysql_select_db("mydatabase", $db);

	// find the requested date
	$mydatestring = $_REQUEST["date"];
	$startdatetime = new DateTime($mydatestring);
	$enddatetime = new DateTime($mydatestring);
	$enddatetime->add(new DateInterval('P1D'));

	// find the data
	$sql = "SELECT logdate, impulsecount FROM powerlog WHERE logdate >= '" . date_format($startdatetime, "Y-m-d") . "' AND logdate < '" . date_format($enddatetime, "Y-m-d") ."' ORDER BY logdate";
	$qt=mysql_query($sql) or die(mysql_error());	
	
	// set Content-type
	header ("Content-type: image/jpg");

	// Some initial calculations
	$x_gap=40; // The width of an hour on the x axis
	$x_max=$x_gap*24; // Maximum width of the graph or horizontal axis
	$y_max=500; // Maximum hight of the graph or vertical axis
	$maxwatt = 3000; // Maximum Watt value to show on the graph. Higher values will go outside the graph.
	$y_gap=100; // The gap between each division in y axis

	$wattprdivision = $maxwatt / $y_max * $y_gap;

	$leftborder=50;
	$bottomborder=50;
	// Create canvas
	$im = @ImageCreate ($x_max + $leftborder, $y_max + $bottomborder) or die ("Cannot Initialize new GD image stream");
	// set colors
	$background_color = ImageColorAllocate ($im, 234, 234, 234);
	$text_color = ImageColorAllocate ($im, 233, 14, 91);
	$graph_color = ImageColorAllocate ($im,25,25,25);
	$graph_color2 = ImageColorAllocate ($im,200,200,200);

	// initialise variables
	$x1=0;
	$y1=0;
	$datetime1 = clone $startdatetime;
	$pointcount = 0;
	
	// draw axis 
	imageline ($im,$leftborder, 0,$leftborder,$y_max,$graph_color);
	imageline ($im,$leftborder, $y_max,$leftborder + $x_max,$y_max,$graph_color);
	
	// draw X axis divisions and labels
	ImageString($im,2,$leftborder - 30, $y_max + 10,"UTC",$graph_color);

	imageline ($im,$leftborder, $y_max,$leftborder,$y_max+5,$graph_color);
	ImageString($im,2,$leftborder, $y_max + 10,"00:00",$graph_color);

	imageline ($im,$leftborder + 6 * $x_gap, $y_max,$leftborder + 6 * $x_gap,$y_max+5,$graph_color);
	ImageString($im,2,$leftborder + 6 * $x_gap, $y_max + 10,"06:00",$graph_color);

	imageline ($im,$leftborder + 12 * $x_gap, $y_max,$leftborder + 12 * $x_gap,$y_max+5,$graph_color);
	ImageString($im,2,$leftborder + 12 * $x_gap, $y_max + 10,"12:00",$graph_color);

	imageline ($im,$leftborder + 18 * $x_gap, $y_max,$leftborder + 18 * $x_gap,$y_max+5,$graph_color);
	ImageString($im,2,$leftborder + 18 * $x_gap, $y_max + 10,"18:00",$graph_color);

	// draw Y axis divisions and labels
	for ($i = 1;$i<=4;$i++) {
		imageline ($im,$leftborder - 10, $y_max - ($i *100),$leftborder ,$y_max - ($i * 100),$graph_color);
		imageline ($im,$leftborder, $y_max - ($i *100),$leftborder + $x_max ,$y_max - ($i * 100),$graph_color2);
		ImageString($im,2,$leftborder - 50, $y_max - ($i * 100) - 5,$wattprdivision * $i . " W",$graph_color);
	}

	// draw the data
	while($nt=mysql_fetch_array($qt))
	{
		$pointcount++;
		$datetime2 = new DateTime($nt["logdate"]);
		$x2 = (to_seconds(date_diff($datetime2, $startdatetime)) / (24*60*60)) * $x_max; // Coordinate on X axis
		$y2=$y_max-$y_max * (3600 * $nt["impulsecount"] / to_seconds(date_diff($datetime2, $datetime1))) / $maxwatt; // Coordinate on Y axis

		// Skip plotting the first line segment since it will have no well-defined starting point
		if($pointcount > 2)
		{ 
			imageline ($im,$leftborder + $x1, $y1,$leftborder + $x2,$y2,$text_color); 
		}
		$x1=$x2; // Storing the value for next draw
		$y1=$y2;
		$datetime1 = clone $datetime2;
	}
	// Output the image
	ImageJPEG ($im);


// This function converts a datetime structure to number of seconds since midnight
function to_seconds($DI)
{
        return ($DI->d * 24 * 60 * 60) +
               ($DI->h * 60 *60) +
               ($DI->i * 60) +
               $DI->s;
}
?>

Below you can see an example of the power usage for my apartment on a normal week day. Based on this visualisation, I have discovered that my stand-by power usage is too high (approx. 132W), and that my refrigerator isn't working as well as it should (all the bumps during the day are the refrigerator running).

Power usage

If you have comments or questions, feel free to contact me.