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 = """
Bewässerung
Bewässerung
%s
"""
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 = "Error 401: Client '" + client_ip + "' is not authorized to use the API!
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
soil1: " + str(lastSoil1) + "
soil2: " + str(lastSoil2) + "
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() + "
MAC address: " + Networking.GetMACAddress() + "
Hostname: " + configs['hostname'] + "
API Port: " + str(configs['api_port']) + "
Uptime (h:m): " + Uptime() + "
Date/Time: " + TimeUtils.DateTimeNow() + "
Version: " + version + "
GMT Timezone Offset (hours): " + str(configs['gmt_offset']) + "
Auto summertime: " + str(configs['auto_summertime']) + "
Housekeep logfiles after days: " + str(configs['log_housekeeping_days']) + "
CPU frequency (MHz): " + str(machine.freq()/1000000) + "
Temperature (°C): " + "%.2f" % temperature + "
Pulse duration (seconds): " + str(configs["pulse_duration"]) + "
Soil moisture measurement interval (minutes): " + str(configs["soil_moisture_measure_interval"]) + "
Soil is: " + SoilState() + "
Soil moisture 1: " + str(lastSoil1) + "
Soil moisture 2: " + str(lastSoil2) + "
Soil moisture 3: " + str(lastSoil3) + "
Automatic watering Mode: " + AutoModeState() + "
Water level status: " + WaterLevelState() + "
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 = "Error: 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 = "Error: Parameter for soil length not an integer!"
else:
stateis = Logger.LastSoils(6)
else:
stateis = "Error: Unknown command!"
else:
stateis = "Error: 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("
", "\", \"")
stateis = stateis.replace("°", "°")
else:
stateis = stateis.replace(";", "\":\"")
stateis = stateis.replace("
", "\", \"")
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 = "Error 400: Invalid usage of API!
Usage: http://servername/api_key/command[/json]
Commands:- wateron
- wateroff
- waterstate
- waterpulse
- autoon
- autooff
- autostate
- waterlevel
- ping
- stats
- reboot
- logs
- soils
API Key: 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()