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:
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()