555 lines
19 KiB
Python
555 lines
19 KiB
Python
# 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
|
|
import datetime
|
|
from datetime import date
|
|
# For PVoutput
|
|
import urllib, urllib2
|
|
|
|
|
|
DEBUG = False
|
|
|
|
# GPIO Pins for the different components:
|
|
switch = 23
|
|
redLED = 17
|
|
greenLED = 18
|
|
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', '2')
|
|
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 not config.has_option('kWh', 'cumuliday'):
|
|
config.set('kWh', 'cumuliday', date.today().strftime("%Y%m%d"))
|
|
|
|
# Add Pvoutput stuff
|
|
if not config.has_section('pvout'):
|
|
config.add_section('pvout')
|
|
if not config.has_option('pvout','pvout_enabled'):
|
|
config.set('pvout','pvout_enabled','false')
|
|
if not config.has_option('pvout','pvout_apikey'):
|
|
config.set('pvout','pvout_apikey','0')
|
|
if not config.has_option('pvout','pvout_sysid'):
|
|
config.set('pvout','pvout_sysid','0')
|
|
|
|
# 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', 'root')
|
|
#if not config.has_option('MySQL', 'passwd'):
|
|
# config.set('MySQL', 'passwd', 'root')
|
|
#if not config.has_option('MySQL', 'db'):
|
|
# config.set('MySQL', 'db', 'kwhRijnsraat214')
|
|
|
|
|
|
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 exportPvoutput(config, gentotal, genpower, contotal, conpower):
|
|
|
|
pvout_enabled = config.getboolean('pvout','pvout_enabled')
|
|
pvout_apikey = config.get('pvout','pvout_apikey')
|
|
pvout_sysid = config.get('pvout','pvout_sysid')
|
|
|
|
url = "http://pvoutput.org/service/r2/addstatus.jsp"
|
|
now = datetime.datetime.now()
|
|
|
|
get_data = {
|
|
'key': pvout_apikey,
|
|
'sid': pvout_sysid,
|
|
'd': now.strftime('%Y%m%d'),
|
|
't': now.strftime('%H:%M'),
|
|
'v1': gentotal * 1000,
|
|
'v2': genpower,
|
|
'v3': contotal * 1000,
|
|
'v4': conpower,
|
|
'c1': "1"
|
|
}
|
|
|
|
|
|
get_data_encoded = urllib.urlencode(get_data) # UrlEncode the parameters
|
|
printinfo(get_data_encoded)
|
|
request_object = urllib2.Request(url + '?' + get_data_encoded) # Create request object
|
|
try:
|
|
response = urllib2.urlopen(request_object) #make the request and store the response
|
|
printinfo("Pvoutput Exported")
|
|
return True
|
|
except urllib.error.HTTPError, e:
|
|
printinfo('HTTPError = ' + str(e.code))
|
|
return False
|
|
except Exception:
|
|
import traceback
|
|
checksLogger.error('generic exception: ' + traceback.format_exc())
|
|
|
|
#http://pvoutput.org/service/r2/addstatus.jsp?
|
|
|
|
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 < 30000) 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("Time %s: \tSL: %s (diff: %s) \t\tSR: %s (diff: %s)" % (elapsedTime, 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
|
|
exportpower = 0
|
|
importpower = 0
|
|
|
|
thisday = config.get("kWh", "cumuliday")
|
|
printinfo("Date: %s" % thisday)
|
|
|
|
|
|
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 1: #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 sensor: . %.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)
|
|
#printinfo("Left off")
|
|
|
|
|
|
|
|
# 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 sensor:. %.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)
|
|
#printinfo("Right off")
|
|
|
|
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("-> importing from net")
|
|
importpower = currentpower
|
|
exportpower = 0
|
|
else:
|
|
printinfo("<- exporting to net")
|
|
exportpower = currentpower * -1
|
|
importpower = 0
|
|
#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 pvoutput, uncomment the line below:
|
|
exportPvoutput(config, (counterexport / rotationsperkwh), exportpower, (counterimport / rotationsperkwh), importpower)
|
|
logexported = True
|
|
else:
|
|
logexported = False
|
|
else:
|
|
loadaverageadded = False
|
|
|
|
if thisday != date.today().strftime("%Y%m%d"):
|
|
if logexported == True:
|
|
exportpower = 0
|
|
importpower = 0
|
|
thisday = date.today().strftime("%Y%m%d")
|
|
config.set('kWh', 'cumuliday',thisday)
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|