# This Python script needs a SPI module. # For more info, please see: # http://www.100randomtasks.com/simple-spi-on-raspberry-pi # # This script also uses a "LoadAverage" module. This module can be downloaded from: # http://patrick.i234.me/kwhmeter/LoadAverage.py # # This script is written for use on a Raspberri Pi, in combination with some # custom-made hardware. # The schematic of the hardware can be found here: # https://www.circuitlab.com/circuit/eae95u/raspberry-pi-kwh-meter/ # # or an PDF can be found on my own site: # http://patrick.i234.me/kwhmeter/raspberry-pi-kwh-meter.pdf # # # # Unused modules, that I might incorporate later #import rrdtool #import os #import MySQLdb import spidev import time import RPi.GPIO as GPIO import ConfigParser # for the module LoadAverage, see: http://patrick.i234.me/kwhmeter/LoadAverage.py import LoadAverage DEBUG = False # GPIO Pins for the different components: switch = 23 redLED = 18 greenLED = 17 boardLED = 22 # Initialise SPI spi = spidev.SpiDev() spi.open(0, 0) # read SPI data from MCP3008 chip, 8 possible adc's (0 thru 7) def readadc(adcnum): if ((adcnum > 7) or (adcnum < 0)): return -1 r = spi.xfer2([1, (8 + adcnum) << 4, 0]) adcout = ((r[1] & 3) << 8) + r[2] return adcout def turnonLED(LED): GPIO.output(LED, True) def turnoffLED(LED): GPIO.output(LED, False) def toggleLED(LED): GPIO.output(LED, GPIO.input(LED) ^ 1) def writeLog(watt, total, importCount, exportCount, rotTime, loadaverage): localtime = time.localtime(time.time()) filename = '%s_kWh-meter.log' % time.strftime("%Y-%m-%d", localtime) line = "%s\t%s\t%s\t%s\t%.3f\t%.3f\t%.3f\t%.0f\t%.3f\t%.3f\t%.3f\n" % ( time.strftime("%Y-%m-%d %H:%M:00", localtime), time.strftime("%Y-%m-%d", localtime), time.strftime("%H:%M:00", localtime), watt, total, importCount, exportCount, rotTime, loadaverage[0], loadaverage[1], loadaverage[2]) logfile = open(filename, 'a') try: logfile.write(line) finally: logfile.close() logfile.close() def writesettings(config): with open('kwhmeter.conf', 'wb') as configfile: config.write(configfile) return True def readsettings(): config = ConfigParser.SafeConfigParser() config.read('kwhmeter.conf') if not config.has_section('kWh Settings'): config.add_section('kWh Settings') if not config.has_option('kWh Settings', 'LeftChannelID'): config.set('kWh Settings', 'LeftChannelID', '1') if not config.has_option('kWh Settings', 'RightChannelID'): config.set('kWh Settings', 'RightChannelID', '0') # Section kWh data if not config.has_section('kWh'): config.add_section('kWh') if not config.has_option('kWh', 'importCounter'): config.set('kWh', 'importCounter', '0') if not config.has_option('kWh', 'exportCounter'): config.set('kWh', 'exportCounter', '0') if not config.has_option('kWh', 'rotationsPerKWh'): config.set('kWh', 'rotationsPerKWh', '600') # If you want to save in a MySQL DB, uncomment below: ## Section MySQL #if not config.has_section('MySQL'): # config.add_section('MySQL') #if not config.has_option('MySQL', 'host'): # config.set('MySQL', 'host', 'localhost') #if not config.has_option('MySQL', 'user'): # config.set('MySQL', 'user', 'smatool') #if not config.has_option('MySQL', 'passwd'): # config.set('MySQL', 'passwd', 'smatool-password-here') #if not config.has_option('MySQL', 'db'): # config.set('MySQL', 'db', 'smatool18') return config def savekwhdata(config, importCounter, exportCounter): config.set('kWh', 'importCounter', "%.0f" % importCounter) config.set('kWh', 'exportCounter', "%.0f" % exportCounter) writesettings(config) # If you want to save in a MySQL DB, uncomment below: #def savetodb(config, watt, total, imported, exported): # section = 'MySQL' # localtime = time.localtime(time.time()) # # conn = MySQLdb.connect(host = config.get(section, 'host'), # user = config.get(section, 'user'), # passwd = config.get(section, 'passwd'), # db = config.get(section, 'db')) # cursor = conn.cursor() # # try: # cursor.execute( # "INSERT INTO `DayDatakWh` (`DateTime`, `CurrentPower`, `ETotalToday`, `EImportToday`, `EExportToday`, `CurrentUsage`, `EUsageToday`, `PVOutput`, `CHANGETIME`) VALUES ('%s', %s, %.3f, %.3f, %.3f, NULL, NULL, NULL, '0000-00-00 00:00:00');" % ( # time.strftime("%Y-%m-%d %H:%M:00", localtime), watt, total, imported, exported)) # conn.commit() # except: # conn.rollback() # conn.close() # # return True def printinfo(line): timeStr = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) print "%s\t%s" % (timeStr, line) #serialDev.writeLine("%s\t%s" % (timeStr, line)) def calibratesensors(config): printinfo("Do calibration of sensors . . .") sensorleftLValue = 9999 sensorleftHValue = -1 sensorrightLValue = 9999 sensorrightHValue = -1 turnoffLED(boardLED) turnoffLED(greenLED) turnonLED(redLED) startTime = millis() elapsedTime = millis() - startTime startBlink = millis() startInfo = millis() sensorleftDiff = 0 sensorrightDiff = 0 while (elapsedTime < 10000) or sensorleftDiff < 40 or sensorrightDiff < 40 or not GPIO.input(switch): # run calibration for min. 10 seconds, # and distance between low and high value must be at least 40 ticks # or while the switch on the board is hold down (pressing the switch = 0, not pressing the switch = 1) sensordata = samplesensors(config) sensorleftCurrent = sensordata['left'] sensorrightCurrent = sensordata['right'] if sensorleftLValue > sensorleftCurrent: sensorleftLValue = sensorleftCurrent if sensorleftHValue < sensorleftCurrent: sensorleftHValue = sensorleftCurrent if sensorrightLValue > sensorrightCurrent: sensorrightLValue = sensorrightCurrent if sensorrightHValue < sensorrightCurrent: sensorrightHValue = sensorrightCurrent if DEBUG: printinfo("Sensor left: %s\tSensor right: %s" % (sensorleftCurrent, sensorrightCurrent)) if millis() - startBlink > 100: toggleLED(boardLED) toggleLED(greenLED) toggleLED(redLED) startBlink = millis() sensorleftDiff = sensorleftHValue - sensorleftLValue sensorrightDiff = sensorrightHValue - sensorrightLValue if millis() - startInfo > 1000: printinfo("Sensor left: %s (diff: %s) \t\tSensor right: %s (diff: %s)" % (sensorleftCurrent, sensorleftDiff, sensorrightCurrent, sensorrightDiff)) startInfo = millis() time.sleep(0.1) elapsedTime = millis() - startTime sensorleftLTh = sensorleftLValue + ((sensorleftDiff / 10) * 5) # 50% above min sensorleftHTh = sensorleftHValue - ((sensorleftDiff / 10) * 2) # 20% below max sensorrightLTh = sensorrightLValue + ((sensorrightDiff / 10) * 5) # 50% above min sensorrightHTh = sensorrightHValue - ((sensorrightDiff / 10) * 2) # 20% below max turnoffLED(boardLED) turnoffLED(greenLED) turnoffLED(redLED) printinfo("Calibration done!") printinfo("Left low: %s" % sensorleftLValue) printinfo(" high: %s" % sensorleftHValue) printinfo(" diff: %s" % sensorleftDiff) printinfo(" lthld.%s" % sensorleftLTh) printinfo(" hthld.%s" % sensorleftHTh) printinfo("Right low: %s" % sensorrightLValue) printinfo(" high: %s" % sensorrightHValue) printinfo(" diff: %s" % sensorrightDiff) printinfo(" lthld.%s" % sensorrightLTh) printinfo(" hthld.%s" % sensorrightHTh) printinfo("") config.set('kWh', 'minleft', "%.0f" % sensorleftLValue) config.set('kWh', 'maxleft', "%.0f" % sensorleftHValue) config.set('kWh', 'minmarginleft', "%.0f" % sensorleftLTh) config.set('kWh', 'maxmarginleft', "%.0f" % sensorleftHTh) config.set('kWh', 'minright', "%.0f" % sensorrightLValue) config.set('kWh', 'maxright', "%.0f" % sensorrightHValue) config.set('kWh', 'minmarginright', "%.0f" % sensorrightLTh) config.set('kWh', 'maxmarginright', "%.0f" % sensorrightHTh) return config def millis(): return int(round(time.time() * 1000)) def samplesensors(config): # Sample the left and right sensor for maxsamples times (in order to get an average value) leftid = config.getint('kWh Settings', 'LeftChannelID') rightid = config.getint('kWh Settings', 'RightChannelID') maxsamples = 20 sensorleft = 0 sensorright = 0 for loopCount in range(0, maxsamples): sensorleft += readadc(leftid) sensorright += readadc(rightid) #print "%s | %s" % (sensorleft, sensorright) sensorleft /= maxsamples sensorright /= maxsamples return {'left': sensorleft, 'right': sensorright} def dotFollower(config): # This is the main part of the code, which will start to minitor the black dot on the disc # Do some initialisations rotationsperkwh = config.getfloat("kWh", "rotationsPerKWh") * 1.00 counterimport = config.getint("kWh", "importCounter") counterexport = config.getint("kWh", "exportCounter") countertotal = counterimport + counterexport currentpower = 0 timeperrotation = 0 left = False right = False leftdetected = False rightdetected = False lefttimestamp = time.time() leftprevtimestamp = time.time() righttimestamp = time.time() rightprevtimestamp = time.time() sensorinnertimestamp = time.time() sensorinnerlength = 0 sensoroutertimestamp = time.time() sensoroutherlength = 0 leftmin = config.getint("kWh", "minmarginleft") leftmax = config.getint("kWh", "maxmarginleft") rightmin = config.getint("kWh", "minmarginright") rightmax = config.getint("kWh", "maxmarginright") logexported = False loadaverage = LoadAverage.Loadaverage() loadaverageadded = False # Keep on running while the switch is NOT pressed while GPIO.input(switch): sensordata = samplesensors(config) # left sensor if sensordata['left'] >= leftmax and not left: # turn left to False left = True turnonLED(greenLED) leftdetected = True leftprevtimestamp = lefttimestamp lefttimestamp = time.time() printinfo("Left rotation: . %.5f secs" % (lefttimestamp - leftprevtimestamp)) if not rightdetected: sensorinnertimestamp = time.time() sensoroutherlength = time.time() - sensoroutertimestamp else: sensoroutertimestamp = time.time() sensorinnerlength = time.time() - sensorinnertimestamp if sensordata['left'] <= leftmin and left: # turn left to False left = False turnoffLED(greenLED) # right sensor if sensordata['right'] >= rightmax and not right: # turn right to False right = True turnonLED(redLED) rightdetected = True rightprevtimestamp = righttimestamp righttimestamp = time.time() printinfo("Right rotation:. %.5f secs" % (righttimestamp - rightprevtimestamp)) if not leftdetected: sensorinnertimestamp = time.time() sensoroutherlength = time.time() - sensoroutertimestamp else: sensoroutertimestamp = time.time() sensorinnerlength = time.time() - sensorinnertimestamp if sensordata['right'] <= rightmin and right: # turn right to False right = False turnoffLED(redLED) if left and right: # Oops...! both sensors are active? Impossible! # Do a new calibration! printinfo("Error: both sensors are True! --> reset (new calibration)") config = calibratesensors(config) left = False right = False leftdetected = False rightdetected = False lefttimestamp = time.time() leftprevtimestamp = time.time() righttimestamp = time.time() rightprevtimestamp = time.time() if not left and not right and leftdetected and rightdetected: #rotation detected! leftdetected = False rightdetected = False if sensorinnerlength < sensoroutherlength: # detection of left and right was correct turnonLED(boardLED) # Determine direction if lefttimestamp < righttimestamp: # The left sensor was seen before the right # S1 --> S2 # Rotation is "importing from the net" rotation = 1 counterimport += 1 else: # The right sensor was seen before the left # S1 <-- S2 # Rotation is "exporting to the net" rotation = -1 counterexport += 1 countertotal = counterimport - counterexport # I'll average the left and right rotation time to get a "smoother" value (in principle I calculate the point between the 2 sensors) timeperrotation = (((lefttimestamp - leftprevtimestamp) + (righttimestamp - rightprevtimestamp)) / 2) currentpower = int(rotation * rotationsperkwh * 10 / (timeperrotation)) if rotation == 1: printinfo("Rotation completed -> importing from net") else: printinfo("Rotation completed -> exporting to net") printinfo("Rotation time: . %.5f secs" % timeperrotation) printinfo("-- -- -- -- -- -- -- -- -- -- --") printinfo("Inner gab: . . . %.5f secs" % sensorinnerlength) printinfo("Outer: . . . . . %.5f secs" % sensoroutherlength) printinfo("Total: . . . . . %.5f secs" % (sensoroutherlength + sensorinnerlength)) printinfo("Rotation time: . %.5f secs" % timeperrotation) printinfo("Current power: . %.0f Watt" % currentpower) printinfo("ImportCount: . . %.0f" % counterimport) printinfo("ExportCount: . . %.0f" % counterexport) printinfo("Import:. . . . . %.2f kWh" % (counterimport / rotationsperkwh)) printinfo("Export:. . . . . %.2f kWh" % (counterexport / rotationsperkwh)) printinfo("Total: . . . . . %.2f kWh" % (countertotal / rotationsperkwh)) printinfo("Load average:. . %.0f W, %.0f W, %.0f W" % loadaverage.loadaverage()) printinfo("--------------------------------------------------------------------------------") # do a tiny nap time.sleep(0.1) turnoffLED(boardLED) else: # Ooops... inner gab is bigger then the outer route??? We are detecting the wrong way! # sleep a bit and try again printinfo("Skipping rotation . . .") if (sensoroutherlength / 4) < 10: time.sleep(sensoroutherlength / 4) else: time.sleep(10) else: time.sleep(0.001) if time.gmtime()[5] % 10 == 0: # Every 10 seconds print the current values if not loadaverageadded: loadaverage.addvalue(currentpower) loadaverageadded = True if time.gmtime()[4] % 5 == 0: # Every 5 minutes, write a log entry & DB record if not logexported: writeLog(currentpower, (countertotal / rotationsperkwh), (counterimport / rotationsperkwh), (counterexport / rotationsperkwh), (timeperrotation * 1000), loadaverage.loadaverage()) savekwhdata(config, counterimport, counterexport) # If you want to save to a DB, uncomment the line below: #savetodb(config, currentpower, (countertotal / rotationsperkwh), (counterimport / rotationsperkwh), (counterexport / rotationsperkwh)) logexported = True else: logexported = False else: loadaverageadded = False writesettings(config) # Main program routine begins here: #---------------------------------- config = readsettings() writesettings(config) #init raspberry pi gpio pins GPIO.setwarnings(False) GPIO.setmode(GPIO.BCM) GPIO.setup(switch, GPIO.IN) GPIO.setup(boardLED, GPIO.OUT, initial = GPIO.LOW) GPIO.setup(greenLED, GPIO.OUT, initial = GPIO.LOW) GPIO.setup(redLED, GPIO.OUT, initial = GPIO.LOW) #set LEDs default off turnoffLED(boardLED) turnoffLED(greenLED) turnoffLED(redLED) writesettings(config) # Infinite loop while True: config = calibratesensors(config) dotFollower(config) # we will never get here... GPIO.remove_event_detect(switch) GPIO.cleanup()