2024-04-16 18:29:19 +02:00
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.9 "
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 = configs [ " soil_dry_value " ]
if ( lastSoil2 > 25000 ) :
lastSoil2 = configs [ " soil_dry_value " ]
if ( lastSoil3 > 25000 ) :
lastSoil3 = configs [ " soil_dry_value " ]
if ( ( lastSoil1 + lastSoil2 + lastSoil3 ) / 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 & auml ; sserung < / title > < / head >
< body > < h1 > Bew & auml ; 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 \n Content-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 \n Content-type: text/json \r \n \r \n ' )
else :
response = html % stateis
writer . write ( ' HTTP/1.0 200 OK \r \n Content-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 \n Content-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 :
2023-07-01 07:28:23 +02:00
asyncio . new_event_loop ( )