ffrouin
Published © GPL3+

Digital AC Clamp and meter [prototype]

Digital Energy Counter that can trigger events based on energy consumption or energy bill threshold.

BeginnerShowcase (no instructions)238
Digital AC Clamp and meter [prototype]

Things used in this project

Hardware components

Arduino UNO
Arduino UNO
×1
sct013 Split-core Current Transformer
×1
5 mm LED: Red
5 mm LED: Red
×1
5 mm LED: Yellow
5 mm LED: Yellow
×1
LED, Blue Green
LED, Blue Green
×1
Resistor 10k ohm
Resistor 10k ohm
×1
Through Hole Resistor, 200 ohm
Through Hole Resistor, 200 ohm
×2
Resistor 100 ohm
Resistor 100 ohm
×1
Tactile Switch, Top Actuated
Tactile Switch, Top Actuated
×1

Story

Read more

Schematics

Main circuit

Code

Prototype sketch

C/C++
static float mV = 4.8828125;
#define ANALOG_RAW_RESOLUTION 150

enum convert { none, seconds, minutes, hours, days };

class timer {
  private:
    bool                micro;
    unsigned long int   start;

  public:
      timer(bool _micro=false);

      void reset(void);
      float duration(convert _c=none);
      String report(convert _c=none, int _digits=0);
};

timer::timer(bool _micro) {
  micro = _micro;
  reset();
}

void timer::reset(void) {
  if (micro) {
    start=micros();
  } else {
    start=millis();
  }
}

float timer::duration(convert _c) {
  unsigned long int len = 0;

  if (micro) {
    len = micros() - start;
  } else {
    len = millis() - start;
  }

  switch(_c) {
    case none:
      return(len);
      break;
    case seconds:
      if (micro) {
        return(len/10000000);
      } else {
        return(len/1000);
      }
      break;
    case minutes:
      if (micro) {
        return(len/600000000);
      } else {
        return(len/60000);
      }
      break;
    case hours:
      if (micro) {
        return(len/36000000000);
      } else {
        return(len/3600000);
      }
      break;
  }
  return(0);
}

String timer::report(convert _c, int _digits) {
  String unit = "none";

  switch(_c) {
    case none:
      if (micro) {
        unit = "us";
      } else {
        unit = "ms";
      }
      break;
    case seconds:
      unit ="sec";
      break;
    case minutes:
      unit = "min";
      break;
    case hours:
      unit = "h";
      break;
  }
  
  return(String(duration(_c),_digits)+unit);
}

class analogSample {
  private:
      int     freq;
      float   timeout;
   
      double  vcc_offset;

      int devicePin;
      int vccDividerPin;
  
      int raw_values[ANALOG_RAW_RESOLUTION];
      unsigned long int raw_timestamp[ANALOG_RAW_RESOLUTION];

      int raw_max;
      int raw_min;
      int values;

      double avg_vcc_offset;
      double avg_vcc_offset_points;
      
      double avg_values;
      double avg_max;
      double avg_min;
      
  public: 
      analogSample(int _freq, int _divider_pin, int _device_pin);

      void readDividerValue(void);
      void resetSample(void);
      void readSample(void);
      void processSample(int _log_level);

      double getUEff(void);
      void report(void);
};

analogSample::analogSample(int _freq, int _divider_pin, int _device_pin) {
 freq =  _freq;

 vcc_offset = 0;

 vccDividerPin = _divider_pin;
 devicePin = _device_pin;
 
 timeout = 1000000/_freq;

 avg_vcc_offset = 99999;
 avg_vcc_offset_points = 99999; 
 
 avg_values = 99999;
 avg_max = 99999;
 avg_min = 99999;

 resetSample();
}

void analogSample::resetSample(void) {
   raw_min = 1024;
   raw_max = -1024;
   values = 0;


   avg_values = 99999;
   avg_max = 99999;
   avg_min = 99999;

   int counter;
   for (counter=0;counter<ANALOG_RAW_RESOLUTION;counter++) {
    raw_values[counter]=9999;
    raw_timestamp[counter]=0;
   }
}

void analogSample::readDividerValue(void) {
  
  int counter = 1;
  vcc_offset = analogRead(vccDividerPin);
  
  timer t(true);
  while(t.duration()<timeout*5) {
    vcc_offset += analogRead(vccDividerPin);
    counter++;
  }
  vcc_offset /= counter;
  
  if (avg_vcc_offset == 99999) {
    avg_vcc_offset = vcc_offset;
  } else {
    avg_vcc_offset += vcc_offset;
    avg_vcc_offset /= 2;
  }

  if (avg_vcc_offset_points == 99999) {
    avg_vcc_offset_points = counter;
  } else {
    avg_vcc_offset_points += counter;
    avg_vcc_offset_points /= 2;
  }
}

void analogSample::readSample(void) {
  int counter = 0;
  timer t(true);
  while(t.duration()<timeout*1.1 && counter<ANALOG_RAW_RESOLUTION) {
    raw_timestamp[counter] = micros();
    raw_values[counter++] = analogRead(devicePin); 
  }
  values = counter;
  if (avg_values == 99999) {
    avg_values = values;
  } else {
    avg_values += values;
    avg_values /= 2;
  }
}

void analogSample::processSample(int _log_level) {
  int counter = 0;

  raw_max = -1024;
  raw_min = 1024;
  
  if (_log_level > 2) {
      Serial.println();
      Serial.println("Analog Sample data table");
  }
  for (counter=0;counter<ANALOG_RAW_RESOLUTION;counter++) {
    if (raw_values[counter] == 9999) {
      break;
    }
    if (_log_level > 2) {
      Serial.println(String(counter)+","+String(raw_timestamp[counter])+","+String(raw_values[counter]));
    }
    if (raw_values[counter]<raw_min) {
      raw_min = raw_values[counter];
    }
    if (raw_values[counter]>raw_max) {
      raw_max = raw_values[counter];
    }
  }

  if ((avg_max||avg_min) == 99999) {
    avg_max = raw_max;
    avg_min = raw_min;
  } else {
    avg_max += raw_max;
    avg_max /= 2;
    avg_min += raw_min;
    avg_min /= 2;
  }
}

double analogSample::getUEff(void) {
  double ueff = mV*((avg_max-avg_min)/2)/sqrt(2);
  return(ueff);
}

void analogSample::report(void) { 
  Serial.print(" [VCC divider: " + String(mV*avg_vcc_offset) + "mV (" + String(avg_vcc_offset_points) + "pts)");
  Serial.print(" " + String(freq) + "Hz:" + String(avg_values) + "pts] ");
  Serial.print("("+String(mV*avg_max,2)+"-"+String(mV*avg_min,2)+"mV c="+String(mV*(avg_max-avg_min)/2));
  Serial.print("mV) Amp "+String((avg_max-avg_min)*mV,2) +"mV UEff: " +String(getUEff())+ "mV");
}

class sct013 {
  private:
    int vccDividerPin;
    double Ieff_calibration;

  public:  
    int devicePin;
    int deviceModel;

    bool calibrated;
    
    sct013(int _vcc_divider_pin, int _device_pin, int _device_model);

    void calibrate(int _length, int _freq, int _log_level=0);
    double probe(int _freq, int _log_level=0, bool _calibrate=false);
};

sct013::sct013(int _vcc_divider_pin, int _device_pin, int _device_model) {
  vccDividerPin = _vcc_divider_pin;
  devicePin = _device_pin;
  deviceModel = _device_model;

  calibrated = false;
  Ieff_calibration = 0;
}

void sct013::calibrate(int _length, int _freq, int _log_level) {
  timer t;
  int counter = 0;
  
  calibrated = true;

  if (_log_level > 0) {
    Serial.println("sct013::calibrate() calibration started, make sure the line is off");
  }
  
  Ieff_calibration = probe(_freq,_log_level,true);
  while(t.duration()<_length) {
    Ieff_calibration += probe(_freq,_log_level,true);
    Ieff_calibration /= 2;
    counter++;
  } 
  if (_log_level > 0) {
    Serial.println();
    if (_log_level > 1) {   
      Serial.println("Calibration data : " + String(Ieff_calibration) + "mA " + String(counter) + " pts.");
    }
    Serial.println("Calibration ended : Ieff noise " + String(Ieff_calibration) + "mA");
    Serial.println();
  }
}

double sct013::probe(int _freq, int _log_level, bool _calibrate) {
  double Ieff=0;
  
  if (!calibrated) {
    Serial.println("sct013::probe() WARNING device not calibrated()");
    return(0);
  }
  
  analogSample as(_freq,vccDividerPin,devicePin);
  int counter = 0;

  if (_log_level >= 1) {
    Serial.print("sct013 1V/" + String(deviceModel) + "A (pin A" + String(devicePin)+")");
  }
  
  as.readDividerValue(); 
  timer t;
  while(t.duration()<1000) {
     as.resetSample();
     as.readSample();
     as.processSample(_log_level);
     counter++;
  }

  if (_log_level >= 2) {
    as.report();
  }

  Ieff = as.getUEff()*deviceModel;
  
  if (_log_level >= 1) {
    Serial.print(" [" + String(counter) + " sample/sec]");
    Serial.println(" I=" + String(Ieff*2) + "mA "); // 2 phases a period Ieff needs x2
  }
  if (_calibrate) {
    return(Ieff*2);  // 2 phases a period Ieff needs x2
  }

  if (Ieff < Ieff_calibration*1.1) { // We take 110% of noise value to exclude noise from measures
    return(0);
  }
  return(Ieff*2); // 2 phases a period Ieff needs x2
}

class EnergyMonitor {
  private:
    int voltage;
    int freq;

    double pCount; // Wh
    
    unsigned long int timestamp;
   
  public:
    int cLed;
    int pLed;
    int wLed;
    double wCount;
    double wStep;
    bool useLeds;
  
    double pLast; // W
    double pAvg; // kWh
    double pMax; // kWh
    double pMin; // kWh
    
    EnergyMonitor(int _voltage, int _freq);

    double getPower(sct013 _device, int _log_level=0);
    double getPowerCount(int _log_level=0);
    double getBudget(float _kWh_cost=0.1740);

    void setupLeds(int _cLed, int _pLed, int _wLed, int _wStep);    
    void updateLeds(sct013 _device);
};

EnergyMonitor::EnergyMonitor(int _voltage, int _freq) {
  voltage=_voltage;
  freq = _freq;
  
  pLed= 9999;
  cLed = 9999;
  wLed = 9999;
  wStep = 9999;
  wCount = 0;
  useLeds = false;

  timestamp = 99999;

  pLast = 99999;
  pAvg = 99999;
  pMax = -99999;
  pMin = 99999;
  pCount = 0;
}

double EnergyMonitor::getPower(sct013 _device, int _log_level) {

  if (useLeds) {
    updateLeds(_device);
  }
  
  double mA = _device.probe(freq,_log_level);
  double p = voltage*mA/1000;
  double Wh = p;
  double kWh = p*24/1000;

  if (timestamp != 99999) {
    unsigned long int now = millis();
    double count = (now-timestamp)/1000;
    count /= 3600;
    count *= p;
    pCount += count;
  }
  timestamp = millis();

  pLast = p;

  if (pAvg == 99999) {
    pAvg = kWh;
  } else {
    pAvg += kWh;
    pAvg /= 2;
  }

  if (kWh < pMin) {
    pMin = kWh;
  }
  if (kWh > pMax) {
    pMax = kWh;
  }

  if (_log_level > 0 && p > 0) {
    Serial.print("EnergyMonitor pin A" + String(_device.devicePin) + ": " + String(p) +"W " + String(pAvg) + "kWh/day");
    Serial.println(" (" + String(pMin) + "-" + String(pMax)+"kWh/day)");
  }
  
  return(kWh);
}

double EnergyMonitor::getPowerCount(int _log_level) {
  if (_log_level > 0 && pLast > 0) {
    Serial.println("EnergyCounter : " + String(pCount/1000,6) + "kWh (" + String(getBudget(),8) + "€) Last=" + String(pLast) + "W [24h a day : Max=" + String(pMax) + "kWh avg=" + String(pAvg) + "kWh]");
  }
  return(pCount/1000);
}

double EnergyMonitor::getBudget(float _kWh_cost) {
  return(double(pCount/1000*_kWh_cost));
}

void EnergyMonitor::setupLeds(int _cLed, int _pLed, int _wLed, int _wStep) {
  cLed = _cLed;
  pLed = _pLed;
  wLed = _wLed;
  wStep = _wStep;
  useLeds = true;
}

void EnergyMonitor::updateLeds(sct013 _device) {
  if (_device.calibrated) {
    digitalWrite(cLed,HIGH);
  } else {
    int i;
    for (i=0;i<3;i++) {
      digitalWrite(cLed,HIGH);
      delay(150);
      digitalWrite(cLed,LOW);
      delay(150);
    }
  }

  if (floor(pCount/wStep) > wCount ) {
    wCount++;
    digitalWrite(wLed,HIGH);
    delay(800);
    digitalWrite(wLed,LOW);
  }
  
  if (pLast > 0) {
    digitalWrite(pLed, HIGH);
  } else {
    digitalWrite(pLed, LOW);
  }
  
}

static int POWER_FREQ=50;           // setup your electrical network frequency
static int POWER_VOLTAGE=220;       // setup your electrical network voltage for power accounting
static float POWER_ACCOUNTING=1;  // setup power accounting in Wh in order wLed to report

static int LOG_LEVEL0=0;             // setup log level from 0 (no logs) to 3 (all required data)
static int LOG_LEVEL1=1;
static int LOG_LEVEL2=2;
static int LOG_LEVEL3=3;

static int SCT013_CALIB_TIMEOUT=10000; // setup timeout for sct013 devices to calibrate

int calibration_button_pin = 2; // end-user can request calibration anytime

// declare all your sct013 devices
sct013 SCT013_01(1,5,10); // (vccDividerPin, devicePin, deviceModel)

// declare all your energy monitors
EnergyMonitor EM(POWER_VOLTAGE,POWER_FREQ);

void setup() {
  // initialize serial port to access reports
  Serial.begin(9600);
  Serial.println();
  Serial.println("Power Accounting and Triggering Engine v0.1 [08/11/2022 - freddy@linuxtribe.fr]");
  EM.setupLeds(12,13,8,POWER_ACCOUNTING); // leds enable report about state of device and power on monitored line (calibration, power on, end-user accounting settings)
}

void loop() {

// check if end-user request sct013 devices calibration
  if (digitalRead(calibration_button_pin) == HIGH) {
    // calibration measures electromagnetic noise induced by the circuit (looks to be dependant on AC supply). Bellow noise level, line is considered as off, ueff=0V
    SCT013_01.calibrate(SCT013_CALIB_TIMEOUT,POWER_FREQ,LOG_LEVEL2); // (time for calibration in milli seconds, frequence of monitored signal, log_level)   
  }
  
// ask energy monitor to probe sct013 device to get line state and proceed to power accounting
  EM.getPower(SCT013_01,LOG_LEVEL0); // (device to use for monitoring, log_level)
  EM.getPowerCount(LOG_LEVEL1);
}

Credits

ffrouin
3 projects • 4 followers
Open Source and Standard greatly help integration and maintenance costs. IT, 3D Printing and Electronic R&D, Repair : The Domestic Industry.
Contact

Comments

Please log in or sign up to comment.