It took much longer than I was hoping and I plan to make a full post on the project, but I finally got the watering system hooked up and working! Again, such a huge thanks for getting me started!!! It would have taken so much longer without the bones of the interface you provided.
I did end up making some changes to the interface, like setting the schedule time individually for each bed and limiting the number of valves open at once to 2 (limited by water pressure and the power supply for the solenoid valves). I also spent quite a bit of time trying to get AJAX to work for updating timer values and things but it seems like you cant actually run javascript on the pico (if I am wrong please let me know though!). The page wont automatically update and have the desired behavoirs so I just turn off a valve thats already off to reload for the time being
. Here is my final working code though:
import network
import socket
from machine import Pin, Timer
import utime
import ntptime
import ujson
import os
# === Config ===
SOLENOID_PINS = [10, 11, 12, 13, 14, 15, 16]
OVERRIDE_TIMEOUT = 5 * 60 # 5 minutes, in seconds
SSID = "Fios-17GPW"
PASSWORD = "win9785dish730toy"
# === State ===
solenoids = [Pin(pin, Pin.OUT) for pin in SOLENOID_PINS]
solenoid_states = [False] * len(solenoids)
#arrays of schedule times for calcs
solenoid_startHour = [6, 6, 8, 8, 10, 10, 12]
solenoid_startMinute = [0, 0, 0, 0, 0, 0, 0]
solenoid_endHour = [7, 7, 9, 9, 11, 11, 13]
solenoid_endMinute = [0, 0, 0, 0, 0, 0, 0]
solenoid_manualTimeSec = [OVERRIDE_TIMEOUT] * len(solenoids)
#extra array for interfacing times with html page
startTime_string = ["06:00", "06:00", "08:00", "08:00", "10:00", "10:00", "12:00"]
endTime_string = ["07:00", "07:00", "09:00", "09:00", "11:00", "11:00", "13:00"]
manual_override = [False] * len(solenoids)
override_start_time = [0] * len(solenoids)
# === Wi-Fi & NTP ===
def connect_wifi():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(SSID, PASSWORD)
while not wlan.isconnected():
utime.sleep(1)
print('Connected to Wi-Fi:', wlan.ifconfig()[0])
return wlan.ifconfig()[0]
def sync_time():
try:
ntptime.settime()
print("Time synced with NTP.")
print(str(utime.localtime()))
except:
print("NTP sync failed.")
# === Solenoid Control ===
def set_solenoid(index, state):
num_on = 0
#check number of solenoids currently on
for i in range(len(solenoids)):
if solenoid_states[i]:
num_on += 1
#less than 2 are on, or state is getting turned off, then set state
if num_on < 2 or not state:
solenoids[index].value(state)
solenoid_states[index] = state
#this method is run in a timer loop to
def update_solenoid_state():
now = utime.time()
t = utime.localtime()
seconds_now = t[3] * 3600 + t[4] * 60 + t[5]
for i in range(len(solenoids)):
schedule_start = solenoid_startHour[i] * 3600 + solenoid_startMinute[i] * 60 #time in seconds
schedule_end = solenoid_endHour[i] * 3600 + solenoid_endMinute[i] * 60 #time in seconds
# Expire per-solenoid manual override
if manual_override[i] and (now - override_start_time[i] > solenoid_manualTimeSec[i]):
manual_override[i] = False
# Skip if manually overridden
if manual_override[i]:
continue
#in_schedule = schedule_start <= seconds_now < schedule_end
#set_solenoid(i, in_schedule)
#has time sarted in scheduled time
if schedule_start <= seconds_now < schedule_end:
#if solenoid is off, turn on
if not solenoid_states[i]:
set_solenoid(i, True)
#outside of schedule time
else:
#if solenoid still on, turn off
if solenoid_states[i]:
set_solenoid(i, False)
# === Timer ===
timer = Timer()
timer.init(period=10000, mode=Timer.PERIODIC, callback=lambda t: update_solenoid_state())
# === Save and Load Overrides ===
def save_overrides_to_flash():
try:
with open("overrides.json", "w") as f:
data = {
"manual_override": manual_override,
"override_start_time": override_start_time,
"solenoid_startHour": solenoid_startHour,
"solenoid_startMinute": solenoid_startMinute,
"solenoid_endHour": solenoid_endHour,
"solenoid_endMinute": solenoid_endMinute,
"startTime_string": startTime_string,
"endTime_string": endTime_string,
"solenoid_manualTimeSec": solenoid_manualTimeSec
}
ujson.dump(data, f)
print("Override states saved.")
except Exception as e:
print("Failed to save override state:", e)
def load_overrides_from_flash():
global manual_override, override_start_time, solenoid_manualTimeSec, solenoid_startHour, solenoid_startMinute, solenoid_endHour, solenoid_endMinute, startTime_string, endTime_string
try:
if "overrides.json" in os.listdir():
with open("overrides.json", "r") as f:
data = ujson.load(f)
now = utime.time()
#for
for i in range(len(solenoids)):
if data["manual_override"][i]:
elapsed = now - data["override_start_time"][i]
if elapsed < solenoid_manualTimeSec[i]:
manual_override[i] = True
override_start_time[i] = data["override_start_time"][i]
set_solenoid(i, True)
else:
manual_override[i] = False
set_solenoid(i, False)
solenoid_startHour = data["solenoid_startHour"]
solenoid_startMinute = data["solenoid_startMinute"]
solenoid_endHour = data["solenoid_endHour"]
solenoid_endMinute = data["solenoid_endMinute"]
startTime_string = data["startTime_string"]
endTime_string = data["endTime_string"]
solenoid_manualTimeSec = data["solenoid_manualTimeSec"]
print("Override states loaded from flash.")
except Exception as e:
print("Failed to load override state:", e)
# === Web Interface ===
def get_override_remaining(i):
if manual_override[i]:
remaining = solenoid_manualTimeSec[i] - (utime.time() - override_start_time[i])
return max(0, int(remaining))
return 0
#def convert_to_time_string(hours, mins):
# timeString = ""
# if hours < 10:
# timeString = "0" + str(hours) + ":"
# else:
# timeString = str(hours) + ":"
# if mins < 10:
# timeString = timeString + "0" + str(mins)
# else:
# timeString = timeString + str(mins)
# return timeString
def web_page():
solenoid_html = ""
for i, state in enumerate(solenoid_states):
mode = "Manual Override" if manual_override[i] else "Scheduled"
remaining = get_override_remaining(i)
override_info = f" | {remaining}s left" if manual_override[i] else ""
solenoid_html += f"""
<p>Solenoid {i+1}: <strong>{'ON' if state else 'OFF'}</strong> | Mode: <strong>{mode}</strong>{override_info}</p>
<form action="/ON{i}" method="get">
<button type="submit">Turn On Solenoid {i+1}</button>
</form>
<form action="/OFF{i}" method="get">
<button type="submit">Turn Off Solenoid {i+1}</button>
</form>
<form action="/ChangeTimes{i}" method="get">
<label for="start{i}">Start Time for {i+1}</label>
<input type="time" id="start{i}" value="{startTime_string[i]}" name="start{i}"><br>
<label for="end{i}">End Time for {i+1}</label>
<input type="time" id="end{i}" value="{endTime_string[i]}" name="end{i}"><br><br>
<label for="manualTime{i}">Manual Time for {i+1}</label>
<input type="text" id="manualTime{i}" value="{solenoid_manualTimeSec[i]}" name="manualTime{i}"><br><br>
<input type="submit" value="Submit"><br><br>
</form>
<hr class="solid">
"""
html = f"""<html>
<head>
<meta content='width=device-width; initial-scale=1.0; maximum-scale=1.0; minimum-scale=1.0; user-scalable=no;' name='viewport'/>
<title>Solenoid Control</title>
<style>button {{ margin-bottom: 10px; }}</style>
</head>
<body>
<h1>7-Zone Solenoid Controller</h1>
<hr>
{solenoid_html}
</body></html>"""
return html
def serve():
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
s = socket.socket()
s.bind(addr)
s.listen(1)
print('Web server running on port 80')
while True:
cl, addr = s.accept()
request = cl.recv(1024).decode()
cl.send('HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Type: text/html\r\n\r\n')
#add extra check so loop only runs once per request, not sure why loop is run twice for each web page submission
#if loop_check:
# Handle individual solenoid toggle
for i in range(len(solenoids)):
if f"/ON{i}" in request.split()[1]:
manual_override[i] = True
override_start_time[i] = utime.time()
set_solenoid(i, True)
save_overrides_to_flash()
elif f"/OFF{i}" in request.split()[1]:
manual_override[i] = False
override_start_time[i] = utime.time()
set_solenoid(i, False)
save_overrides_to_flash()
#handle changes to scheduled water times
elif f"/ChangeTimes{i}" in request.split()[1]:
url_end = request.split()[1]
#update manual time
manual_time = url_end.split("=")[3]
try:
solenoid_manualTimeSec[i] = int(manual_time)
except Exception as e:
pass
#parse times from url
start = url_end.split("=")[1]
start_hr = start.split('%')[0]
start_min = (start.split('&')[0]).split('A')[1]
end = url_end.split("=")[2]
end_hr = end.split("%")[0]
end_min = (end.split('&')[0]).split('A')[1]
#ensure end time is after start time
if int(end_hr) > int(start_hr):
#arrays of time strings for interface
startTime_string[i] = start_hr + ":" + start_min
endTime_string[i] = end_hr + ":" + end_min
#arrays of times for calcs
solenoid_startHour[i] = int(start_hr)
solenoid_startMinute[i] = int(start_min)
solenoid_endHour[i] = int(end_hr)
solenoid_endMinute[i] = int(end_min)
#check if minutes of end are larger than start if hours are same
elif int(end_hr) == int(start_hr) and int(end_min) > int(start_min):
startTime_string[i] = start_hr + ":" + start_min
endTime_string[i] = end_hr + ":" + end_min
solenoid_startHour[i] = int(start_hr)
solenoid_startMinute[i] = int(start_min)
solenoid_endHour[i] = int(end_hr)
solenoid_endMinute[i] = int(end_min)
save_overrides_to_flash()
response = web_page()
cl.sendall(response)
cl.close()
# === Start System ===
ip = connect_wifi()
sync_time()
load_overrides_from_flash()
print(f"Access the web interface at http://{ip}")
serve()
I may add multiple waterings per day at some point, and I really want to add a weather forecast check and automatically pause if rain is in the forecast, but for now it serves its purpose.