Thursday, January 23, 2014

Whole House Power Monitor - Part 4 - Current Calibration

The voltage calibration was fairly easy, since it only has to work over a fairly narrow range, nominal 120V, plus or minus perhaps 10V.  

Current calibration covers a much wider range, from no load to max load.  The linearity and residual noise of the hardware made it impossible to get good results using the Arduino map() function, with only two calibration points.  I tried various combinations of no load or a small load for the lower calibration point; and various loads for the upper calibration point, but could never get good accuracy across the whole range, from no load to full load. 

In hindsight, I probably should have incorporated some better signal conditioning, but that's water under the bridge.  I was bound and determined to find a software solution, and in the end it worked out fine.

I created two current ranges in the software.  The lower current range covers no load to about 250W, and the upper range covers 250W to full load.  The cal standards for the lower range were a 75W light bulb and a 250W light bulb.  And for the upper range, I used the same 250W light bulb and a 1000W heat gun.  For reference I used the Kill-A-Watt current and power readings, also correlated the current readings with a Craftsman Current Clamp Meter.

After calibration, I spot checked a bunch of different loads.  On the low end, I used a 25W lightbulb.  It measured pretty low, about 15W, but I can live with that.  No load does read zero, so that's good.  I measured several loads ranging from 75W to 1000W, and got pretty good accuracy, within a few percent.

But I wanted to measure something much more substantial, and figured out a way to use a Weller soldering gun to create about 24A.  That's up in the range I expect my main breakers are carrying on average.  As shown in this YouTube video, the results were pretty good.

Regarding max load, the current clamps are rated for 600A, my main breakers are 200A, and I expect my max current will be under 50A.  The current clamps have a switch to select either 1mV/A, or 10mV/A, and I'm using the 10mV/A setting.  So at 50A, the current probe output will be 0.5V.  That's RMS, so the peak to peak will be about 1.4V.  Perhaps a bit more than that, depending on the shape of the current waveform.  The input range of the Moteino is 3.3V, so I think I've got plenty of headroom.

Here's the final sketch, with the calibration constants:


const char* NAME = "Moteino_Main_Tx ";
const char* VERSION = "1.414 ";
const char* DATE = "1/25/2014 ";

// Moteino_Main_Tx (using LowPowerLab RFM12b library)
// www.mikesmicromania.com
// LED on D9
// Phase 1 current sense to A1
// Phase 2 current sense to A2
// Phase 1 voltage sense to A3
// Phase 2 voltage sense to A4
// 60 Hz INT to D3 (INT1)
// Serial and Tx output @ 1Hz: ASCII string, e.g.
// "MAIN 1500 N 120.1 V1 3.2 A1 349.7 W1 119.5 V2 8.3 A2 919.3 W2"

#include <PString.h>   //arduiniana.org/libraries/PString/
#include <RFM12B.h>    //www.lowpowerlab.com

#define ShowRawA2D false // set to true for calibration measurements

// cal factors for converting raw A2D values to volts, amps, watts
// initial default values map 1 to 1 output raw A2D values 0 to 1023
const float CFrawV1low  = 214.0; // raw A2D value
const float CFrawV1high = 251.5;
const float CFcalV1low  = 110.0; // scaled Volts
const float CFcalV1high = 130.0; 

const float CFrawV2low  = 214.0; // raw A2D value
const float CFrawV2high = 251.5;
const float CFcalV2low  = 110.0; // scaled Volts
const float CFcalV2high = 130.0; 
  
const float CFrawA1low  = 2.75; // A2D value, 68W load
const float CFrawA1mid  = 7.28; // A2D value, 250W load
const float CFrawA1high = 27.7; // A2D value, 970W load
const float CFcalA1low  = .645; // Amps, 68W load
const float CFcalA1mid  = 2.26; // Amps, 250W load
const float CFcalA1high = 8.77; // Amps, 970W load

const float CFrawA2low  = 2.65; // A2D value, 68W load
const float CFrawA2mid  = 7.18; // A2D value, 250W load
const float CFrawA2high = 27.7; // A2D value, 970W load
const float CFcalA2low  = .645; // Amps, 68W load
const float CFcalA2mid  = 2.26; // Amps, 250W load
const float CFcalA2high = 8.77; // Amps, 970W load 

// radio settings
const int NODEID    =  3; // this node ID
const int GATEWAYID =  1; // send to node 1: Moteino Rx R-Pi
const int NETWORKID = 99; // Rx must be on same group to receive
const int myCS = 10;      // RFM12B chip select, SPI SS. 
                          // Pin 9 on my UNO shields. 10 on JeeNodes, Moteinos.

RFM12B radio;             // object, instance of class RFM12B 

// HW pin assignments
const int LED    = 9; // activity LED, 1 Hz
const int LEDBIT = 1; // activity LED, PORTB
const int A1SENS = 1; // phase 1 current: A1
const int A2SENS = 2; // phase 2 current: A2
const int V1SENS = 3; // phase 1 voltage: A3
const int V2SENS = 4; // phase 2 voltage: A4
const int HzSENS = 3; // IRQ = D3 (60Hz from Q1 collector)

// global constants
const int HzINT  = 1; // INT1, pin D3
const int MaxN  = 65; // max # readings (good down to 48 Hz)
const int NAVG = 30;  // average over 30 cylces, 0.5 seconds

void setup () { 
  radio.Initialize(NODEID, RF12_433MHZ, NETWORKID, 0, 8, RF12_2v75, myCS);
  pinMode(HzSENS, INPUT);      // D3 is IRQ1, active low 
  digitalWrite(HzSENS, HIGH);  // enable internal pullup
  pinMode(LED, OUTPUT);        // D9 drives LED to ground
  digitalWrite(LED, LOW);      // LOW =  LED off
  // speed up ADC to minimize phase error measuring voltage and current
  // arduino cookbook 2nd ed, pg 624
  bitClear(ADCSRA, ADPS0);
  bitClear(ADCSRA, ADPS1);
  bitSet(ADCSRA, ADPS2);  
  Serial.begin(115200);
  Serial.print(NAME);
  Serial.print(VERSION);
  Serial.println(DATE); 
}

volatile int unsigned measState; // state machine, clocked by line frequency

void loop () {
  //Serial.println("loop()");
  int V_1[MaxN], A_1[MaxN], V_2[MaxN], A_2[MaxN];  // waveform arrays
  int KAW_N = 0;
  float KAW_V1=0, KAW_A1=0, KAW_V2=0, KAW_A2=0, KAW_W1=0, KAW_W2=0;
  for(int i = 0; i < NAVG; i++){
    // acquire voltage and current samples
    int n = 0;
    if(i > 0)
      measState = 1; // 1 is partial half cycle
    else
      measState = 0; // first attachInterrupt take longer
    
    attachInterrupt(HzINT, AC60HZ, FALLING);
    while(measState <= 1);  // skip cycle 0 and 1
    //digitalWrite(LED,HIGH);
    //bitSet(PORTB, LEDBIT);
    while((measState <= 2) && (n < MaxN)) { // measure 1 cycle
      //digitalWrite(LED,HIGH);
      //bitSet(PORTB, LEDBIT);     // 0
      A_1[n] = analogRead(A1SENS); // 16.8 us
      V_1[n] = analogRead(V1SENS); // 16.8 us
      A_2[n] = analogRead(A2SENS); // 16.8 us
      V_2[n] = analogRead(V2SENS); // 16.8 us
      //digitalWrite(LED,LOW);
      //bitClear(PORTB, LEDBIT);   // 68 us
      n++;
      // time difference between voltage and current readings is about 20.4 us
      // 16.8 us is about 0.44 degrees at 60 Hz, good enough!
      // target 50 quad-readings over 1 cycle @ 60Hz
      // total reading time: 4 * 16.8 us * 50 = 3.4 ms
      // total dead time: (1/60) - 3.4 ms = 13.2666 ms
      // dead time between readings, for 50 readings: 
      // 13.2666 ms / 50 = 265 us
      delayMicroseconds(265); // fine tuned for 50 readings @ 60 Hz
    } 
    //digitalWrite(LED,LOW);
    //bitClear(PORTB, LEDBIT);
    detachInterrupt(HzINT);
    KAW_N += n; // number of readings
    // compute offset levels
    float V1Sum=0, A1Sum=0, V2Sum=0, A2Sum=0, W1Sum=0, W2Sum=0;
    for(int i=0; i<n; i++){
      V1Sum += V_1[i];
      A1Sum += A_1[i];  
      V2Sum += V_2[i];
      A2Sum += A_2[i];   
    }
    
    float aV1, aA1, aV2, aA2; // averages = DC mean values
    aV1 = V1Sum / n;
    aA1 = A1Sum / n;
    aV2 = V2Sum / n;
    aA2 = A2Sum / n;
    
    V1Sum = A1Sum = V2Sum = A2Sum = 0; // compute std.dev, AC RMS
    for(int i=0; i<n; i++){
      V1Sum += (V_1[i] - aV1) * (V_1[i] - aV1); // dV1^2
      A1Sum += (A_1[i] - aA1) * (A_1[i] - aA1); // dA1^2
      V2Sum += (V_2[i] - aV2) * (V_2[i] - aV2); // dV2^2
      A2Sum += (A_2[i] - aA2) * (A_2[i] - aA2); // dA2^2
      W1Sum += (V_1[i] - aV1) * (A_1[i] - aA1); // dV1*dA1
      W2Sum += (V_2[i] - aV2) * (A_2[i] - aA2); // dV2*dA2
    }
     
    KAW_V1 += sqrt(V1Sum / n);  // root(mean(V1^2))
    KAW_A1 += sqrt(A1Sum / n);  // root(mean(A1^2))
    KAW_V2 += sqrt(V2Sum / n);  // root(mean(V2^2))
    KAW_A2 += sqrt(A2Sum / n);  // root(mean(A2^2))
    KAW_W1 += W1Sum / n;        // mean(sum(dV1[i]*dA1[i]))
    KAW_W2 += W2Sum / n;        // mean(sum(dV2[i]*dA2[i]))
  }
  
  KAW_V1 /= NAVG; // average all measurements
  KAW_A1 /= NAVG;
  KAW_V2 /= NAVG;
  KAW_A2 /= NAVG;
  KAW_W1 /= NAVG;
  KAW_W2 /= NAVG;

  if(!ShowRawA2D)
  {  // apply cal factors
  
    // voltage
    KAW_V1 = map(KAW_V1, CFrawV1low, CFrawV1high, CFcalV1low, CFcalV1high); 
    KAW_V2 = map(KAW_V2, CFrawV2low, CFrawV2high, CFcalV2low, CFcalV2high); 
    
    // current & power, phase 1
    if(KAW_A1 < CFrawA1mid * .9) // use lower current range for loads under about 225 watts
    { 
      KAW_A1 = map(KAW_A1, CFrawA1low, CFrawA1mid, CFcalA1low, CFcalA1mid);
      KAW_W1 = map(KAW_W1, CFrawV1low * CFrawA1low, CFrawV1high * CFrawA1mid, CFcalV1low * CFcalA1low, CFcalV1high * CFcalA1mid);
    }
    else // use upper current range for loads above about 200W
    {
      KAW_A1 = map(KAW_A1, CFrawA1mid, CFrawA1high, CFcalA1mid, CFcalA1high);
      KAW_W1 = map(KAW_W1, CFrawV1low * CFrawA1mid, CFrawV1high * CFrawA1high, CFcalV1low * CFcalA1mid, CFcalV1high * CFcalA1high);
    }
    
    // current & power, phase 2
    if(KAW_A2 < CFrawA2mid * .9) // use lower current range for loads under about 225 watts
    { 
      KAW_A2 = map(KAW_A2, CFrawA2low, CFrawA2mid, CFcalA2low, CFcalA2mid);
      KAW_W2 = map(KAW_W2, CFrawV2low * CFrawA2low, CFrawV2high * CFrawA2mid, CFcalV2low * CFcalA2low, CFcalV2high * CFcalA2mid);
    }
    else // use upper current range for loads above about 200W
    {
      KAW_A2 = map(KAW_A2, CFrawA2mid, CFrawA2high, CFcalA2mid, CFcalA2high);
      KAW_W2 = map(KAW_W2, CFrawV2low * CFrawA2mid, CFrawV2high * CFrawA2high, CFcalV2low * CFcalA2mid, CFcalV2high * CFcalA2high);
    }
    
    // power
    //KAW_W1 = map(KAW_W1, CFrawV1lo*CFrawA1lo, CFrawV1hi*CFrawA1hi, CFcalV1lo*CFcalA1lo, CFcalV1hi*CFcalA1hi);
    //KAW_W2 = map(KAW_W2, CFrawV2lo*CFrawA2lo, CFrawV2hi*CFrawA2hi, CFcalV2lo*CFcalA2lo, CFcalV2hi*CFcalA2hi);
    
    // prevent negative values (e.g. no load current, plus noise)
    if(KAW_A1 < 0.4)
      KAW_A1 = KAW_W1 = 0;
    if(KAW_A2 < 0.4)
      KAW_A2 = KAW_W2 = 0;  
  }
  
  // format Tx string like: "Main 1500 N 120.1 V1 3.0 A1 300 W1 120.1 V2 3.0 A2 300 W2"
  char buffer[67];
  PString str(buffer, sizeof(buffer));
  str.print("Main ");
  str.print(KAW_N);
  str.print(" N ");
  str.print(KAW_V1);
  str.print(" V1 ");
  str.print(KAW_A1);
  str.print(" A1 ");
  str.print(KAW_W1);
  str.print(" W1 ");
  str.print(KAW_V2);
  str.print(" V2 ");
  str.print(KAW_A2);
  str.print(" A2 ");
  str.print(KAW_W2);
  str.print(" W2 ");
  
  //Serial.println(str);
  radio.Send(GATEWAYID, str, str.length());
  
  // flash LED so we know we're alive
  //digitalWrite(LED,HIGH);
  bitSet(PORTB, LEDBIT);   
  delay(10);
  bitClear(PORTB, LEDBIT); 
  //digitalWrite(LED,LOW);  // save power
}

void AC60HZ(){ 
  //digitalWrite(LED,HIGH);
  //bitSet(PORTB, LEDBIT);
  measState++;
  //Serial.print("measState:");
  //Serial.println(measState);
  //digitalWrite(LED,LOW);
  //bitClear(PORTB, LEDBIT);
}

// overload Arduino integer map() function to support floats, doubles
double map(double x, double in_min, double in_max, double out_min, double out_max)
{
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

Sunday, January 19, 2014

Whole House Power Monitor - Part 3 - Voltage Calibration

I just posted video about voltage calibration:


http://www.youtube.com/watch?v=_YdMvKDjalU


The Main_Moteino_Tx sketch makes measurements on four analog inputs: the two main voltages and line currents.  Each input is sampled at 3 KHz over 30 AC line cycles, resulting in 1500 samples (@60 Hz).  The mean, or average, and the standard deviation from the mean, or AC rms, is calculated on each channel, using raw A2D values.  The results for each cycle are averaged over 30 cycles.  Finally, the Arduino map() function is used to get calibrated volts and amps.  Concurrently, true watts is calculated on each pair of voltage and current samples.

The map() function is a linear transform, so the accuracy of the calibration is dependent on the linearity of the measurement hardware.  Voltage is nominally 120V, and typically within a few, to at most several, volts.  This makes it relatively easy on the hardware.  I calibrated at 110V and 130V.  Final measurements were made at 110V, 120V, and 130V, and all the readings were within +/- 1 volt.  Works for me.

Next up: calibrating current, and power readings.


Tuesday, January 14, 2014

Wireless Whole House Power Monitor - Part 2 - Software

Here's the sketch for the Wireless Whole House Power Monitor.  About once per second, 1500 measurements are taken across four channels, connected to both AC line voltage phases, and two AC current clamps around the main conductors.  These readings are formatted into ASCII text and transmitted, for example:

"MAIN 1500 N 120.1 V1 3.2 A1 349.7 W1 119.5 V2 8.3 A2 919.3 W2"

Here's the sketch.  Note, this is before calibration, which I'll discuss in my next post.

Moteino_Main_Tx.ino


const char* NAME = "Moteino_Main_Tx ";
const char* VERSION = "0.7 ";
const char* DATE = "1/9/2014 ";

// Moteino_Main_Tx (using LowPowerLab RFM12b library)
// www.mikesmicromania.com
// LED on D9
// Phase 1 current sense to A1
// Phase 2 current sense to A2
// Phase 1 voltage sense to A3
// Phase 2 voltage sense to A4
// 60 Hz INT to D3 (INT1)
// Serial and Tx output @ 1Hz: ASCII string, e.g.
// "MAIN 1500 N 120.1 V1 3.2 A1 349.7 W1 119.5 V2 8.3 A2 919.3 W2"

#include <PString.h>   //arduiniana.org/libraries/PString/
#include <RFM12B.h>    //www.lowpowerlab.com

// cal factors for converting raw A2D values to volts, amps, watts
// initial default values map 1 to 1 output raw A2D values 0 to 1023
const float CFrawV1lo = 110.0; // raw A2D value
const float CFrawV1hi = 130.0;
const float CFcalV1lo = 110.0; // scaled Volts
const float CFcalV1hi = 130.0; 

const float CFrawV2lo = 110.0; // raw A2D value
const float CFrawV2hi = 130.0;
const float CFcalV2lo = 110.0; // scaled Volts
const float CFcalV2hi = 130.0; 

const float CFrawA1lo = 2.3;  // raw A2D value
const float CFrawA1hi = 9.7;
const float CFcalA1lo = 2.3;  // scaled Amps
const float CFcalA1hi = 9.7;  

const float CFrawA2lo = 2.3;  // raw A2D value
const float CFrawA2hi = 9.7; 
const float CFcalA2lo = 2.3;  // scaled Amps 
const float CFcalA2hi = 9.7;  

// radio settings
const int NODEID    =  3; // this node ID
const int GATEWAYID =  1; // send to node 1: Moteino Rx R-Pi
const int NETWORKID = 99; // Rx must be on same group to receive
const int myCS = 10;      // RFM12B chip select, SPI SS. 
                          // Pin 9 on my UNO shields. 10 on JeeNodes, Moteinos.

RFM12B radio;             // object, instance of class RFM12B 

// HW pin assignments
const int LED    = 9; // activity LED, 1 Hz
const int LEDBIT = 1; // activity LED, PORTB
const int A1SENS = 1; // phase 1 current: A1
const int A2SENS = 2; // phase 2 current: A2
const int V1SENS = 3; // phase 1 voltage: A3
const int V2SENS = 4; // phase 2 voltage: A4
const int HzSENS = 3; // IRQ  = D3 (Red wire from Q3 collector, 60Hz)

// global constants
const int HzINT  = 1; // INT1
const int MaxN  = 65; // max # readings (good down to 48 Hz)
const int NAVG = 30;

void setup () { 
  radio.Initialize(NODEID, RF12_433MHZ, NETWORKID, 0, 8, RF12_2v75, myCS);
  pinMode(HzSENS, INPUT);      // D3 is IRQ1, active low 
  digitalWrite(HzSENS, HIGH);  // enable internal pullup
  pinMode(LED, OUTPUT);        // D9 drives LED to ground
  digitalWrite(LED, LOW);      // LOW = off
  // speed up ADC to minimize phase error measuring voltage and current
  // arduino cookbook 2nd ed, pg 624
  bitClear(ADCSRA, ADPS0);
  bitClear(ADCSRA, ADPS1);
  bitSet(ADCSRA, ADPS2);  
  Serial.begin(115200);
  Serial.print(NAME);
  Serial.print(VERSION);
  Serial.println(DATE); 
}

volatile int unsigned measState; // state machine, clocked by line frequency

void loop () {
  //Serial.println("loop()");
  int V_1[MaxN], A_1[MaxN], V_2[MaxN], A_2[MaxN];  // waveform arrays
  int KAW_N = 0;
  float KAW_V1=0, KAW_A1=0, KAW_V2=0, KAW_A2=0, KAW_W1=0, KAW_W2=0;
  for(int i = 0; i < NAVG; i++){
    // acquire voltage and current samples
    int n = 0;
    if(i > 0)
      measState = 1; // 1 is partial half cycle
    else
      measState = 0; // first attachInterrupt take longer
    
    attachInterrupt(HzINT,AC60HZ,FALLING);
    while(measState <= 1);  // skip cycle 0 and 1
    //digitalWrite(LED,HIGH);
    //bitSet(PORTB, LEDBIT);
    while((measState <= 2) && (n < MaxN)) { // measure 1 cycle
      //digitalWrite(LED,HIGH);
      //bitSet(PORTB, LEDBIT);     // 0
      A_1[n] = analogRead(A1SENS); // 16.8 us
      V_1[n] = analogRead(V1SENS); // 16.8 us
      A_2[n] = analogRead(A2SENS); // 16.8 us
      V_2[n] = analogRead(V2SENS); // 16.8 us
      //digitalWrite(LED,LOW);
      //bitClear(PORTB, LEDBIT);   // 68 us
      n++;
      // time difference between voltage and current readings is about 20.4 us
      // 16.8 us is about 0.44 degrees at 60 Hz, good enough!
      // target 50 quad-readings over 1 cycle @ 60Hz
      // total reading time: 4 * 16.8 us * 50 = 3.4 ms
      // total dead time: (1/60) - 3.4 ms = 13.2666 ms
      // dead time between readings, for 50 readings: 
      // 13.2666 ms / 50 = 265 us
      delayMicroseconds(265); // fine tuned for 50 readings @ 60 Hz
    } 
    //digitalWrite(LED,LOW);
    //bitClear(PORTB, LEDBIT);
    detachInterrupt(HzINT);
    KAW_N += n; // number of readings
    // compute offset levels
    float V1Sum=0, A1Sum=0, V2Sum=0, A2Sum=0, W1Sum=0, W2Sum=0;
    for(int i=0; i<n; i++){
      V1Sum += V_1[i];
      A1Sum += A_1[i];  
      V2Sum += V_2[i];
      A2Sum += A_2[i];   
    }
    
    float aV1, aA1, aV2, aA2; // averages = DC mean values
    aV1 = V1Sum/n;
    aA1 = A1Sum/n;
    aV2 = V2Sum/n;
    aA2 = A2Sum/n;
    
    V1Sum = A1Sum = V2Sum = A2Sum = 0; // compute std.dev, AC RMS
    for(int i=0; i<n; i++){
      V1Sum += (V_1[i]-aV1)*(V_1[i]-aV1); // dV1^2
      A1Sum += (A_1[i]-aA1)*(A_1[i]-aA1); // dA1^2
      V2Sum += (V_2[i]-aV2)*(V_2[i]-aV2); // dV2^2
      A2Sum += (A_2[i]-aA2)*(A_2[i]-aA2); // dA2^2
      W1Sum += (V_1[i]-aV1)*(A_1[i]-aA1); // dV1*dA1
      W2Sum += (V_2[i]-aV2)*(A_2[i]-aA2); // dV2*dA2
    }
     
    KAW_V1 += sqrt(V1Sum/n);  // root(mean(V1^2))
    KAW_A1 += sqrt(A1Sum/n);  // root(mean(A1^2))
    KAW_V2 += sqrt(V2Sum/n);  // root(mean(V2^2))
    KAW_A2 += sqrt(A2Sum/n);  // root(mean(A2^2))
    KAW_W1 += W1Sum/n;        // mean(sum(dV1[i]*dA1[i]))
    KAW_W2 += W2Sum/n;        // mean(sum(dV2[i]*dA2[i]))
  }
  
  KAW_V1 /= NAVG; // average all measurements
  KAW_A1 /= NAVG;
  KAW_V2 /= NAVG;
  KAW_A2 /= NAVG;
  KAW_W1 /= NAVG;
  KAW_W2 /= NAVG;
  
// apply cal factors
  // voltage
  KAW_V1 = map(KAW_V1, CFrawV1lo, CFrawV1hi, CFcalV1lo, CFcalV1hi); 
  KAW_V2 = map(KAW_V2, CFrawV2lo, CFrawV2hi, CFcalV2lo, CFcalV2hi); 
  
  // current
  KAW_A1 = map(KAW_A1, CFrawA1lo, CFrawA1hi, CFcalA1lo, CFcalA1hi);  
  KAW_A2 = map(KAW_A2, CFrawA2lo, CFrawA2hi, CFcalA2lo, CFcalA2hi);  
    
  // power
  KAW_W1 = map(KAW_W1, CFrawV1lo*CFrawA1lo, CFrawV1hi*CFrawA1hi, CFcalV1lo*CFcalA1lo, CFcalV1hi*CFcalA1hi);
  KAW_W2 = map(KAW_W2, CFrawV2lo*CFrawA2lo, CFrawV2hi*CFrawA2hi, CFcalV2lo*CFcalA2lo, CFcalV2hi*CFcalA2hi);
  
  // prevent negative values (e.g. no load current, plus noise)
  KAW_A1 = max(KAW_A1, 0);
  KAW_W1 = max(KAW_W1, 0);  
  KAW_A2 = max(KAW_A2, 0);
  KAW_W2 = max(KAW_W2, 0);  
  
  // format Tx string, e,g.
  // "Main 1500 N 120.1 V1 3.0 A1 300 W1 120.1 V2 3.0 A2 300 W2"
  char buffer[67];
  PString str(buffer, sizeof(buffer));
  str.print("Main ");
  str.print(KAW_N);
  str.print(" N ");
  str.print(KAW_V1);
  str.print(" V1 ");
  str.print(KAW_A1);
  str.print(" A1 ");
  str.print(KAW_W1);
  str.print(" W1 ");
  str.print(KAW_V2);
  str.print(" V2 ");
  str.print(KAW_A2);
  str.print(" A2 ");
  str.print(KAW_W2);
  str.print(" W2 ");
  
  //Serial.println(str);
  radio.Send(GATEWAYID, str, str.length());
  
  // flash LED so we know we're alive
  //digitalWrite(LED,HIGH);
  bitSet(PORTB, LEDBIT);   
  delay(10);
  bitClear(PORTB, LEDBIT); 
  //digitalWrite(LED,LOW);  // save power
}

void AC60HZ(){ 
  //digitalWrite(LED,HIGH);
  //bitSet(PORTB, LEDBIT);
  measState++;
  //Serial.print("measState:");
  //Serial.println(measState);
  //digitalWrite(LED,LOW);
  //bitClear(PORTB, LEDBIT);
}

// overload Arduino integer map() function to support floats, doubles
double map(double x, double in_min, double in_max, double out_min, double out_max)
{
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

Sunday, January 12, 2014

Wireless Whole House Power Monitor - Part 1 - Hardware

This post is long overdue.  My Wireless Whole House Power Monitor (WWHPM) has been up and running for many months now.  Actually, it's not installed in my circuit-breaker panel yet, but it's been running on the bench, in my PanaVise, and sending measurements to my Raspberry-Pi wireless base station project.  I'll cover that in a future blog, detailing my adventures in Python, with a smart data-logger class that automatically summarizes and reduces data over time.

WARNING and DISCLAIMER: this project is for educational purposes only.  Working in or around AC utility lines is inherently dangerous.  The reader assumes all responsibility for complying with local and national electrical codes.  Consult a licensed  electrician about connecting this or any other project to the AC utility lines.

The hardware design of the WWHPM is pretty straightforward and features transformer coupling for the two voltages and two currents.  The only DC connection to the AC utility lines is the safety ground.  The voltages are measured using small 4.5Vrms transformers, and the currents are measured using AC current clamps.  These signals are attenuated and fed into analog inputs on Arduino-compatible Moteino.  Note, my Moteinos are R2, using the RFM12B.  On the new R4 Moteinos, you can choose the RFM12B or RFM69W.






Everything to left of the dashed line will be inside the main circuit-breaker panel.  The project box is plastic, attached side by side to a  standard wiring box.  The AC voltage lines should be wired using standard Romex cable, to two circuit breakers on each phase.  The AC current clamps are low voltage signals, and per the national electrical code they should run through a separate conduit.  

Each analog input is protected by diode clamps, D1 through D8, in case of line surges, transient spikes, etc.  Likewise, fuses F1 though F6 provide over-current protection.

The current clamps are sold as an accessory to a DMM, so they already have the burden resistor inside, and they output a voltage proportional to the current.  R1 and C1, and R2 and C2, serve as low pass filters to improve SNR, and the resistors also limit the current in case the clamp diodes ever conduct.

Transformers T1 and T2 provide 4.5 Vrms, which is attenuated by R3 and R4, and R5 and R6, respectively.  Note: the analog input impedance is about 100 megohms, so no significant load on the circuit.  A third transformer, T3 provides the DC power supply current and the 60 Hz (or 50 Hz) interrupt signal via Q1.  

The power supply is pretty basic, with bridge rectifier BR1 and filter cap C4 to provide a nominal +4.5Vdc to the Moteino Vin.  R8 forward biases D10 and D11, to create a nominal +1.8Vdc.  This voltage provides a "pedestal" or baseline voltage for the AC voltage and current signals, keeping the analog inputs within their 0 to 3.3V dynamic range.

And that's pretty much it.  The rest as they say, is all software.

YouTube video: http://youtu.be/REFGZdT9ghU