pico-s0-powermeter/main.py

360 lines
15 KiB
Python

import rp2, re, os
import TimeUtils, Logger, Networking, NTP
import uasyncio as asyncio
import utime as time
import usocket as socket
import urequests as requests
import json as xjson
from machine import ADC, Pin
from secrets import secrets
from configs import configs
rp2.country(configs['country'])
version = "0.6"
TimeUtils = TimeUtils.TimeUtils()
Logger = Logger.Logger(configs['log_housekeeping_days'])
Networking = Networking.Networking(Logger, secrets['ssid'], secrets['pw'])
NTP = NTP.NTP(Logger)
boottime = time.time()
tempSensor = ADC(4) # internal temperature sensor
s0Pin = Pin(5, Pin.IN, Pin.PULL_UP) # gpio 5
temperature = 0.0
zaehlerStand = 0 # Wh
momentanVerbrauch = 0.000 # W
interrupt = False
debounce_time = 0
secPerImp = 3600.0 / float(configs['ticks_per_kWh'])
lastSaved = 0
lastPulled = 0
lastTick = 0
impulses = 0
lastImpTime = 0
todayConsumption = 0.000 # Wh
yesterdayConsumption = 0.000 # Wh
weekConsumption = 0.000 # Wh
lastWeekConsumption = 0.000 # Wh
monthConsumption = 0.000 # Wh
lastMonthConsumption = 0.000 # Wh
yearConsumption = 0.000 # Wh
# Handle s0 interrupts
def callback(s0Pin):
global interrupt, debounce_time
if (time.ticks_ms() - debounce_time) > 20:
interrupt = True
debounce_time = time.ticks_ms()
s0Pin.irq(trigger = Pin.IRQ_RISING, handler = callback)
# Checks if string is integer
def IsInt(possibleint):
try:
int(possibleint)
except:
return False
else:
return True
# Reboots the Pico W (f.e. in case of an error)
def Reboot():
Logger.LogMessage("Performing Reboot")
machine.reset()
# Calculates the uptime in days, hours and minutes
def Uptime():
global boottime
timeDiff = time.time() - boottime
(minutes, seconds) = divmod(timeDiff, 60)
(hours, minutes) = divmod(minutes, 60)
(days,hours) = divmod(hours, 24)
return str(days)+":"+f"{hours:02d}"+":"+f"{minutes:02d}"
#seconds = (time.time() - boottime) % (24 * 3600)
#hours = seconds // 3600
#seconds %= 3600
#minutes = seconds // 60
#seconds %= 60
#return "%d:%02d" % (hours, minutes)
# Reads the onboard temperature sensor's value
def ReadTemp():
global temperature
temperature = 27 - ((tempSensor.read_u16() * (3.3 / (65535))) - 0.706) / 0.001721
# Checks the count of the Powermeter (reboot safe)
def CheckPowermeterCount():
global zaehlerStand
try:
file = open("powercount.txt", "r")
except:
file = open("powercount.txt", "w")
file.write("0.000")
countFile = float(file.read())
file.close()
if zaehlerStand < countFile:
zaehlerStand = countFile
Logger.LogMessage("Powermeter count is " + str(zaehlerStand / 1000) + " kWh.")
# store zaehlerStand and momentanVerbrauch via API in DB
def SendAPI():
global lastSaved, zaehlerStand, momentanVerbrauch
if ((lastSaved == 0) or ((time.ticks_ms() - lastSaved) > configs['db_api_interval_minutes'] * 60000)):
lastSaved = time.ticks_ms()
response = requests.get(configs['db_api'] + "?action=send&total=" + str(zaehlerStand) + "&current=" + str(momentanVerbrauch))
response.close()
Logger.LogMessage("Powermeter total: " + GetZaehlerstand())
# pull consumption stats from DB via API
def GetStatsAPI():
global lastPulled, todayConsumption, yesterdayConsumption, weekConsumption, lastWeekConsumption, monthConsumption, lastMonthConsumption, yearConsumption
if ((lastPulled == 0) or ((time.ticks_ms() - lastPulled) > configs['db_api_pull_interval_minutes'] * 60000)):
lastPulled = time.ticks_ms()
response = requests.get(configs['db_api'] + "?action=stats&total&current")
#map json response values to global variables
j = xjson.loads(xjson.dumps(response.json()))
todayConsumption = float(j["todayConsumption"])
yesterdayConsumption = float(j["yesterdayConsumption"])
weekConsumption = float(j["weekConsumption"])
lastWeekConsumption = float(j["lastWeekConsumption"])
monthConsumption = float(j["monthConsumption"])
lastMonthConsumption = float(j["lastMonthConsumption"])
yearConsumption = float(j["yearConsumption"])
response.close()
Logger.LogMessage("Pulled power consumption stats from DB via API: " + str(j))
# calculate current power consumption and increase the total power consuption
def PowerMeasurement():
global interrupt, secPerImp, lastTick, impulses, lastImpTime, momentanVerbrauch
if interrupt:
#current power consumption (watt)
nowTick = time.ticks_ms()
timeDiff = nowTick - lastTick
#1000 (watt) / (timeDiff in s / secPerImp)
momentanVerbrauch = 1000.0 / ((timeDiff / 1000.0) / secPerImp)
lastTick = nowTick
impulses = impulses + 1
#powermeter total count (wh)
if (time.ticks_ms() - lastImpTime) > 60000: #every minute
nowImpTime = time.ticks_ms()
timeImpDiff = nowImpTime - lastImpTime
#energy = power times time (in hour)
newConsumption = (((1000.0 * impulses) / ((timeImpDiff / 1000.0) / secPerImp)) * (timeImpDiff / 3600000.0))
AddZaehlerstand(newConsumption)
impulses = 0
lastImpTime = nowImpTime
interrupt = False
# get power meter total in kWh
def GetZaehlerstand():
global zaehlerStand
return "{0:.3f}".format(zaehlerStand / 1000)
# set power meter total in Wh
def SetZaehlerstand(setStand):
global zaehlerStand
zaehlerStand = float(setStand)
file = open("powercount.txt", "w")
file.write(str(zaehlerStand))
file.close()
# Adds power consumption to power meter total in Wh
def AddZaehlerstand(power):
global zaehlerStand
SetZaehlerstand(zaehlerStand + float(power))
# Returns current power consumption in W
def GetCurrentPower():
global momentanVerbrauch
return "{0:.2f}".format(momentanVerbrauch)
# Returns todays total power consumption in kWh
def GetTodayConsumption():
global todayConsumption
return "{0:.2f}".format(todayConsumption / 1000)
# Returns yesterdays total power consumption in kWh
def GetYesterdayConsumption():
global yesterdayConsumption
return "{0:.2f}".format(yesterdayConsumption / 1000)
# Returns this weeks total power consumption in kWh
def GetWeekConsumption():
global weekConsumption
return "{0:.2f}".format(weekConsumption / 1000)
# Returns last weeks total power consumption in kWh
def GetConsumptionLastWeek():
global lastWeekConsumption
return "{0:.2f}".format(lastWeekConsumption / 1000)
# Returns this months total power consumption in kWh
def GetConsumptionMonth():
global monthConsumption
return "{0:.2f}".format(monthConsumption / 1000)
# Returns last months total power consumption in kWh
def GetConsumptionLastMonth():
global lastMonthConsumption
return "{0:.2f}".format(lastMonthConsumption / 1000)
# returns this years total power consumption in kWh
def GetConsumptionYear():
global yearConsumption
return "{0:.2f}".format(yearConsumption / 1000)
#####################################################################
# Helper-method to allow error handling and output in asyncio
def set_global_exception():
def handle_exception(loop, context):
Logger.LogMessage("Fatal error: " + str(context["exception"]))
import sys
sys.print_exception(context["exception"])
sys.exit()
Reboot()
loop = asyncio.get_event_loop()
loop.set_exception_handler(handle_exception)
# Main method for all the Powermeter-handling
async def PowermeterHandling():
Logger.LogMessage("Powermeter handling started")
while True:
ReadTemp()
PowerMeasurement()
SendAPI()
GetStatsAPI()
await asyncio.sleep(0.01)
# Main method for the API
html = """<!DOCTYPE html>
<html>
<head> <title>Powermeter</title> </head>
<body> <h1>Powermeter</h1>
<p>%s</p>
</body>
</html>"""
json = """{ "Powermeter": { "%s" } }"""
async def APIHandling(reader, writer):
request_line = await reader.readline()
while await reader.readline() != b"\r\n":
pass
request = str(request_line)
try:
request = request.split()[1]
except IndexError:
pass
client_ip = writer.get_extra_info('peername')[0]
if request != "/favicon.ico":
Logger.LogMessage("API request: " + request + " - from client IP: " + client_ip)
if (configs['api_client_ip'] != "") and (configs['api_client_ip'] != client_ip):
Logger.LogMessage("Unauthorized client! Aborting API Handling now.")
stateis = "<b>Error 401:</b> Client '" + client_ip + "' is not authorized to use the API!<br><br>Set authorized client IP in configs.py!"
response = html % stateis
writer.write('HTTP/1.0 401 Unauthorized\r\nContent-type: text/html\r\n\r\n')
else:
req = request.split('/')
stateis = ""
if (len(req) == 3 or len(req) == 4 or len(req) == 5):
if (req[1] == secrets['api']):
if (req[2] == "ping"):
stateis = "ping: OK"
elif (req[2] == "total"):
stateis = "power meter count total: " + GetZaehlerstand() + " kWh"
elif (req[2] == "current"):
stateis = "current power consumption: " + GetCurrentPower() + " W"
elif (req[2] == "stats"):
stateis = "IP address: " + Networking.GetIPAddress() + "<br>MAC address: " + Networking.GetMACAddress() + "<br>Hostname: " + configs['hostname'] + "<br>API Port: " + str(configs['api_port']) + "<br>Uptime (d:h:m): " + Uptime() + "<br>Date/Time: " + TimeUtils.DateTimeNow() + "<br>Version: " + version + "<br>GMT Timezone Offset (hours): " + str(configs['gmt_offset']) + "<br>Auto summertime: " + str(configs['auto_summertime']) + "<br>Housekeep logfiles after days: " + str(configs['log_housekeeping_days']) + "<br>CPU frequency (MHz): " + str(machine.freq()/1000000) + "<br>Temperature (&#176;C): " + "%.2f" % temperature + "<br>Powermeter count (kWh): " + GetZaehlerstand() + "<br>Interval to store via API in DB (minutes): " + str(configs['db_api_interval_minutes']) + "<br>Ticks per kWh: " + str(configs['ticks_per_kWh']) + "<br>Current power consumption (W): " + GetCurrentPower() + "<br>Consumption Today (kWh): " + GetTodayConsumption() + "<br>Consumption Yesterday (kWh): " + GetYesterdayConsumption() + "<br>Consumption this week (kWh): " + GetWeekConsumption() + "<br>Consumption last week (kWh): " + GetConsumptionLastWeek() + "<br>Consumption this month (kWh): " + GetConsumptionMonth() + "<br>Consumption last month (kWh): " + GetConsumptionLastMonth() + "<br>Consumption this year (kWh): " + GetConsumptionYear()
elif (req[2] == "reboot"):
stateis = "Rebooting device: now..."
Reboot()
elif (req[2] == "settotal"):
if (len(req) == 4):
if (IsInt(req[3])):
SetZaehlerstand(req[3])
stateis = "Zaehlerstand: " + GetZaehlerstand() + " kWh"
else:
stateis = "<b>Error:</b> Parameter for set not an integer!"
else:
stateis = "<b>Error:</b> Unknown command!"
elif (req[2] == "logs"):
if (len(req) >= 4 and req[3] != "json"):
if (IsInt(req[3])):
stateis = Logger.LastLogs(int(req[3]))
else:
stateis = "<b>Error:</b> Parameter for log length not an integer!"
else:
stateis = Logger.LastLogs(10)
else:
stateis = "<b>Error:</b> Unknown command!"
else:
stateis = "<b>Error:</b> API key is invalid!"
if ((len(req) == 4 and req[3] == "json") or (len(req) == 5 and req[4] == "json")):
if (req[2] != "logs"):
stateis = stateis.replace(": ", "\":\"")
stateis = stateis.replace("<br>", "\", \"")
stateis = stateis.replace("&#176;", "°")
else:
stateis = stateis.replace(";", "\":\"")
stateis = stateis.replace("<br>", "\", \"")
stateis = stateis.replace("\n", "").replace("\r", "")
response = json % stateis
writer.write('HTTP/1.0 200 OK\r\nContent-type: text/json\r\n\r\n')
else:
response = html % stateis
writer.write('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
else:
stateis = "<b>Error 400:</b> Invalid usage of API!<br><br><u>Usage:</u> http://servername/api_key/command[/json]<br><br><u>Commands:</u><ul><li>ping</li><li>total</li><li>current</li><li>stats</li><li>reboot</li><li>settotal</li><li>logs</li></ul><br><u>API Key:</u> set 'api' in secrets.py file."
response = html % stateis
writer.write('HTTP/1.0 400 Bad Request\r\nContent-type: text/html\r\n\r\n')
writer.write(response)
await writer.drain()
await writer.wait_closed()
# Main method for daily housekeeping
async def Housekeeper():
Logger.LogMessage("Housekeeper started")
while True:
Logger.LogMessage("Housekeeper is performing actions")
Logger.Housekeeping()
Logger.LogMessage("Housekeeper is performing NTP sync")
NTP.SetRTCTimeFromNTP(configs['ntp_host'], configs['gmt_offset'], configs['auto_summertime'])
Logger.LogMessage("Housekeeper has finished its jobs")
await asyncio.sleep(86400)
# Main entry point after booting
async def Main():
global autoState
set_global_exception()
Logger.LogMessage("Entering MainLoop")
boottime = time.time()
loop = asyncio.get_event_loop()
Logger.LogMessage("Setting up API on port " + str(configs['api_port']) + " with key " + secrets['api'])
loop.create_task(asyncio.start_server(APIHandling, Networking.GetIPAddress(), configs['api_port']))
Logger.LogMessage("API started")
Logger.LogMessage("Booting complete with Firmware " + version)
CheckPowermeterCount()
loop.create_task(PowermeterHandling())
loop.create_task(Housekeeper())
loop.run_forever()
# Booting the device
def Boot():
Networking.Connect(configs['disable_wifi_powersavingmode'])
if (Networking.Status()):
if (NTP.SetRTCTimeFromNTP(configs['ntp_host'], configs['gmt_offset'], configs['auto_summertime'])):
Logger.DisableTempLogfile()
else:
time.sleep(3)
Reboot()
#####################################################################
Boot()
try:
asyncio.run(Main())
except KeyboardInterrupt:
Logger.LogMessage("Shutdown.")
finally:
asyncio.new_event_loop()