My house has a Ferraris meter (or more generically, an electromechanical induction meter) measuring electricity consumption. This type of meter is pretty common in older houses. There's a digital (but mechanical!) display of kWh "units" consumed. This ticks up slowly, driven by a rotating disc. The disc is part of what is effectively a motor, sensing (and powered by) the current flowing past it into the house.
My objective with this project was to capture and log the energy consumption of my house. More modern electronic meters often have an LED that flashes at a rate proportional to power, and it's a popular project to use an LDR to capture that signal.
An optical solution works here too. The rotating disc is mostly polished (and therefore reflective!) metal, but there's a small arc of black paint on the circumference, to aid visual inspection. So if we shine a light on the disc, we should be able to pick up the change in reflection.
The meter is stamped with the information we'll need to calibrate our sensor. "166 2/3 rev/kWh" on this one. So one revolution corresponds to 6 Wh. One revolution per hour would imply power consumption of 6 W.
In the end I was able to get good accuracy across a range of at least ~30–6000 W. (It's worth noting the meter itself is likely to have some inaccuracy, especially at low power.)
Parts required:
I used a discrete IR LED and photodiode for my sensor. You can buy ready-made reflective optical sensors, which include LED and matching phototransistor in a housing. The housing in particular is a significant aspect of the design. It turns out that getting a decent signal is strongly dependent the sensor being positioned correctly with respect to the light source. Both must also be positioned carefully with respect to the target.
I did initially try using an eBay Arduino LED/photodiode "distance sensing" module. These have discrete digital high/low output. Setting the threshold between high and low is a matter of adjusting a trimpot. I wasn't able to find a reliable setting. It turns out much of this project is actually the software interpretation of the analogue light level.
The IR photodiode is basically an LED in reverse: it induces a current when it's illuminated by light at or near the wavelength it's sensitive to. My module uses an op-amp in transimpedance amplifier configuration to convert this (tiny) current into a (slightly less tiny) voltage.
The sensor module is pressed against the plastic window of the meter with a bit of press-stick. I'm running about 10m of telephone wire inside to the Arduino. The Arduino is hitched to a Raspberry Pi via USB. This setup has a couple of implications. The voltage drop caused by the IR LED drawing current down the wire means we have (even less) headroom for the op-amp. And USB power is extremely noisy.
The LM328 can only put out Vcc –1.5 V. In my configuration I find it clips at about 2.6 V. What I suspect is bias current due to the IR LED also places a floor on the output, at about 1 V. I set the Arduino Aref to EXTERNAL and wired it to the Arduino's own 3.3 V output to try to claw back some resolution.
Testing the module "free-hand", waving it around about 2 cm above my laptop, I was able to confirm what looked like a decent output swing, going from
shiny bezel to dark keyboard. But the upshot of the floor and headroom issues means analogRead()
returns values in the range of about 300 to 800,
or only about 50% of the available ADC resolution. Hardware is not my strong suit.
Most of the work is done on the Arduino:
#define PIN_CTRL 3
#define PIN_SIG A1 // IR photodiode output, amplified.
#define PIN_LED_PWR 5 // Continuously pulsed at rate proportional to power measured.
#define PIN_LED_PULSE 13 // Simple indicator of pulse status (inverted).
#define METER_ENERGY_PER_REV 6 // Nameplate: 166.66... revs/kWh => 1000 revs/6000 Wh => 1 rev/6 Wh (== 21600 Joules).
#define METER_POWER_PER_REVMS 21600000L // 21600 J/sec at 1-sec revolution.
#define PULSE_WIDTH_RATIO 16 // Ratio of total disc circumference to painted length. Empirically determined and not importantt
#define PULSE_MAX_DURATION 20000L // Limits minimum readable power (not energy). 15 sec is about 100 W. 60 sec, 25W. But energy only requires pulse-start edges, and a lower limit facilitates detection of the next one.
#define REV_MIN_DURATION 2160L // Revs completed faster than this are not counted. Second-order noise? 21600000L / 10kW = 2160L. 10kW / 230V ~= 40 amps. House supply at breaker: 40 amps.
unsigned long tme_last_serial = 0;
unsigned long num_revs_last = 0;
enum {FALSE = 0, TRIG_INIT, TRIG_CONFIRM, TRUE} in_pulse ;
unsigned long mindur = 0;
unsigned long tme_pulse_start = 0;
unsigned long tme_pulse_end = 0;
unsigned long tme_pulse_confirm = 0;
unsigned long dur_last_rev = 0;
unsigned long num_revs = 0;
unsigned long pwr = 0;
unsigned long err_pulse_timeouts = 0;
unsigned long err_fast_revs = 0;
unsigned long err_pulse_aborts = 0;
int filter (long* iir, short sample, unsigned char bits) {
*iir += ((((long)sample << 16) - *iir) >> bits);
return (int)((*iir + 0x8000) >> 16);
}
void setup() {
analogReference(EXTERNAL);
Serial.begin(9600);
pinMode(PIN_LED_PULSE, OUTPUT);
pinMode(PIN_LED_PWR, OUTPUT);
pinMode(PIN_CTRL, OUTPUT);
}
void loop() {
unsigned long now = millis();
digitalWrite(PIN_CTRL, HIGH);
delayMicroseconds(100); // Let there be light. We delay to allow current to build.
int a = analogRead(PIN_SIG); // Given 10 bits to start, could multiply by 5. But diffamp needs headroom.
a += analogRead(PIN_SIG); // Each read is about 100us.
a += analogRead(PIN_SIG);
a += analogRead(PIN_SIG);
a >>= 2;
digitalWrite(PIN_CTRL, LOW);
// About 0.5ms will have passed now. Duty cycle is therefore ~0.5/2.5 = 20%.
static long filtLT = 0; // Starting at zero means a pulse is impossible until filter has settled.
static long filtST = 0;
static short baseline;
short sample = filter(&filtST, a, 3); // We need to not attenuate too much at 10kW ~125ms pulses.
// Long-term (baseline) filter gets increasingly "sticky" as time since last pulse (start) increases.
// This means we can adapt to changes in ambient light and to disc wobble at high rotation rates,
// but we become "self-calibrated" to a near-constant baseline as rotation slows.
// It's a second-order effect, I guess.
// A slow pulse that occurs shortly after startup will probably be missed.
short filterbits = min(13, 9+((now - tme_pulse_end) >> 15)); // 30sec => 0(9), 60s => 1(10), 120s => 3(12), 180s => 5(14), 300s => 9(15)
baseline = filter(&filtLT, sample, filterbits);
// Multiplying by 32 (<< 5) means that if (sample - baseline) is ~3% of baseline, ampdiff is equal to baseline.
// This is more fun than taking the raw differential and then testing it against baseline*0.03.
// This is safe given 10-bit ADC and 16-bit int. Max diff of 1023 => 32 736 < 32 767.
// A pulse is a negative deviation from baseline.
int ampdiff = (sample - baseline) << 5;
if (!in_pulse && ampdiff < -1*baseline) {
// TRIG_INIT is just a way to mark the "true" start of a pulse. It also marks noise.
in_pulse = TRIG_INIT;
tme_pulse_start = now;
}
if (in_pulse == TRIG_INIT && ampdiff < -1*(baseline + (baseline << 1))) {
// About 9% e.g. baseline 500, sample 454 (ampdiff <= -1500, diff 46).
// TODO: relate mindur to filterbits? But careful!
// If wheel speeds up after long slow period, we must start catching fast pulses.
in_pulse = TRIG_CONFIRM;
mindur = (now - tme_pulse_start) + 50L;
}
if (in_pulse == TRIG_INIT && now - tme_pulse_start > 50L) {
// Since TRIG_INIT is trying to mark the start of a pulse, and given noise,
// we limit the time we remain in continuous INIT state, to limit the error
// in marking the start. This doesn't affect pulse detection, just pulse timing.
// I'm not sure we need it at all. We could use the confirmation point at the pulse start.
// Note both are delayed by filtering phase shift, so if anything, that's the thing to account for.
in_pulse = FALSE;
}
if (in_pulse == TRIG_CONFIRM && ampdiff > -1*(baseline << 1)) {
// A pulse fails if it deviates below the 6% threshold at all during mindur.
// Once in a pulse however the exit is relaxed.
err_pulse_aborts++;
in_pulse = FALSE;
}
// TODO: Detect false pulse -- ampdiff goes *too* negative i.e. dark
if (in_pulse == TRIG_CONFIRM && now - tme_pulse_start > mindur) {
if (tme_pulse_confirm) {
if (now - tme_pulse_confirm < REV_MIN_DURATION) {
// Uh-oh - too-fast rev...
// TODO: Need to (re)set anything?
// TODO: Move this up to prevent state change?
// Pulse is effectively cancelled.
err_fast_revs++;
}
else {
dur_last_rev = now - tme_pulse_confirm;
pwr = METER_POWER_PER_REVMS / dur_last_rev;
num_revs++;
}
}
in_pulse = TRUE;
tme_pulse_confirm = tme_pulse_start;
digitalWrite(PIN_LED_PULSE, HIGH);
}
if (in_pulse != FALSE && ampdiff >= -1*baseline) {
// Note we use the long-term baseline as a sort of hysteresis; in effect a negative hysteresis, i.e. pulses end prematurely.
// An alternative is to use the "trigger point" regardless of the baseline, i.e. at what value did we go into the pulse?
// But we don't actually use pulse length - we just need to ensure we don't run over the next one.
if (in_pulse == TRUE) {
tme_pulse_end = now;
}
if (in_pulse == TRIG_CONFIRM) {
err_pulse_aborts++;
}
in_pulse = FALSE;
digitalWrite(PIN_LED_PULSE, LOW);
}
if (in_pulse == TRUE && now - tme_pulse_start > PULSE_MAX_DURATION) {
// Uh-oh. Did we wget stuck in a pulse? Or is this just a very low-energy rotation?
in_pulse = FALSE;
tme_pulse_end = now;
digitalWrite(PIN_LED_PULSE, LOW);
filtLT = filtST; // Have to reset filter, or we could immediately be in a pulse again.
err_pulse_timeouts++;
}
if (dur_last_rev && now - tme_pulse_confirm > dur_last_rev) {
pwr = METER_POWER_PER_REVMS / (now - tme_pulse_confirm);
}
/*
* Pulse width: we don't use this, but we could. If marker is 5.2% of disc circumference (measured empirically),
* then at maximum power (10kW, rev in 2.1 sec), we expect pulse width of ~110ms.
* Conversely a pulse width of 2 sec can be expected in a rev of 38 sec at power of ~570 W.
* Not very accurate though - can expect ~20% error due to edge detection.
* We can also use pulse width vs. rev duration to detect misreads, although tricky due to legitimate changes in rotation rate.
* At 5000 W a pulse is about 250ms - only 5 display cycles. So a 10kW pulse is about 125ms - tricky to catch.
*/
//unsigned long pwrEst = (tme_pulse_end ? METER_POWER_PER_REVMS / ((tme_pulse_end - tme_pulse_start)*PULSE_WIDTH_RATIO) : 0);
unsigned long energy = (num_revs * METER_ENERGY_PER_REV);
static boolean fast_serial = true;
if ((fast_serial && now - tme_last_serial > 50) ||
(!fast_serial && (now - tme_last_serial > 1000 || num_revs != num_revs_last))) {
Serial.print(baseline);
Serial.print(",");
Serial.print(sample);
Serial.print(",");
Serial.print(in_pulse);
Serial.print(",");
Serial.print(err_pulse_timeouts);
Serial.print(",");
Serial.print(err_fast_revs);
Serial.print(",");
Serial.print(err_pulse_aborts);
Serial.print(",");
Serial.print(filterbits);
Serial.print(",");
Serial.print(pwr);
Serial.print(",");
Serial.print(energy);
Serial.print(",");
Serial.println(ampdiff);
tme_last_serial = now;
num_revs_last = num_revs;
}
// LED power indicator
unsigned int led_pulse_duration = 0;
static unsigned long tme_led_pulse_start = 0;
unsigned int ledp = 0;
if (pwr) {
led_pulse_duration = 1000000L/pwr; // 1kW => 1/sec, 100W => 1/10sec, 5kW = 10/sec.
}
if (led_pulse_duration) {
ledp = (511*(now - tme_led_pulse_start)/led_pulse_duration);
}
if (ledp > 511) {
tme_led_pulse_start = now;
ledp = 0;
}
// Quadratic, for better perception (more time in low-brightness steps).
analogWrite(PIN_LED_PWR, ledp < 256 ? ((ledp*ledp) >> 8) : (((512-ledp)*(512-ledp)-1) >> 8));
static char buf[8];
static char i = 0;
if (Serial.available()) {
char c = 0;
do {
c = Serial.read();
if (c < 0) {
break;
}
if (i < 8-1) {
buf[i++] = c;
}
} while (1);
buf[i] = '\0';
if (strncmp(buf, "DEBUG", 5) == 0) {
fast_serial = fast_serial ? false : true;
}
if (i > 5-1) {
buf[0] = '\0';
i = 0;
}
}
// This is how we maintain duty-cycle on the LED.
// Changing it will probably necessitate filter parameter adjustment.
delay(2);
}
The signal is sampled about 400 times per second, and passed though an infinite impulse response (IIR) filter. I use this filter algorithm
in almost all my Arduino projects. It's effectively a software RC low-pass filter. The bits
argument controls the degree of filtering – the time constant, essentially.
We're detecting a change in light level that varies in frequency. At 10 kW my meter disc does a revolution in roughly 2 seconds, and the "pulse width" (the dark band) passes by in about 0.1 seconds. Too much filtering and we risk missing fast pulses. But I want to detect pulses below 100 W as well. At about 36 W the disc takes a full 10 minutes to rotate, and the "pulse" lasts 30 seconds. So if we're filtering too little, we risk detecting spurious pulses.
We also have the problem of setting a threshold. What light level (voltage, or analogRead()
value) constitutes a pulse?
The hardware setup is enclosed in a box, but I wanted to avoid trying to calibrate an absolute threshold. I found that slight changes in sensor position
created huge variations in signal level, and I also found that despite the enclosure, afternoon sunlight caused changes in the background level.
So the technique I used was to filter the signal twice, and compare the signals to each other, rather than to an absolute value. The first filter gives us a "sample", reasonably free of noise but still fast-changing, and the second filter derives a slow-moving "baseline". We determine a pulse by looking at the difference between the sample and the baseline. In this way the absolute signal level doesn't matter.
I realised this was basically a single-pixel digital camera, and my solution was a (probably very naive!) form of automatic gain control. The plots above are a visualisation of the disc rotation at about 3000W: 5 pulses in a 33-second period. From top to bottom the plots are:
ampdiff
in the code).
You can see trailing edge of the pulses is sharper on the bottom plot. Because the baseline has started adjusting to the new level represented by the pulse, the end of the pulse causes a larger jump.
This example doesn't actually demonstrate the strength of the technique very well, because the ambient light was constant when I captured this, and because simply displaying the data in the first place implies we're doing some "ranging" to map the signal levels to colours. But it looks pretty cool.
I became a little obsessed with visualizing this stuff. Above are plots spanning about 30 seconds, showing a pulse
corresponding to about 430 W. The bottom plot shows the ampdiff
again, but this time the colour illustrates the sign:
red corresponds to the signal going below baseline, and green above.
Below is another view on the same data, also showing the moving baseline (red line, top), and the baseline-relative thresholds (orange, yellow, white lines).
The little square-wave plot right at the bottom shows the Arduino capturing the pulse. The small spikes correspond to "candidate" pulses where the signal has crossed the 3% threshold. If there are lots of these (as there are here) it's an indication the threshold may be too small for the signal. Or, the signal may be too noisy for the threshold!
I had to track power consumption for quite a while before I was confident the error rate was acceptable, because it turns out kWh don't accumulate all that quickly. Here's a chart showing consumption during the day. The area visible is about 10 kWh. The meter itself counts down to 0.1 kWh, but with a funky mechanism I was struggling to read it accurately to 1 kWh.
But I've left it for months now, and the error is below one kWh a month, or less than 1%. Opening the box to take a manual reading does disturb the sensor for a few seconds and cause a spurious pulse or two. It may be that this is the only source of error in practice.
With months of data a heatmap becomes an interesting visualisation. The figures are watts, averaged by hour. You can clearly see the evening peak, and the geyser cycling on every couple of hours in the middle of the night.