Interface for Garden Watering System

Hi Everyone!

I finally got all the parts needed to make a garden watering system but I am hoping for some advice on how to do the interface for it. We have 7 beds with a solenoid valve to control watering independently for each bed. I was originally going to use a pico W for controling the valves but I also have a pi zero W that could be used if that would work better.

My original plan was to water each bed using a timer approach but we also would like to have manual control of the valves if we needed to add additional water. I was thinking of hosting a simple html page on the pico for manual valve control but I am not sure how to have the timer function run at the same time. I was playing around with multithreading a little bit last night but could not get the web page to load when the timer thread was running too (maybe the networking functions need both threads?).

I also came across OpenHub which seems like it could work for my needs (I have not tested it at all yet). I think i would still use the pico for the calve control, but i would run the OpenHub gui on the pi zero. My issue with this option though is that I would like to avoid having to go back inside and log into the zero to turn a valve on or off. The advantage of the basic html page is that we could load it on our phone to control the valves.

I am fairly familiar with python but I am still learning networking basics. I am also redoing our home network so I can run this system on a vlan for security if that opens up any options as well. If anyone has any suggestions on how I can set up a simple interface that ideally can be controlled from a phone I would very much appreciate it.
Thanks,
Eric

Hi @Eric289745

That sounds like a really interesting project!

I’ve thrown together some code below that should at least get you started with what you’re looking to do, it will get the time via NTP, and automatically turn the valves on at 6am, with them turning off at 6:10am.

I’ve added in a physical button on the pico for a manual override, as well as a master override in the web interface and also individual overrides.

You can remove any or add onto the code to meet your needs.

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]
BUTTON_PIN = 9
OVERRIDE_TIMEOUT = 10 * 60  # 10 minutes
SCHEDULE_HOUR = 6
SCHEDULE_DURATION = 10 * 60  # 10 minutes

SSID = 'YourSSID'
PASSWORD = 'YourPassword'

# === State ===
solenoids = [Pin(pin, Pin.OUT) for pin in SOLENOID_PINS]
solenoid_states = [False] * len(solenoids)

manual_override = [False] * len(solenoids)
override_start_time = [0] * len(solenoids)

# === Physical Button State ===
physical_override = False
physical_override_start = 0

# === 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.")
    except:
        print("NTP sync failed.")

# === Solenoid Control ===
def set_solenoid(index, state):
    solenoids[index].value(state)
    solenoid_states[index] = state

def update_solenoid_state():
    now = utime.time()
    t = utime.localtime()
    seconds_now = t[3] * 3600 + t[4] * 60 + t[5]
    schedule_start = SCHEDULE_HOUR * 3600
    schedule_end = schedule_start + SCHEDULE_DURATION

    global physical_override

    # Expire physical override
    if physical_override and (now - physical_override_start > OVERRIDE_TIMEOUT):
        physical_override = False
        for i in range(len(solenoids)):
            manual_override[i] = False

    for i in range(len(solenoids)):
        # Expire per-solenoid manual override
        if manual_override[i] and (now - override_start_time[i] > OVERRIDE_TIMEOUT):
            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)

# === Button Interrupt ===
button = Pin(BUTTON_PIN, Pin.IN, Pin.PULL_DOWN)

def button_handler(pin):
    global physical_override, physical_override_start
    physical_override = not physical_override
    physical_override_start = utime.time()
    new_state = not solenoid_states[0]
    for i in range(len(solenoids)):
        set_solenoid(i, new_state)
        manual_override[i] = True
        override_start_time[i] = utime.time()
    save_overrides_to_flash()

button.irq(trigger=Pin.IRQ_RISING, handler=button_handler)

# === 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
            }
            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
    try:
        if "overrides.json" in os.listdir():
            with open("overrides.json", "r") as f:
                data = ujson.load(f)
                now = utime.time()
                for i in range(len(solenoids)):
                    if data["manual_override"][i]:
                        elapsed = now - data["override_start_time"][i]
                        if elapsed < OVERRIDE_TIMEOUT:
                            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)
                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 = OVERRIDE_TIMEOUT - (utime.time() - override_start_time[i])
        return max(0, int(remaining))
    return 0

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="/toggle{i}" method="get">
            <button type="submit">Toggle Solenoid {i+1}</button>
        </form>
        """

    html = f"""<html>
    <head>
        <title>Solenoid Control</title>
        <meta http-equiv="refresh" content="10">
        <style>button {{ margin-bottom: 10px; }}</style>
    </head>
    <body>
    <h1>7-Zone Solenoid Controller</h1>
    <form action="/master" method="get">
        <button style="background-color:lightcoral;" type="submit"><strong>MASTER TOGGLE ALL</strong></button>
    </form>
    <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()

        # Handle master override toggle
        if "/master" in request:
            global physical_override, physical_override_start
            physical_override = not physical_override
            physical_override_start = utime.time()
            new_state = not solenoid_states[0]
            for i in range(len(solenoids)):
                set_solenoid(i, new_state)
                manual_override[i] = True
                override_start_time[i] = utime.time()
            save_overrides_to_flash()

        # Handle individual solenoid toggle
        for i in range(len(solenoids)):
            if f"/toggle{i}" in request:
                manual_override[i] = not manual_override[i]
                override_start_time[i] = utime.time()
                set_solenoid(i, not solenoid_states[i])
                save_overrides_to_flash()

        response = web_page()
        cl.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
        cl.send(response)
        cl.close()

# === Start System ===
ip = connect_wifi()
sync_time()
load_overrides_from_flash()
print(f"Access the web interface at http://{ip}")
serve()
1 Like

Oh wow! Thank you so much!!! This is super helpful. I will be spending some time this weekend trying to get everything up and running.

Hey @Eric289745

No worries at all, hopefully you found some success with the project :slight_smile:

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 :slightly_smiling_face:. 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.




3 Likes

@Eric289745

Great setup and project!

Always good to see a project taken all the way and finished nicely, getting the project to work in the field is always the hard part!

1 Like