405 lines
16 KiB
Python
405 lines
16 KiB
Python
|
import rp2, re, os
|
||
|
import TimeUtils, Logger, Networking, NTP, OTA
|
||
|
import uasyncio as asyncio
|
||
|
import utime as time
|
||
|
import usocket as socket
|
||
|
from machine import ADC, Pin, Timer
|
||
|
|
||
|
from secrets import secrets
|
||
|
from configs import configs
|
||
|
|
||
|
rp2.country(configs['country'])
|
||
|
version = "0.8"
|
||
|
|
||
|
TimeUtils = TimeUtils.TimeUtils()
|
||
|
Logger = Logger.Logger(configs['log_housekeeping_days'])
|
||
|
Networking = Networking.Networking(Logger, secrets['ssid'], secrets['pw'])
|
||
|
NTP = NTP.NTP(Logger)
|
||
|
ota_host = configs['ota_host']
|
||
|
project_name = "watering"
|
||
|
filenames = ["configs.py", "Logger.py", "main.py", "Networking.py", "NTP.py", "OTA.py", "secrets.py", "TimeUtils.py"]
|
||
|
|
||
|
boottime = time.time()
|
||
|
pumpRelais = Pin(15, Pin.OUT, value=0) #gpio15
|
||
|
heartbeatLed = Pin(0, Pin.OUT, value=0) #gpio0
|
||
|
wifiLed = Pin(1, Pin.OUT, value=0) #gpio1
|
||
|
autoLed = Pin(2, Pin.OUT, value=0) #gpio2
|
||
|
pumpLed = Pin(3, Pin.OUT, value=0) #gpio3
|
||
|
tempSensor = ADC(4) #internal temperature sensor
|
||
|
soilSensor1 = ADC(0) #gpio26
|
||
|
soilSensor2 = ADC(1) #gpio27
|
||
|
soilSensor3 = ADC(2) #gpio28
|
||
|
waterLevelSensor = Pin(5, Pin.IN, Pin.PULL_UP) #gpio5
|
||
|
pumpState = False
|
||
|
autoState = False
|
||
|
soilDry = False
|
||
|
waterLevel = False
|
||
|
wateringPulseState = False
|
||
|
temperature = 0.0
|
||
|
lastSoil1 = 0
|
||
|
lastSoil2 = 0
|
||
|
lastSoil3 = 0
|
||
|
lastWateringPulse = 0
|
||
|
|
||
|
# 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 hours and minutes
|
||
|
def Uptime():
|
||
|
global boottime
|
||
|
seconds = (time.time() - boottime) % (24 * 3600)
|
||
|
hours = seconds // 3600
|
||
|
seconds %= 3600
|
||
|
minutes = seconds // 60
|
||
|
seconds %= 60
|
||
|
return "%d:%02d" % (hours, minutes)
|
||
|
|
||
|
# Turns on the watering pump
|
||
|
def WateringOn():
|
||
|
global pumpState, waterLevel
|
||
|
if (waterLevel):
|
||
|
pumpRelais.on()
|
||
|
pumpLed.on()
|
||
|
pumpState = True
|
||
|
Logger.LogMessage("Pump turned on")
|
||
|
else:
|
||
|
pumpRelais.off()
|
||
|
pumpLed.off()
|
||
|
pumpState = False
|
||
|
Logger.LogMessage("Could not turn on the pump - water level low!")
|
||
|
|
||
|
# Turns off the watering pump
|
||
|
def WateringOff(value = ""):
|
||
|
global pumpState, wateringPulseState
|
||
|
pumpRelais.off()
|
||
|
pumpLed.off()
|
||
|
pumpState = False
|
||
|
wateringPulseState = False
|
||
|
Logger.LogMessage("Pump turned off")
|
||
|
|
||
|
# Returns the state of the watering pump
|
||
|
def WateringState():
|
||
|
global pumpState
|
||
|
if (pumpState):
|
||
|
return "on"
|
||
|
else:
|
||
|
return "off"
|
||
|
|
||
|
# Reads the onboard temperature sensor's value
|
||
|
def ReadTemp():
|
||
|
global temperature
|
||
|
temperature = 27 - ((tempSensor.read_u16() * (3.3 / (65535))) - 0.706) / 0.001721
|
||
|
|
||
|
# Turns on the automatic watering mode and writes file to set automode on reboot
|
||
|
def AutoModeOn():
|
||
|
global autoState
|
||
|
if not "auto" in os.listdir():
|
||
|
file = open("auto","w")
|
||
|
file.write("auto")
|
||
|
file.close()
|
||
|
autoLed.on()
|
||
|
autoState = True
|
||
|
Logger.LogMessage("Automatic mode turned on")
|
||
|
|
||
|
# Turns off the automatic watering mode and removes file for automode on reboot
|
||
|
def AutoModeOff():
|
||
|
global autoState
|
||
|
if "auto" in os.listdir():
|
||
|
os.remove("auto")
|
||
|
autoLed.off()
|
||
|
autoState = False
|
||
|
Logger.LogMessage("Automatic mode turned off")
|
||
|
|
||
|
# Returns the state of the automatic watering mode
|
||
|
def AutoModeState():
|
||
|
global autoState
|
||
|
if (autoState):
|
||
|
return "on"
|
||
|
else:
|
||
|
return "off"
|
||
|
|
||
|
# Turns on the watering pump for pulse_duration seconds and turns it off after that
|
||
|
def WateringPulse():
|
||
|
global waterLevel, wateringPulseState, lastWateringPulse
|
||
|
if (waterLevel):
|
||
|
WateringOn()
|
||
|
lastWateringPulse = time.time()
|
||
|
wateringPulseState = True
|
||
|
timer3 = Timer(period=(1000 * configs["pulse_duration"]), mode=Timer.ONE_SHOT, callback=WateringOff)
|
||
|
Logger.LogMessage("Triggered watering pulse")
|
||
|
else:
|
||
|
Logger.LogMessage("Could not trigger watering pulse - Water level low!")
|
||
|
|
||
|
# Reads the soil moisture sensors and triggers watering pulse if automatic mode is enabled
|
||
|
def ReadSoil():
|
||
|
global soilDry, autoState, lastSoil1, lastSoil2, lastSoil3
|
||
|
read1 = soilSensor1.read_u16()
|
||
|
read2 = soilSensor2.read_u16()
|
||
|
read3 = soilSensor3.read_u16()
|
||
|
Logger.LogSoil(str(read1) + "," + str(read2) + "," + str(read3) + ",")
|
||
|
lastSoil1 = read1
|
||
|
lastSoil2 = read2
|
||
|
lastSoil3 = read3
|
||
|
# eliminate false readings
|
||
|
if (lastSoil1 > 25000):
|
||
|
lastSoil1 = 100
|
||
|
if (lastSoil2 > 25000):
|
||
|
lastSoil2 = 100
|
||
|
if (lastSoil3 > 25000):
|
||
|
lastSoil3 = 100
|
||
|
if ((read1 + read2 + read3) / 3 <= configs["soil_dry_value"]):
|
||
|
soilDry = False
|
||
|
Logger.LogMessage("Soil is wet")
|
||
|
else:
|
||
|
soilDry = True
|
||
|
Logger.LogMessage("Soil is dry!")
|
||
|
if (autoState):
|
||
|
Logger.LogMessage("Triggering watering pulse due to automatic mode")
|
||
|
WateringPulse()
|
||
|
|
||
|
# Returns if soil is dry or wet
|
||
|
def SoilState():
|
||
|
global soilDry
|
||
|
if (soilDry):
|
||
|
return "dry"
|
||
|
else:
|
||
|
return "wet"
|
||
|
|
||
|
# Returns the water level state
|
||
|
def WaterLevelState():
|
||
|
global waterLevel, pumpState
|
||
|
if (waterLevel):
|
||
|
return "full"
|
||
|
else:
|
||
|
return "empty"
|
||
|
|
||
|
# Reads the water level sensor
|
||
|
def ReadWaterLevel():
|
||
|
global waterLevel
|
||
|
if (waterLevelSensor.value() == 0):
|
||
|
waterLevel = True
|
||
|
else:
|
||
|
waterLevel = False
|
||
|
if (pumpState):
|
||
|
Logger.LogMessage("Water level low - protecting the pump")
|
||
|
WateringOff()
|
||
|
|
||
|
# This function makes sure, the watering pulse got turned off by the timer (can happen that it fails)
|
||
|
def CheckWateringPulseOff():
|
||
|
global pumpState, lastWateringPulse
|
||
|
if (pumpState):
|
||
|
if (lastWateringPulse + 1 + configs["pulse_duration"] <= time.time()):
|
||
|
Logger.LogMessage("Turning watering pump off, because pulse timer failed to do so")
|
||
|
WateringOff()
|
||
|
|
||
|
#####################################################################
|
||
|
|
||
|
# 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 Watering-handling
|
||
|
async def WateringHandling():
|
||
|
Logger.LogMessage("Watering handling started")
|
||
|
while True:
|
||
|
ReadTemp()
|
||
|
ReadWaterLevel()
|
||
|
CheckWateringPulseOff()
|
||
|
#todo any super cool action here
|
||
|
heartbeatLed.toggle()
|
||
|
await asyncio.sleep(0.5)
|
||
|
|
||
|
# Main method for the API
|
||
|
html = """<!DOCTYPE html>
|
||
|
<html>
|
||
|
<head> <title>Bewässerung</title> </head>
|
||
|
<body> <h1>Bewässerung</h1>
|
||
|
<p>%s</p>
|
||
|
</body>
|
||
|
</html>"""
|
||
|
json = """{ "Watering": { "%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] == "wateron"):
|
||
|
Logger.LogMessage("Watering turned on")
|
||
|
stateis = "Watering turned: on"
|
||
|
WateringOn()
|
||
|
elif (req[2] == "wateroff"):
|
||
|
Logger.LogMessage("Watering turned off")
|
||
|
stateis = "Watering turned: off"
|
||
|
WateringOff()
|
||
|
elif (req[2] == "waterstate"):
|
||
|
Logger.LogMessage("Watering is turned " + WateringState())
|
||
|
stateis = "Watering is turned: " + WateringState()
|
||
|
elif (req[2] == "waterpulse"):
|
||
|
Logger.LogMessage("Watering Pulse activated")
|
||
|
stateis = "Watering Pulse: activated"
|
||
|
WateringPulse()
|
||
|
elif (req[2] == "autoon"):
|
||
|
Logger.LogMessage("Automatic-Mode turned on")
|
||
|
stateis = "Automatic-Mode turned: on"
|
||
|
AutoModeOn()
|
||
|
elif (req[2] == "autooff"):
|
||
|
Logger.LogMessage("Automatic-Mode turned off")
|
||
|
stateis = "Automatic-Mode turned: off"
|
||
|
AutoModeOff()
|
||
|
elif (req[2] == "autostate"):
|
||
|
Logger.LogMessage("Automatic-Mode is turned " + AutoModeState())
|
||
|
stateis = "Automatic-Mode is turned: " + AutoModeState()
|
||
|
elif (req[2] == "forcesoilread"):
|
||
|
ReadSoil()
|
||
|
Logger.LogMessage("Forced Soil Measurement Readings: " + str(lastSoil1) + ", " + str(lastSoil2) + ", " + str(lastSoil3))
|
||
|
stateis = "Forced Soil Measurement Readings: now<br>soil1: " + str(lastSoil1) + "<br>soil2: " + str(lastSoil2) + "<br>soil3: " + str(lastSoil3)
|
||
|
elif (req[2] == "waterlevel"):
|
||
|
Logger.LogMessage("Water level is: " + WaterLevelState())
|
||
|
stateis = "Water level is: " + WaterLevelState()
|
||
|
elif (req[2] == "ping"):
|
||
|
stateis = "ping: OK"
|
||
|
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 (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>Pulse duration (seconds): " + str(configs["pulse_duration"]) + "<br>Soil moisture measurement interval (minutes): " + str(configs["soil_moisture_measure_interval"]) + "<br>Soil is: " + SoilState() + "<br>Soil moisture 1: " + str(lastSoil1) + "<br>Soil moisture 2: " + str(lastSoil2) + "<br>Soil moisture 3: " + str(lastSoil3) + "<br>Automatic watering Mode: " + AutoModeState() + "<br>Water level status: " + WaterLevelState() + "<br>Check for updates (minutes): " + str(configs["check_for_update"])
|
||
|
elif (req[2] == "reboot"):
|
||
|
stateis = "Rebooting device: now..."
|
||
|
Reboot()
|
||
|
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)
|
||
|
elif (req[2] == "soils"):
|
||
|
if (len(req) >= 4 and req[3] != "json"):
|
||
|
if (IsInt(req[3])):
|
||
|
stateis = Logger.LastSoils(int(req[3]))
|
||
|
else:
|
||
|
stateis = "<b>Error:</b> Parameter for soil length not an integer!"
|
||
|
else:
|
||
|
stateis = Logger.LastSoils(6)
|
||
|
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" and req[2] != "soils"):
|
||
|
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>wateron</li><li>wateroff</li><li>waterstate</li><li>waterpulse</li><li>autoon</li><li>autooff</li><li>autostate</li><li>waterlevel</li><li>ping</li><li>stats</li><li>reboot</li><li>logs</li><li>soils</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)
|
||
|
ReadSoil()
|
||
|
timer = Timer(period=(1000 * 60 * configs["soil_moisture_measure_interval"]), mode=Timer.PERIODIC, callback=lambda t:ReadSoil())
|
||
|
Logger.LogMessage("Started timer for soil moisture measurement every " + str(configs["soil_moisture_measure_interval"]) + " minutes")
|
||
|
if "auto" in os.listdir():
|
||
|
autoState = True
|
||
|
autoLed.on()
|
||
|
Logger.LogMessage("Turned Automatic mode on from previous state")
|
||
|
loop.create_task(WateringHandling())
|
||
|
loop.create_task(Housekeeper())
|
||
|
loop.run_forever()
|
||
|
|
||
|
# Booting the device
|
||
|
def Boot():
|
||
|
Networking.Connect(configs['disable_wifi_powersavingmode'])
|
||
|
if (Networking.Status()):
|
||
|
wifiLed.on()
|
||
|
if (NTP.SetRTCTimeFromNTP(configs['ntp_host'], configs['gmt_offset'], configs['auto_summertime'])):
|
||
|
Logger.DisableTempLogfile()
|
||
|
Logger.DisableTempSoilfile()
|
||
|
if (ota_host != ""):
|
||
|
OTA.ota_update(ota_host, project_name, filenames, use_version_prefix=False, hard_reset_device=True, soft_reset_device=False, timeout=5)
|
||
|
timer2 = Timer(period=(1000 * 60 * configs["check_for_update"]), mode=Timer.PERIODIC, callback=lambda t:OTA.check_for_ota_update(ota_host, project_name, soft_reset_device=False, timeout=5))
|
||
|
else:
|
||
|
wifiLed.off()
|
||
|
time.sleep(3)
|
||
|
Reboot()
|
||
|
|
||
|
#####################################################################
|
||
|
|
||
|
Boot()
|
||
|
try:
|
||
|
asyncio.run(Main())
|
||
|
except KeyboardInterrupt:
|
||
|
Logger.LogMessage("Shutdown.")
|
||
|
pumpRelais.off()
|
||
|
pumpLed.off()
|
||
|
autoLed.off()
|
||
|
heartbeatLed.off()
|
||
|
wifiLed.off()
|
||
|
finally:
|
||
|
asyncio.new_event_loop()
|