Skip to content

Instantly share code, notes, and snippets.

@phec
Last active January 31, 2017 23:21
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save phec/9254793 to your computer and use it in GitHub Desktop.
Save phec/9254793 to your computer and use it in GitHub Desktop.
Port of Open Energy Monitor emonlib to Spark SemonLib.cpp
//energy display v1.0 for Arduino pro mini
//receives strings from Pi powerControl2_01.py
//if they start with Arduino they are processed
// and the Arduino returns the switch state
// 0 = auto 1 = manual on 2 = manual off
//if no input is received for 10 minutes the display reports
// loss of Pi signal
//display modified to show:
//.........!..........
//Auto on Cost -0.69
//imp 1.77 ! 2.79-1.02
// bar graph
//gas24 5.5!
//.........!..........
//Input codes:
//ArduinoDn + n real values
//ArduinoM + message to show
#include <LiquidCrystal.h>
#define STATE_AUTO 1
#define STATE_ON 2
#define STATE_OFF 3
// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(12, 11, 6, 7, 8, 9);
const int switchPin = 10; // this pin detects user input to manually override the auto immersion settings
const int immersionPin = 13; // this pin could be used to switch a relay. I just pass the immersion on request to a PC over the USB link
const long DEBOUNCE = 500.0; // make debounce quite long so not triggered by button release
const long BUTBOUNCE = 100.0; // time to wait for button debounce
const long PiTimeout = 600000; // 10 minute timeout for messages from Pi
const long updateLCD = 1000; //update lcd every second
//String inStr, message;
char message[64];
boolean PiOK;
long messageTime;
long LCDtime;
int lastScreen = 0; //note last screen displayed clear display if changed
volatile long unsigned int lastSwitchTime; //time switch was pressed last
volatile long unsigned int thisSwitchTime; //current switch time
volatile int switchState = STATE_AUTO; //switch state
int lastButtonState = HIGH;
int swProcessed = 0;
volatile int lcdState = 1; //LCD state set by long press
//define lcd variables
//////////////////////////////
// define lcd characters
byte exc1[8] = {
16,16,16,16,16,16,16,16};
byte exc2[8] = {
24,24,24,24,24,24,24,24};
byte exc3[8] = {
28,28,28,28,28,28,28,28};
byte exc4[8] = {
30,30,30,30,30,30,30,30};
byte exc5[8] = {
1,1,1,1,1,1,1,1};
byte exc6[8] = {
3,3,3,3,3,3,3,3};
byte exc7[8] = {
7,7,7,7,7,7,7,7};
byte exc0[8] = {
15,15,15,15,15,15,15,15};
// exclamation defined below - replaced by extra negative bar characters
//byte exc6[8] = {
// 12,30,30,30,30,30,12,0};
//byte exc7[8] = {
// 0,12,30,30,12,0,0,0};
// array to hold lcd characters
char barplus[6];
char barminus[6];
// define energy variables
float powerGas;
float powerGen;
float powerExp;
float powerUse;
float avgGas;
float earnedVal;
int immersionOn;
////////////////////////////////////////////////////////////////////
// function plotbar()
int plotBar(float start, float finish){
const int WIDTH = 20;
const float MIN = -4; //range of plot values
const float MAX = 4; //should probably put in function parameters
float scale, s, f;
// scale start and finish to s and f measured in Bar units
// i.e. first point is 0, last is 15 or 19 or WIDTH-1 of display
// don't use map() which returns integer values
scale = 1.0*WIDTH/(MAX - MIN);
s = (start - MIN) * scale;
if (s<0) s=0;
f = (finish - MIN) * scale;
if (f>WIDTH) f=WIDTH;
if (s > f) return 1;
// deal with case where lass than a full bar is filled
// keep start correct - so surplus is correct and round up gen
if ((f - s) < 1){
float used = f - s;
f = ceil(f);
s = f - used;
}
// step across display width
lcd.setCursor(0,2);
for (int i = 0; i<WIDTH;i++){
if ((i < (s-1)) || (i > f)) {
if (i == WIDTH/2-1) lcd.print(barminus[1]);
else lcd.print(' ');
}
else if ((s - i) > 0) lcd.print(barminus[int(5*(1-s+i))]);
else if ((f - i) < 1) lcd.print(barplus[int(5*(f-i))]);
else lcd.print(char(255));
}
if (start < MIN){
lcd.setCursor(2,2);
lcd.print(-start);
lcd.print("kW");
}
return 0;
}//end of plotBar
////////////////////////////////////////////////////////////////////
// switch cycles through manual states and returns 1 if long press
int swInt(){ // poll version
int reading = digitalRead(switchPin);
if ((reading == 0)&&(lastButtonState ==1)){//button just pressed
lastSwitchTime = millis();//start time
thisSwitchTime=lastSwitchTime;//reset finish time
lastButtonState = 0;
swProcessed = 0;//clear processed flag
}
if ((reading == 1)&&(lastButtonState == 0)){//button up
thisSwitchTime = millis();//stop time
lastButtonState = 1;
}
int pressTime = thisSwitchTime-lastSwitchTime;
if ((pressTime>BUTBOUNCE)&&(swProcessed==0)){//length of press
if(pressTime<1000){
switchState++;
if (switchState>STATE_OFF) switchState=STATE_AUTO;
swProcessed = 1;
}//short press
else{
lcdState = !lcdState;
swProcessed = 1;
}//long press
}
return pressTime;
}//swint
void setup() {
lcd.createChar(0,exc0);
lcd.createChar(1,exc1);
lcd.createChar(2,exc2);
lcd.createChar(3,exc3);
lcd.createChar(4,exc4);
lcd.createChar(5,exc5);
lcd.createChar(6,exc6);
lcd.createChar(7,exc7);
//setup char arrays
barplus[0] =' ';
barplus[1] = char(1);
barplus[2] = char(2);
barplus[3] = char(3);
barplus[4] = char(4);
barplus[5] = char(255);
barminus[0] = ' ';
barminus[1] = char(5);
barminus[2] = char(6);
barminus[3] = char(7);
barminus[4] = char(0);
barminus[5] = ' ';
lcd.begin(20, 4);
lcd.print(" Starting");
lastSwitchTime = millis();
messageTime = millis();
LCDtime = millis();
strcpy(message,"");
pinMode(immersionPin, OUTPUT);
pinMode(switchPin, INPUT_PULLUP);
//attachInterrupt(switchPin-2, swInt, FALLING);//switch is on pin 10 at
//moment. move when poss to 2
Serial.begin(9600);
lcdState = 1; //normal display
//////////////////////////
// DEBUG
// temporarily assign values to powers
powerGas = 8.8;
powerGen = 1.5;
powerExp = 0.5;
avgGas = 2.22;
earnedVal = 0.0;
immersionOn = 0;
}
void loop() {
//obtain values, infer use and display
powerUse = powerExp + powerGen;
swInt();
if (millis() > messageTime + PiTimeout) {//lost Pi signal
if (lastScreen != 3) lcd.clear();
lcd.setCursor(0,1);
lcd.print("No message from Pi");
lastScreen = 3;
delay(100);
}
else { // Pi is still sending stuff
if (millis() > LCDtime + updateLCD){//time to update LCD
LCDtime = millis();
//////////////////////////////////////////////////////
// display update can be slow - every 10 secs or so
// except for the response to the button click
if (message[0]!=0){ //if theres a message show it
if (lastScreen !=2) lcd.clear();
lastScreen = 2;
lcd.setCursor(0,1);
lcd.print(message);
//delay(10);
}
else { //show power display
// we can flip between two displays by pressing the button for more than a second
if (lcdState==1){
if (lastScreen != 1) lcd.clear();
lastScreen = 1;
lcd.setCursor(0,0);
if ((switchState==STATE_AUTO) &&immersionOn) lcd.print("Auto on ");
else if ((switchState==STATE_AUTO) &&!immersionOn)lcd.print("Auto off ");
else if (switchState==STATE_ON) lcd.print("Man: on ");
else if (switchState==STATE_OFF) lcd.print("Man: off ");
lcd.print(" Cost ");
lcd.print(earnedVal, 2);
lcd.setCursor(0,1);
if (powerExp>0) lcd.print("Imp ");
else lcd.print("Exp ");
lcd.print(abs(powerExp),2);
lcd.print(" ");
lcd.setCursor(9,1);
lcd.print(barminus[1]);
lcd.print(" ");
lcd.setCursor(11,1);
lcd.print(powerUse, 2);
lcd.print("-");
lcd.print(powerGen, 2);
lcd.setCursor(0,3);
lcd.print("Gas24 ");
lcd.print(avgGas, 2);
lcd.setCursor(9,3);
lcd.print(barminus[1]);
///////////////////////////////////
// plot Gen bar
plotBar(-powerExp,powerGen);
/////////////////////////////////////
// this bit needs to be quick to get feedback from button press
// LCD display of switch state updated every time round loop
}//end of switch state 1
else{
if (lastScreen !=4) lcd.clear();
lastScreen = 4;
lcd.setCursor(0,1);
lcd.print(" Alternate Screen");
}//end of lcdState 0
}//end of lcd update
}
if (immersionOn){
digitalWrite(immersionPin,HIGH);
}
else{
digitalWrite(immersionPin,LOW);
};
////////////////////////////////////////
//DEBUG message to lcd
//lcd.setCursor(0,2);
//lcd.print(message);
//delay(100);
} //end of have valid Pi data within last 5 mins
//delay(100);
} //end of loop
void serialEvent(){
// get Serial input if available
if (Serial.available()>8){
if(Serial.findUntil("Arduino","\n")){
//have Arduino message so wipe any old message
memset(&message[0], 0, sizeof (message));
messageTime = millis();
delay(10);
char command = Serial.read();
switch (command){
case 'M':
{
// read message and print it
if(Serial.available()){
Serial.readBytesUntil('\n',message,sizeof(message));
Serial.println(switchState); //send data back to Pi
//strcpy(message,"test message");
}
break;
}
case 'D':
{
// read data
while(Serial.available()<5); //block till next 5 characters arrive
powerGas = Serial.parseFloat();
powerGen = Serial.parseFloat();
powerExp = Serial.parseFloat();
avgGas = Serial.parseFloat();
earnedVal = Serial.parseFloat();
immersionOn = Serial.parseInt();
Serial.println(switchState); //send data back to Pi
//DEBUG
lcd.setCursor(0,3);
lcd.print(switchState);
break;
}
}//end of switch on command
}//end of received Arduino command
}//end of if serial available
}
# module to handle max avg files and data
# modifield for more frequent avg data points - change file names
import numpy
import cPickle as pickle
lastfile = ''
lastbin = 0
N = 0
f_handle = None
gen24Interval = 1
#constants
DEBUG = False
print ("myFile initiallised")
# load data files
def loadPVdata():
global maxP, avgP, useP,netP, count, gen24, mtr24, avGasP, gas24
'''Load max and average data from text files.'''
try:
maxP = numpy.loadtxt('/media/HD-EU2/PVstuff/PVdata/PVmaxq.txt')
avgP = numpy.loadtxt('/media/HD-EU2/PVstuff/PVdata/PVavgq.txt')
useP = numpy.loadtxt('/media/HD-EU2/PVstuff/PVdata/PVuseq.txt')
avGasP = numpy.loadtxt('/media/HD-EU2/PVstuff/PVdata/PVgasq.txt')
netP = numpy.loadtxt('/media/HD-EU2/PVstuff/PVdata/PVnetq.txt')
count = numpy.loadtxt('/media/HD-EU2/PVstuff/PVdata/PVcountq.txt', dtype = int)
except:
print("Normal loadPVfiles failed - using backup")
maxP = numpy.loadtxt('/media/HD-EU2/PVstuff/PVdata/PVmaxbakq.txt')
avgP = numpy.loadtxt('/media/HD-EU2/PVstuff/PVdata/PVavgbakq.txt')
useP = numpy.loadtxt('/media/HD-EU2/PVstuff/PVdata/PVusebakq.txt')
avGasP = numpy.loadtxt('/media/HD-EU2/PVstuff/PVdata/PVgasbakq.txt')
netP = numpy.loadtxt('/media/HD-EU2/PVstuff/PVdata/PVnetbakq.txt')
count = numpy.loadtxt('/media/HD-EU2/PVstuff/PVdata/PVcountbakq.txt', dtype = int)
try:
fg = open('/media/HD-EU2/PVstuff/PVdata/PVgen24.p', 'rb')
fm = open('/media/HD-EU2/PVstuff/PVdata/PVmtr24.p', 'rb')
fgas = open('/media/HD-EU2/PVstuff/PVdata/PVgas24.p', 'rb')
gen24 = pickle.load(fg)
mtr24 = pickle.load(fm)
gas24 = pickle.load(fgas)
except: #cant read files so try backup
try:
fg = open('/media/HD-EU2/PVstuff/PVdata/PVgen24bak.p', 'rb')
fm = open('/media/HD-EU2/PVstuff/PVdata/PVmtr24bak.p', 'rb')
fgas = open('/media/HD-EU2/PVstuff/PVdata/PVgas24bak.p', 'rb')
gen24 = pickle.load(fg)
mtr24 = pickle.load(fm)
gas24 = pickle.load(fgas)
print("Backup file tried")
except: #can't read backup either so make new file
gen24 = [0]*(24*60/gen24Interval) # make this the right length for the number of bins per 24h
mtr24 = [0]*(24*60/gen24Interval)
gas24 = [0]*(24*60/gen24Interval)
print("New 24hr records started")
print("Data loaded from file")
try:
fgas
except NameError:
pass
else:
fg.close
fm.close
fgas.close
print("Data file closed")
# save data files
def savePVdata():
'''Save max and average values to text files.'''
numpy.savetxt('/media/HD-EU2/PVstuff/PVdata/PVmaxq.txt',maxP)
numpy.savetxt('/media/HD-EU2/PVstuff/PVdata/PVavgq.txt',avgP)
numpy.savetxt('/media/HD-EU2/PVstuff/PVdata/PVuseq.txt',useP)
numpy.savetxt('/media/HD-EU2/PVstuff/PVdata/PVgasq.txt',avGasP)
numpy.savetxt('/media/HD-EU2/PVstuff/PVdata/PVnetq.txt',netP)
numpy.savetxt('/media/HD-EU2/PVstuff/PVdata/PVcountq.txt',count,fmt='%i')
numpy.savetxt('/media/HD-EU2/PVstuff/PVdata/PVmaxbakq.txt',maxP)
numpy.savetxt('/media/HD-EU2/PVstuff/PVdata/PVavgbakq.txt',avgP)
numpy.savetxt('/media/HD-EU2/PVstuff/PVdata/PVusebakq.txt',useP)
numpy.savetxt('/media/HD-EU2/PVstuff/PVdata/PVgasbakq.txt',avGasP)
numpy.savetxt('/media/HD-EU2/PVstuff/PVdata/PVnetbakq.txt',netP)
numpy.savetxt('/media/HD-EU2/PVstuff/PVdata/PVcountbakq.txt',count,fmt='%i')
fg = open('/media/HD-EU2/PVstuff/PVdata/PVgen24.p', 'wb')
fm = open('/media/HD-EU2/PVstuff/PVdata/PVmtr24.p', 'wb')
fgas = open('/media/HD-EU2/PVstuff/PVdata/PVgas24.p', 'wb')
pickle.dump(gen24,fg)
pickle.dump(mtr24,fm)
pickle.dump(gas24,fgas)
if DEBUG:
print("Data saved to file")
try:
fg.close
fm.close
fgas.close
print("Data file closed")
except IOError:
pass
# update data mth hr becomes mth hr*4+quarter
def update(mth, hr, minute ,genP, mtrP, gasP):
'''check for new maxima for current month and hour and update averages.'''
global lastbin, N
maxbin = hr*4+minute/15 # calculate bin number for max avg plot
if genP > maxP[mth][maxbin]:
maxP[mth][maxbin] = genP
if genP > maxP[mth][96]:
maxP[mth][96] = genP
if genP > maxP[0][maxbin]:
maxP[0][maxbin] = genP
if genP > maxP[0][96]:
maxP[0][96] = genP
mth = mth%12 # convert range to 0-11 0 = Dec, 1 = Jan ..
c = count[mth][maxbin]
cbig = 1.0*c/(c+1)
csml = 1.0/(c+1)
if mtrP < 0:
netP[mth][maxbin] = netP[mth][maxbin] * cbig + mtrP * csml
else:
netP[mth][maxbin] = netP[mth][maxbin] * cbig
avgP[mth][maxbin] = avgP[mth][maxbin] * cbig + genP * csml
useP[mth][maxbin] = useP[mth][maxbin] * cbig + (genP - mtrP) * csml
avGasP[mth][maxbin] = avGasP[mth][maxbin] * cbig + gasP * csml
count[mth][maxbin] += 1
if DEBUG:
print("max avg updated",mth,maxbin )
#update rolling 24 hr plot
bin = int(hr * 60 + minute)/gen24Interval # 5min samples make this match the number of bins at line 30
if DEBUG:
print("bin =",bin)
if bin != lastbin: #new bin means 5 mins are up so save data and start new bin
N = 0
lastbin = bin
savePVdata()
if DEBUG:
print("PVfiles updated")
N += 1
denominator = 1.0/(1.0*N)
gen24[bin] = 1.0*gen24[bin]*(N - 1)*denominator + genP*denominator
mtr24[bin] = 1.0*mtr24[bin]*(N - 1)*denominator + mtrP*denominator
gas24[bin] = 1.0*gas24[bin]*(N - 1)*denominator + gasP*denominator
#plot list as is and get a non scrolling 24h display
########################################
# append daily data
# check whether file exists and either open or append
# filename is today's date
def appendDailyData(date,f):
# change format to avoid trailing comma
global lastfile , f_handle
filename = '/media/HD-EU2/PVstuff/PVdata/PVgas-'+ str(date.year) + '_'+ str(date.month) + '_' + str(date.day) +'.txt'
if filename != lastfile:
#newfile
if len(lastfile) > 3:
f_handle.flush()
f_handle.close()
print("Yesterday's log closed")
f_handle = open(filename, 'a')
lastfile = filename
print("New daily log file opened")
# write timestamp then copy data
f_handle.write("%s" % str(date.replace(microsecond=0)))
#for item in f:
# f_handle.write(", %s" % item)
# depending on version of python sometimes get each individual character
#so use ardData instead
for i in range(0,8):
f_handle.write(", %0.2f" % float(f[i]))
f_handle.write('\n')
f_handle.flush()
# close latest file when tidying
def closeDailyData():
try:
f_handle.close()
print("Daily log closed")
except IOError:
pass
# open and read PV data files on remote computer
# plot averages at more frequent intervals no change made to hourly version
# v3includes gas data
# v4 has function to plot waveform
#
import pickle
import Image, ImageDraw, ImageFont
wid = 1024
hgt = 768
im = Image.new("RGB",(wid,hgt))
draw = ImageDraw.Draw(im)
wim = Image.new("RGB",(wid,hgt))
wdraw = ImageDraw.Draw(wim)
#constants
DEBUG = False
print ("myPlot initialised")
def plotWaveform(V,I,gen,exp,ph): #V and I are char arrays 0 to 256 and there are 128 values
wim.paste((255,255,255),(0,0,wid,hgt))
timeScale = wid/len(V) # haven't yet settled on array size
waveScale = 3 # = hgt/256 V and I are chars so 0-255
for i in range(len(V)-1):
wdraw.line((timeScale*i,waveScale*ord(V[i]),timeScale*(i+1),waveScale*ord(V[i+1])),fill = (255, 0, 0))
wdraw.line((timeScale*i,waveScale*ord(I[i]),timeScale*(i+1),waveScale*ord(I[i+1])),fill = (0, 255, 0))
wdraw.line((0,hgt/2,wid,hgt/2),fill = (0,0,0))
font = ImageFont.truetype('/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf', 20)
wdraw.text((30, 30), 'generate '+str(gen), font=font, fill="black")
wdraw.text((30, 60), 'import '+str(exp), font=font, fill="black")
wdraw.text((30, 90), 'power factor '+ str(ph), font=font, fill="black")
filename = "/media/HD-EU2/www/waveform" + "%.1f_" %gen +"%.1f.png" % exp
wim.save(filename)
print('waveform plotted '+filename)
def newGraph(maxP,avgP,useP, avGasP,netP,mtr24,gen24, gas24):
im.paste((255,255,255),(0,0,wid,hgt)) #clear graph
font = ImageFont.truetype('/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf', 30)
powScale = hgt/8000.0
# plot last 24 hrs history then update just latest value as it arrives
tScale = wid/(1.0*len(gen24)) #scale for len(gen24) points per day
gasSum = 0
for i in range(len(gen24)):
draw.line((i*tScale,hgt/2,i*tScale,hgt/2-gen24[i]*powScale),fill = (80,80,150))
# draw.line((i*tScale,hgt/2,i*tScale,hgt/2+gas24[i]/10.0*powScale),fill = (80,80,80))
# draw 10 point moving average gas use - sum 1st 50 then add next subtract -50th ...
gasSum += gas24[i]
if i< 10:
gashgt = gasSum/(i+1)/10.0
else:
gashgt = gasSum/100.0
gasSum -= gas24[i-10]
if gashgt > -mtr24[i]: #draw gas first if bigger than electric
draw.line((i*tScale,hgt/2,i*tScale,hgt/2+gashgt*powScale),fill = (40,40,40)) #(40,40,40)
if mtr24[i] < 0: #now draw electric
draw.line((i*tScale,hgt/2,i*tScale,hgt/2-mtr24[i]*powScale),fill = (180,0,0))#importing
else:
draw.line((i*tScale,hgt/2,i*tScale,hgt/2-mtr24[i]*powScale),fill = (0,180,0))#exporting
if gashgt <= -mtr24[i]: #draw gas last is smaller
draw.line((i*tScale,hgt/2,i*tScale,hgt/2+gashgt*powScale),fill = (40,40,40))
# plot hour markers every 3 hours and hourly time scale
tScale = wid/24.0 #scale for 24 points per day
for t in range(24):
draw.line((t*tScale,0,t*tScale,hgt),fill = (127,127,127))
if t%3 == 0:
stringTime = str(t)
w,h = draw.textsize(stringTime)
draw.text((t*tScale-w, hgt/2.0), stringTime, font=font, fill="yellow")
# plot horizontal power scale
for y in range(0,hgt,hgt/8):
draw.line((0,y,wid,y),fill = (127,127,127))
tScale = wid/(1.0*len(useP)) #scale for 24 points per day
#plot average values
for t in range(len(useP)):
draw.line((t*tScale,-maxP[t]*powScale+hgt/2,(t+1)*tScale,-maxP[t]*powScale+hgt/2),fill = (80,0,8)) #max orange
draw.line((t*tScale,-avgP[t]*powScale+hgt/2,(t+1)*tScale,-avgP[t]*powScale+hgt/2),fill = (100,255,100)) #avg green
draw.line((t*tScale,avGasP[t]*powScale/10.0+hgt/2,(t+1)*tScale,avGasP[t]*powScale/10.0+hgt/2),fill = (0,0,0)) #gas black
draw.line((t*tScale,useP[t]*powScale+hgt/2,(t+1)*tScale,useP[t]*powScale+hgt/2),fill = (0,127,127)) #used light blue
draw.line((t*tScale,-netP[t]*powScale+hgt/2,(t+1)*tScale,-netP[t]*powScale+hgt/2),fill = (255,80,80)) #net red
if t<len(useP)-1:
draw.line(((t+1)*tScale,-maxP[t]*powScale+hgt/2,(t+1)*tScale,-maxP[t+1]*powScale+hgt/2),fill = (80,0,80)) #max orange
draw.line(((t+1)*tScale,-avgP[t]*powScale+hgt/2,(t+1)*tScale,-avgP[t+1]*powScale+hgt/2),fill = (100,255,100)) #avg green
draw.line(((t+1)*tScale,avGasP[t]*powScale/10.0+hgt/2,(t+1)*tScale,avGasP[t+1]*powScale/10.0+hgt/2),fill = (0,0,0)) #gas grey
draw.line(((t+1)*tScale,useP[t]*powScale+hgt/2,(t+1)*tScale,useP[t+1]*powScale+hgt/2),fill = (0,127,127)) #used light blue
draw.line(((t+1)*tScale,-netP[t]*powScale+hgt/2,(t+1)*tScale,-netP[t+1]*powScale+hgt/2),fill = (255,80,80)) #net red
im.save("/media/HD-EU2/www/testGraph.png")
# del draw #not needed if image is made persistent
print("New graph plotted")
def updateGraph(hr,immOn,genP,mtrP, avGasP):
# draw = ImageDraw.Draw(im) #not needed if image is made persistent
powScale = hgt/8000.0
tScale = wid/24.0
#this is latest line only version (allows immOn colour change)
draw.line((hr*tScale,hgt/2,hr*tScale,hgt/2-genP*powScale),fill = (100,100,255)) #gen
if (avGasP/10 > -mtrP):
draw.line((hr*tScale,hgt/2,hr*tScale,hgt/2+avGasP*powScale/10.0),fill = (100,100,100)) #gas
if mtrP < 0:
draw.line((hr*tScale,hgt/2,hr*tScale,hgt/2-mtrP*powScale),fill = (255,50,50)) #import
elif immOn:
draw.line((hr*tScale,hgt/2,hr*tScale,hgt/2-mtrP*powScale),fill = (255,255,50)) #export immOn
else:
draw.line((hr*tScale,hgt/2,hr*tScale,hgt/2-mtrP*powScale),fill = (50,255,50)) #export immOff
if (avGasP/10 <= -mtrP):
draw.line((hr*tScale,hgt/2,hr*tScale,hgt/2+avGasP*powScale/10.0),fill = (100,100,100)) #gas
#plot timeline
draw.line((0,hgt/2,hr*tScale,hgt/2),fill = (255,255,255),width = 3)
im.save("/media/HD-EU2/www/testGraph.png")
# del draw #not needed if image is persistent
if DEBUG:
print("Graph updated")
#!/usr/bin/python
#test Arduino Comms
###################################
# powerControl v 2_01 goes with Senergy v 2_0
# polls Spark UDP server for energy data and:
# sends serial data to display
# save data to file(s)
# prepare a plot of energy use and save as image (for web)
# display expects a string starting Arduino
# followed by either M and a message string
# or D and powerGas, powerGen, powerExp, avP (24h average gas),
# earned (24h energy cost), immersionOn (flags immersion heater is really on)
#
# v1.2 includes plot_3 and file_3
# TODO
# look at alternative plot by value have a myPlot::init that scales
# gen -45p
# gas 3p
# exp 15p day import, 8p night import -1.5p export
# use net total value -gas + exp + gen
#
# database is in place report 24 hr average gas use OK
# also included daily average earned value
# scale values sent to myFile and myPlot electric * 1000 gas * 1000
# discard first return from Spark following restart of python
# also check that the time over which Spark has averaged data is
# more than 8 secs to avoid spurious high powers resulting from a single flash
# within a very short sample time
#
# trapped unicode decode errors
# split socket input into 3 char[] power, V wave, I wave
# power data consists of timeSinceLastRequest emonV emonI emonPower emonPF gasP genP expP
import socket
import serial
import signal
import sys
import time
from datetime import datetime
from subprocess import call
import numpy
import myFile_3 as myFile # Contains main data file management stuff
import myPlot_4 as myPlot # Contains graphics stuff
host = '192.168.1.101' # Can't use name of anonymous Spark server so use fixed IP
port = 5204 # Reserve a port for your service.
i=0
avP = 0.00
earned = 1000.00
TARIFF = 47.0
GAS = 3.5
NIGHT = 8.5
DAY = 12.5
GEN = 46
EXP = 1.5
STATE_AUTO = 1
STATE_ON = 2
STATE_OFF = 3
state = 1
immersionOn = 0
LOOPT = 15 #time between polls of Spark server
arraySize = 24*60*60/LOOPT #use %array size to prevent array error rounding secs
gas24h = [0]*(arraySize+1)
earned24h = [0]*(arraySize+1)
totalDailyCost = [0]*(arraySize+1)
savedData =0 #flags whether gas data has been saved this hour
readData = 0 #flags whether Spark has been polled
firstPoll = 1
##########################
# functions
def signal_handler(signal, frame):
print("Shutting down")
myFile.savePVdata()
myFile.closeDailyData()
numpy.savetxt('/media/HD-EU2/SparkEnergy/gas24h.txt',gas24h,fmt='%.1f')
numpy.savetxt('/media/HD-EU2/SparkEnergy/earned24h.txt',earned24h,fmt='%.6f')
numpy.savetxt('/media/HD-EU2/SparkEnergy/totalcost.txt',totalDailyCost,fmt='%.4f')
ser.close()
time.sleep(5)
sys.exit(0)
############################
# setup data from files
ser = serial.Serial('/dev/ttyUSB0',9600)
try:
gas24h = numpy.loadtxt('/media/HD-EU2/SparkEnergy/gas24h.txt')
except:
print('there is no gas24h file')
try:
earned24h = numpy.loadtxt('/media/HD-EU2/SparkEnergy/earned24h.txt')
except:
print('there is no earned value file')
try:
totalDailyCost = numpy.loadtxt('/media/HD-EU2/SparkEnergy/totalcost.txt')
except:
print('there is no totalcost file')
#setup signal handler to intecept system shutdown (to close files)
signal.signal(signal.SIGINT, signal_handler)
myFile.loadPVdata()
lastPlot = datetime.now()
mth = lastPlot.month % 12
# start a new graph
myPlot.newGraph(myFile.maxP[lastPlot.month],myFile.avgP[mth],myFile.useP[mth],myFile.avGasP[mth],myFile.netP[mth],myFile.mtr24,myFile.gen24,myFile.gas24)
# outer loop - make sure there is always a socket available
while True:
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
print(s)
s.settimeout(3)
# while we have a socket poll for data from Spark every 15 seconds
while s:
theTime = datetime.now()
#untidy way to start as close as possible to 15.0 secs after last poll
if (theTime.second % 15 < 0.5): #it will always take more than 0.5 secs to process
print (theTime)
try:
s.connect((host, port)) # connect to Spark server
s.sendall(b'Pi Ready\0 ') # client ready for data
except socket.error:
print('unable to connect')
break
r='not read anything'
try:
r = s.recv(1024)
except socket.timeout:
print ('socket.timeout')
break
if r == 0: # if r is 0 then the sender has closed for good
print('socket disconnected')
print(s)
break
# should now have received text from server in r
# split into 3 char[]
try:
power = r[0:128]
Vwav = r[128:256]
Iwav = r[256:]
text = power.decode("utf-8")
except UnicodeError:
print("Can't decode ",power)
break
except:
print('Problem understanding socket data')
break
# now parse this
ardData = []
ardData = text.split()
print(ord(Vwav[10]),ord(Vwav[11]))
if firstPoll:
ser.write('ArduinoMStarting RaspberryPi')
time.sleep(10)
firstPoll = 0
print('First Poll')
elif ( float(ardData[0])<8 ): # too short a time to average over
time.sleep(10)
print('Time too short')
elif (len(ardData) == 9): # valid data
powerFac = float(ardData[4])
powerGas = float(ardData[5])
powerGen = float(ardData[6])
powerExp = float(ardData[7])
print('got data ',float(ardData[4]),float(ardData[3]),powerGas,powerGen,powerExp)
# have now got formatted good data so send it to file
#should use 60/LOOPT rather than 4
timeBin = theTime.hour*60*4+theTime.minute*4+theTime.second/4
# to avoid blank bins fill the next two should be uneccesary now we control poll
# but they will be overwritten if data on time so no harm
try:
rate = TARIFF/(100*60*60/LOOPT)
gas24h[timeBin%arraySize] = powerGas
gas24h[(timeBin+1)%arraySize] = powerGas
gas24h[(timeBin+2)%arraySize] = powerGas
earned24h[timeBin] = powerGen*rate
earned24h[(timeBin+1)%arraySize] = powerGen*rate
earned24h[(timeBin+2)%arraySize] = powerGen*rate
except IndexError:
print ('Index Error', theTime, timeBin)
if theTime.hour < 7:
rate = NIGHT
elif powerExp <0:
rate = EXP
else:
rate = DAY
totalDailyCost[timeBin%arraySize] = (-powerGen*GEN+powerExp*rate+powerGas*GAS)/(100*60*60/LOOPT)
# save running 24h averages - should be in myPlot but is a late addition
if (theTime.minute == 0):
if (savedData == 0):
numpy.savetxt('/media/HD-EU2/SparkEnergy/gas24h.txt',gas24h,fmt='%.1f')
numpy.savetxt('/media/HD-EU2/SparkEnergy/earned24h.txt',earned24h,fmt='%.6f')
numpy.savetxt('/media/HD-EU2/SparkEnergy/totalcost.txt',totalDailyCost,fmt='%.4f')
savedData = 1
else:
savedData = 0
# send data to display
ser.flushInput()
avP = sum(gas24h)/len(gas24h)
#earned = sum(earned24h)
earned = sum(totalDailyCost)
outputData = ('ArduinoD '+ "%.2f " %powerGas + \
"%.3f " % powerGen + \
"%.3f " % powerExp + \
"%.3f " % avP + "%.2f " %earned +\
"% 1d" %immersionOn)
print(outputData)
ser.write(outputData)
# read Arduino display response which is the immersion demand Auto/Off/On
state = int(ser.read())
time.sleep(.1)
# have a return value from Arduino so set ImmersionOn appropriately
if (state == STATE_AUTO):
if ((powerGen > 1.3) and (powerExp < -1.3)):
immersionOn = 1
print('immersion on auto')
elif ((powerGen < 0.1) or (powerExp > 0)):
immersionOn = 0
print('immersion off auto')
elif (state == STATE_ON):
immersionOn = 1
print('immersion on manual')
elif (state == STATE_OFF):
immersionOn = 0
print('immersion off manual')
print(state)
# switch Telldus (a USB dongle that switches remote control sockets)
if (immersionOn):
call("/usr/local/bin/tdtool --on immersion",shell=True)
else:
call("/usr/local/bin/tdtool --off immersion",shell=True)
# save data negate powerExp so import is - and multiply by 1000 to get watts
# these changes allow me to use legacy myFile and myPlot libraries
myFile.update(int(theTime.month),int(theTime.hour),int(theTime.minute),powerGen*1000,-powerExp*1000, powerGas*1000)
myFile.appendDailyData(theTime, ardData)
elapsedT = theTime - lastPlot
if (elapsedT.total_seconds()>29): # = 1/2 minute
print('update graph')
myPlot.updateGraph(theTime.hour+theTime.minute/60.0,immersionOn==1,powerGen*1000,-powerExp*1000, powerGas*1000)
if theTime.minute%15<lastPlot.minute%15: # new quarter hour
print("Updating entire graph and saving data",theTime.month)
myFile.savePVdata()
mth = theTime.month%12 # make sure december is 0 for all mut max
myPlot.newGraph(myFile.maxP[theTime.month],myFile.avgP[mth],myFile.useP[mth],\
myFile.avGasP[mth],myFile.netP[mth],myFile.mtr24,\
myFile.gen24, myFile.gas24)
lastPlot = theTime
# process waveform data
myPlot.plotWaveform(Vwav,Iwav,powerGen,float(ardData[3]),powerFac)
print ('finished graph')
print('Out of inner loop') #finished read of good data
s.close() #close socket
if (ser):
ser.write('ArduinoM Waiting for Spark');
print ("Finished...")
/*
Spark port of emonlib - Library for the open energy monitor
Original Created by Trystan Lea, April 27 2010
GNU GPL
Modified to suit Spark Core 10 Feb 2014 Peter Cowley
***********************
Changes made:
1) ADC range changed from 1023 to 4095
2) long EnergyMonitor::readVcc() deleted and calls replaced by 3300
for my Spark actual V = 1.0004 x measured ADC reading
averaged over 8 resistor divider values - pretty accurate.
3) Removed references to Arduino.h and similar
4) Changed references to zero v near 500 to zero v near 2000 (mid ADC range)
5) Changed variable 'timeout' type to unsigned int to avoid warning
6) Spark samples much faster so the lag between V and I readings is small
so set the phase correction close to 1
7) Put in 250uS delay between pairs of ADC reads to allow Arduino style phase
correction. Each pair is now collected every 300uS
8) crossCount is measured using filtered signal and only +ve crossings
This gives consistent plots of the waveform.
9) Unused functions are now deleted rather than commented out
EnergyMonitor::voltageTX
EnergyMonitor::currentTX
readVcc
NOTE more recent versions of emonlib include some of these changes
to accommodate 12bit ADCs on newer Arduino models.
* ADDED - make noOfSamples and crossCount are made public for diagnostics
* add char arrays Vwaveform and I waveform to log waveform
* size of these is determined by available RAM
* scale to fit in 8 bit char array (V/16, I/8)
*/
#include "SemonLib20.h"
#include "application.h"
#include "math.h"
//--------------------------------------------------------------------------------------
// Sets the pins to be used for voltage and current sensors and the
// calibration factors which are set in setup() in the main program
// For 1v per 30a SCT-013-030 ICAL is 30
// For 9v ac power with 10:1 divider VCAL is 250
// For Spark the theoretical PHASECAL is 1.12
//--------------------------------------------------------------------------------------
void EnergyMonitor::voltage(int _inPinV, float _VCAL, float _PHASECAL)
{
inPinV = _inPinV;
VCAL = _VCAL;
PHASECAL = _PHASECAL;
}
void EnergyMonitor::current(int _inPinI, float _ICAL)
{
inPinI = _inPinI;
ICAL = _ICAL;
}
//--------------------------------------------------------------------------------------
// emon_calc procedure
// Calculates realPower,apparentPower,powerFactor,Vrms,Irms,kwh increment
// From a sample window of the mains AC voltage and current.
// The Sample window length is defined by the number of half wavelengths or crossings we choose to measure.
// Typically call this with 20 crossings and 2000mS timeout
// SPARK replace int SUPPLYVOLTAGE = readVcc(); with = 3300;
// SPARK count +ve crossings by filteredV keep 20 for 20 cycles
// SPARK timeout of 2000 has caused Spark problems with comms so reduce to 1600
// probably not a problem with recent software - not checked as timeout
// is not reached.
//--------------------------------------------------------------------------------------
void EnergyMonitor::calcVI(int crossings, unsigned int timeout)
{
int SUPPLYVOLTAGE = 3300; //Get supply voltage
crossCount = 0; //SPARK now a global variable
numberOfSamples = 0; //SPARK now a global variable
//-------------------------------------------------------------------------------------------------------------------------
// 1) Waits for the waveform to be close to 'zero'
// SPARK 'zero' on sin curve is 2048 on ADC
// SPARK there is sufficient delay time in the loop for ADC to settle
//-------------------------------------------------------------------------------------------------------------------------
boolean st=false; //an indicator to exit the while loop
unsigned long start = millis(); //millis()-start makes sure it doesnt get stuck in the loop if there is an error.
// wait for a reading close to zero volts before updating filtered values
while(st==false) //the while loop...
{
startV = analogRead(inPinV); //using the voltage waveform
if ((startV < 2078 ) && (startV > 2018)) st=true; //check its within range
if ((millis()-start)>timeout) st = true; //with 50uS delay ADC changes 15 units per sample at 0V
}
//SPARK now we're close to zero start updating filtered values and wait for
//a +ve zero crossing
while (st ==true){
lastSampleV=sampleV; //Used for digital high pass filter
lastSampleI=sampleI; //Used for digital high pass filter
lastFilteredV = filteredV; //Used for offset removal
lastFilteredI = filteredI; //Used for offset removal
sampleV = analogRead(inPinV); //Read in raw voltage signal
sampleI = analogRead(inPinI); //Read in raw current signal
delayMicroseconds(250); //SPARK this delay spaces samples to allow phase correction
filteredV = 0.996*(lastFilteredV+sampleV-lastSampleV);
filteredI = 0.996*(lastFilteredI+sampleI-lastSampleI);
if((filteredV>0)&&(lastFilteredV<0)) st = false;//SPARK always start on upward transition
}
//-------------------------------------------------------------------------------------------------------------------------
// 2) Main measurement loop
// SPARK V and I are measured very close together so little or no
// phase correction is needed for sample lag (9v transformer is another matter)
//-------------------------------------------------------------------------------------------------------------------------
start = millis();
while ((crossCount < crossings) && ((millis()-start)<timeout))
{
numberOfSamples++;
lastSampleV=sampleV; //Used for digital high pass filter
lastSampleI=sampleI; //Used for digital high pass filter
lastFilteredV = filteredV; //Used for offset removal
lastFilteredI = filteredI; //Used for offset removal
//-----------------------------------------------------------------------------
// A) Read in raw voltage and current samples
//
//-----------------------------------------------------------------------------
sampleV = analogRead(inPinV); //Read in raw voltage signal
sampleI = analogRead(inPinI); //Read in raw current signal
delayMicroseconds(250); //SPARK this delay spaces samples to allow phase correction
//-----------------------------------------------------------------------------
// B) Apply digital high pass filters to remove 1.65V DC offset (centered on 0V).
// SPARK grab the waveform data using [numberOfSamples%128] means that we
// end up with the last 128 values sampled in the arrays.
//-----------------------------------------------------------------------------
filteredV = 0.996*(lastFilteredV+sampleV-lastSampleV);
filteredI = 0.996*(lastFilteredI+sampleI-lastSampleI);
Vwaveform[numberOfSamples%128]=char((filteredV+2048)/16);//SPARK save waveform
Iwaveform[numberOfSamples%128]=char((filteredI+1024)/8); //SPARK save waveform
//-----------------------------------------------------------------------------
// C) Root-mean-square method voltage
//-----------------------------------------------------------------------------
sqV= filteredV * filteredV; //1) square voltage values
sumV += sqV; //2) sum
//-----------------------------------------------------------------------------
// D) Root-mean-square method current
//-----------------------------------------------------------------------------
sqI = filteredI * filteredI; //1) square current values
sumI += sqI; //2) sum
//-----------------------------------------------------------------------------
// E) Phase calibration
// SPARK theoretical shift is 1.12 but current clamp/transformer
// difference may swamp this
//-----------------------------------------------------------------------------
phaseShiftedV = lastFilteredV + PHASECAL * (filteredV - lastFilteredV);
//-----------------------------------------------------------------------------
// F) Instantaneous power calc
//-----------------------------------------------------------------------------
instP = phaseShiftedV * filteredI; //Instantaneous Power
sumP +=instP; //Sum
//-----------------------------------------------------------------------------
// G) Find the number of times the voltage has crossed the initial voltage
// - every 2 crosses we will have sampled 1 wavelength
// - so this method allows us to sample an integer number of half
// wavelengths which increases accuracy
// SPARK simplify and improve accuracy by using filtered values
//-----------------------------------------------------------------------------
if((filteredV>0)&&(lastFilteredV<0)) crossCount++;//SPARK always ends on upward transition
} //closing brace for counting crossings
//-------------------------------------------------------------------------------------------------------------------------
// 3) Post loop calculations
// SPARK replace 1024 for Arduino 10bit ADC with 4096 in voltage calculation
// VCAL shouldn't change much from Arduino value as SUPPLYVOLTAGE looks
// after the 5v to 3.3v change
//-------------------------------------------------------------------------------------------------------------------------
//Calculation of the root of the mean of the voltage and current squared (rms)
//Calibration coeficients applied.
float V_RATIO = VCAL *((SUPPLYVOLTAGE/1000.0) / 4096.0);
Vrms = V_RATIO * sqrt(sumV / numberOfSamples);
float I_RATIO = ICAL *((SUPPLYVOLTAGE/1000.0) / 4096.0);
Irms = I_RATIO * sqrt(sumI / numberOfSamples);
//Calculation power values
realPower = V_RATIO * I_RATIO * sumP / numberOfSamples;
apparentPower = Vrms * Irms;
powerFactor=realPower / apparentPower;
//Reset accumulators
sumV = 0;
sumI = 0;
sumP = 0;
}
//--------------------------------------------------------------------------------------
// SPARK replace int SUPPLYVOLTAGE = readVcc(); with = 3300;
// note that SUPPLYVOLTAGE is redefined here
//
//--------------------------------------------------------------------------------------
//
float EnergyMonitor::calcIrms(int NUMBER_OF_SAMPLES)
{
int SUPPLYVOLTAGE = 3300; //SPARK delete readVcc();
for (int n = 0; n < NUMBER_OF_SAMPLES; n++)
{
lastSampleI = sampleI;
sampleI = analogRead(inPinI);
delayMicroseconds(250); //SPARK this delay spaces samples to allow phase correction
lastFilteredI = filteredI;
filteredI = 0.996*(lastFilteredI+sampleI-lastSampleI);
// Root-mean-square method current
// 1) square current values
sqI = filteredI * filteredI;
// 2) sum
sumI += sqI;
}
float I_RATIO = ICAL *((SUPPLYVOLTAGE/1000.0) / 4096.0);
Irms = I_RATIO * sqrt(sumI / NUMBER_OF_SAMPLES);
//Reset accumulators
sumI = 0;
//--------------------------------------------------------------------------------------
return Irms;
}
void EnergyMonitor::serialprint()
{
Serial.print(realPower);
Serial.print(' ');
Serial.print(apparentPower);
Serial.print(' ');
Serial.print(Vrms);
Serial.print(' ');
Serial.print(Irms);
Serial.print(' ');
Serial.print(powerFactor);
Serial.println(' ');
delay(100);
}
/*
Semonlib.h - Library for openenergymonitor
Created by Trystan Lea, April 27 2010
GNU GPL
Modified for Spark Core Feb 10 2014
* 1) changed boolean variable type to bool
* 2) changed timeout to unsigned int to avoid warning
* 3) changed double to float to save RAM
* 4) added 3 new public variables: numberOfSamples, Vwaveform[] and Iwaveform[]
*
*/
#ifndef SemonLib_h
#define SemonLib_h
class EnergyMonitor
{
public:
void voltage(int _inPinV, float _VCAL, float _PHASECAL);
void current(int _inPinI, float _ICAL);
void voltageTX(float _VCAL, float _PHASECAL);
void currentTX(int _channel, float _ICAL);
void calcVI(int crossings, unsigned int timeout);
float calcIrms(int NUMBER_OF_SAMPLES);
void serialprint();
long readVcc();
//Useful value variables
float realPower,
apparentPower,
powerFactor,
Vrms,
Irms;
int numberOfSamples; // SPARK make public to check conversion rate
char Vwaveform[128]; // SPARK try to get size up to 128 by economising on RAM
char Iwaveform[128]; // SPARK new arrays to hold waveform use char to save RAM
private:
//Set Voltage and current input pins
int inPinV;
int inPinI;
//Calibration coefficients
//These need to be set in order to obtain accurate results
float VCAL;
float ICAL;
float PHASECAL;
//--------------------------------------------------------------------------------------
// Variable declaration for emon_calc procedure
//--------------------------------------------------------------------------------------
unsigned int lastSampleV,sampleV; //sample_ holds the raw analog read value, lastSample_ holds the last sample
unsigned int lastSampleI,sampleI; //SPARK make unsigned for bitwise operation
float lastFilteredV,filteredV; //Filtered_ is the raw analog value minus the DC offset
float lastFilteredI, filteredI;
float phaseShiftedV; //Holds the calibrated phase shifted voltage.
float sqV,sumV,sqI,sumI,instP,sumP; //sq = squared, sum = Sum, inst = instantaneous
unsigned int startV; //Instantaneous voltage at start of sample window.
bool lastVCross, checkVCross; //Used to measure number of times threshold is crossed.
int crossCount; // ''
};
#endif
/* Senergy20.cpp UDP energy data server
* Copyright (C) 2014 peter cowley
* *********************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* for a discussion of this software see:
* https://community.spark.io/t/open-energy-monitor-port/3166
* and
* http://openenergymonitor.org/emon/
*
* ********************************************************************
*
* uses SemonLib20 which is a slightly modified version of
* openenergymonitor.org's emonLib
*
* Spark Connections:
* ac voltage on pin A0 using 9v ac transformer divided 11:1 with
* 100k/10k resistors
* ac current on pin A1 from SCT-013-030 clip on sensor on mains supply tail
* these are wired as described at: http://openenergymonitor.org
*
* In addition to emonlib functions, the monitor has interrupts on:
* pin D0 - reed switch attached to gas meter pull down to ground
* pin D1 - photoresistor on generator meter LED on meter pulls pin to ground
* pin D2 - photoresistor on domestic meter registers total domestic load
* but does not indicate whether importing or exporting
*
* All digital pins are pulled high with 100k resistors and decoupled with
* 100nF ceramic capacitors
* ***************************************************************************
* The software is a UDP server
* it responds to requests on port 5204 this could be any unused port number
* the program loops continuously until there is a request when it makes
* the measurements and returns them - having the client set the measurement
* time avoids missing readings
* Output is a string containing:
* timeSinceLastRequest/1000 in secs
* emon1.Vrms volts
* emon1.Irms amps
* emon1.realPower/1000.0 in Kw
* emon1.powerFactor -1 all export to +1 all import
* powerGas kW
* powerGen kW - from flash counting
* powerExp kW - from flash counting
* crossCount - number of mains cycles sampled
*
* because gas interrupts are widely spaced they are accumulated over
* 20 (=NLOOPGAS) requests
*
*****************************************************************************
* History
* v0.1 10/2/14
* v0.3 12/2/14 added reeds witch and photoresistor interrupts
* v0.4 12/2/14 added original Arduino PVgas.ino analysis
* to calculate cumulative and instantaneous power
* on the fly
* v1.0 19/2/14 include flag to indicate unread data output
* deleted in v1.1 when made a UDP SERVER
* tends to oscillate between adjacent values .24 - .48 0r .96 - 1.08
* because of the low number of flashes per LOOPT at low powers
* maybe note last nFlash and use intermediate value if delta is only 1?
* v1.1 don't update every 15 secs but every time polled
* this ensures that data are up to date and
* synchronised with external clock
* Everything goes inside parse packet loop
* Add reed relay chatter check. If powerGas > 40kW set to zero (normal max 32kW))
* v1.2 11/3/14 send waveform data - runs with powerControl2_0.py
* v2_0 13/3/14 tidy up and test
*****************************************************************************/
#include "SemonLib20.h"
#include "application.h"
// set up an instance of EnergyMonitor Class from SemonLib
EnergyMonitor emon1;
// variables to convert flashes to kW
const long FLASHKWH = 3600; // 1 flash per sec is this many watts
const float TICKKWH = 400000.0; // 1 gas switch per sec is this many watts
const int NLOOPGAS = 20; // check gas every few loops 5 minutes for 15sec query
unsigned long currentTime; // loop timer to keep UDP alive
unsigned long previousPoll; // time of last request
unsigned long timeSinceLastRequest; //elapsed time since last request
int gasCount = 0; // count number of times round loop since last gas update
// variables for UDP communications
UDP udp;
char UDPinData[64];
char UDPoutData[384]; //128 bytes each of power, V wave and I wave
unsigned long portRead; //last socket read
unsigned int localPort = 5204; //reserved for incoming traffic
int packetSize;
// variables for interrupts
const long DEBOUNCE = 200;
int gasPin = D0;
int genPin = D1;
int expPin = D2;
int ledPin = D7;
volatile unsigned long lastGas; //time since last flash for debounce
volatile unsigned long lastGen;
volatile unsigned long lastExp;
volatile int nGas = 0; //number of flashes
volatile int nGen = 0;
volatile int nExp = 0;
volatile long cumGas = 0; //cumulative number of flashes
volatile long cumGen = 0;
volatile long cumExp = 0;
float powerGas; //power values
float powerGen;
float powerExp;
int gasVal = 0; //copy of number of flashes for small delta
int genVal = 0; //so that adjacent measurements can be averaged
int expVal = 0;
float avFlash; //temporary storage for average of two adjacent nGen etc.
///////////////////////////////////////////
// interrupt function prototypes
void gasInt(void);
void genInt(void);
void expInt(void);
///////////////////////////////////////////
void setup() {
udp.begin(localPort);
portRead = millis(); //when port was last read
previousPoll = portRead;
emon1.voltage(0, 250.0, 2.0); //initialise emon with pin, Vcal and phase
emon1.current(1, 30); //pin, Ical correct at 1kW
pinMode(gasPin, INPUT);
pinMode(genPin, INPUT);
pinMode(expPin, INPUT);
pinMode(ledPin, OUTPUT);
attachInterrupt(gasPin, gasInt, RISING);
attachInterrupt(genPin, genInt, RISING);
attachInterrupt(expPin, expInt, RISING);
lastGas = previousPoll;
lastGen = previousPoll;
lastExp = previousPoll;
digitalWrite(ledPin, LOW);
}
///////////////////////////////////////////
void loop() {
currentTime = millis();
// keep UDP socket open
if (currentTime - portRead > 50000) { //make sure that socket stays open
portRead = currentTime; //60 sec timeout no longer an issue
udp.stop(); //but keep in in case of comms reset
delay(100); //eventually system will do this too
udp.begin(localPort);
}
// check whether there has been a request to the server and process it
packetSize = udp.parsePacket();
if (packetSize) {
timeSinceLastRequest = currentTime - previousPoll;
previousPoll = currentTime;
// read the packet into packetBufffer
udp.read(UDPinData, 64);
// prepare power data packet
udp.beginPacket(udp.remoteIP(), udp.remotePort());
// update emon values
emon1.calcVI(20, 1600);
// now get values from meter flashes
// the interrupt routines set nGas, nExp and nGen
// first deal with the export meter flashes
avFlash = nExp;
if (abs(nExp - expVal) == 1) { //interpolate between small changes
avFlash = (nExp + expVal) / 2.0;
}
powerExp = (float) FLASHKWH * avFlash / (1.0 * timeSinceLastRequest);
if (nExp == 0) { //no flashes since last request so use emon value
powerExp = emon1.realPower / 1000.0;
}
else if (emon1.powerFactor < 0) {
powerExp *= -1.0; //use PF to add correct sign to meter value
}
// note - you can get accurate and remarkably reliable import/export estimates
// by correlating the last 5 readings. Not implemented here but Arduino code
// available if anyone wants it.
expVal = nExp; // remember number of flashes for next time
nExp = 0; //reset interrupt counter
// now deal with PV meter flashes
avFlash = nGen;
if (abs(nGen - genVal) == 1) {//interpolate between small changes
avFlash = (nGen + genVal) / 2.0;
}
powerGen = (float) FLASHKWH * avFlash / (1.0 * timeSinceLastRequest);
genVal = nGen;
nGen = 0;
// now deal with gas ticks of the reed switch
// only update gas every NLOOPGAS loops (20 = 5min as ticks are slow
gasCount++;
if (gasCount == NLOOPGAS) {
gasCount = 0;
gasVal = nGas;
powerGas = TICKKWH * nGas / (1.0 * NLOOPGAS * timeSinceLastRequest);
nGas = 0;
if (powerGas > 40) {//trap chatter if meter stops mid switch
powerGas = 0;
}
} //end of slow gas calculation
digitalWrite(ledPin, LOW); //set high by meter flash
// we have finished calculating powers so put into a string for the UDP packet
sprintf(UDPoutData, "%.1f %.1f %.1f %.2f %.2f %.2f %.3f %.3f %4d \n", \
timeSinceLastRequest/1000.0, emon1.Vrms, emon1.Irms, \
emon1.realPower/1000.0, emon1.powerFactor, powerGas, \
powerGen, powerExp,emon1.crossCount);
//and add the waveform arrays to the string
for (int i = 0; i<128; i++){
UDPoutData[128+i]=emon1.Vwaveform[(emon1.numberOfSamples+i+1)%128];
UDPoutData[256+i]=emon1.Iwaveform[(emon1.numberOfSamples+i+1)%128];
// offset by the number of samples so that we get the last 128
}
udp.write((unsigned char*)UDPoutData,384);
udp.endPacket();
//clear the buffer for next time
memset(&UDPoutData[0], 0, sizeof (UDPoutData));
}//finished writing packet
}//end of loop
///////////////////////////////////////////
void gasInt() {
unsigned long thisTime;
thisTime = millis();
if ((thisTime - lastGas) > DEBOUNCE) {
lastGas = thisTime;
nGas++;
cumGas++;
}
}
void genInt() {
unsigned long thisTime;
thisTime = millis();
if ((thisTime - lastGen) > DEBOUNCE) {
lastGen = thisTime;
nGen++;
cumGen++;
}
}
void expInt() {
unsigned long thisTime;
thisTime = millis();
if ((thisTime - lastExp) > DEBOUNCE) {
lastExp = thisTime;
nExp++;
cumExp++;
digitalWrite(ledPin, HIGH);
}
}
@phec
Copy link
Author

phec commented Mar 15, 2014

Updated to plot the voltage and current waveform.
The analog sampling has been slowed down to Arduino speeds by adding a 250uS delay

@phec
Copy link
Author

phec commented Mar 16, 2014

Corrected miss-copy 16/03/2014

@jaysettle
Copy link

Good job on porting the code! I was looking for something like this. However, right off the bat if I copy and paste into spark IDE, I get:

In file included from ../inc/spark_wiring.h:29:0,
from ../inc/application.h:29,
from SemonLib20.cpp:165:
../../core-common-lib/SPARK_Firmware_Driver/inc/config.h:12:2: warning: #warning "Defaulting to Release Build" [-Wcpp]
#warning "Defaulting to Release Build"
^
In file included from SemonLib20.cpp:163:0:
SemonLib20.h: In function 'void loop()':
SemonLib20.h:133:6: error: 'int EnergyMonitor::crossCount' is private
int crossCount; // ''
^
SemonLib20.cpp:473:36: error: within this context
powerGen, powerExp,emon1.crossCount);
^
make: *** [SemonLib20.o] Error 1

Error: Could not compile. Please review your code.

Did you compile this before posting the code? If so did you get these errors. I've made no changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment