-
-
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