-
-
Save phec/9254793 to your computer and use it in GitHub Desktop.
//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); | |
} | |
} |
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.
Corrected miss-copy 16/03/2014