360 lines
15 KiB
Python
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) + "¤t=" + 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¤t")
|
|
#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 (°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("°", "°")
|
|
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()
|