Last active
January 31, 2017 23:21
-
-
Save phec/9254793 to your computer and use it in GitHub Desktop.
Port of Open Energy Monitor emonlib to Spark SemonLib.cpp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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 | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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") | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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...") | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.