diff --git a/configs.py b/configs.py new file mode 100644 index 0000000..6835ac6 --- /dev/null +++ b/configs.py @@ -0,0 +1,16 @@ +configs = { + 'country': 'AT', + 'ntp_host': 'pool.ntp.org', + 'gmt_offset': 1, + 'auto_summertime': True, + 'disable_wifi_powersavingmode': True, + 'api_port': 80, + 'hostname': 'watering', + 'log_housekeeping_days': 7, + 'api_client_ip': '', + 'pulse_duration': 15, #seconds + 'soil_moisture_measure_interval': 20, #minutes + 'soil_dry_value': 11500, #soil is dry above this value + 'check_for_update': 60, #minutes + 'ota_host': 'http://192.168.1.132' #http://ip-of-your-update-host - leave empty to disable + } \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..c67a59d --- /dev/null +++ b/main.py @@ -0,0 +1,405 @@ +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 = """ + +
%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!