,

DIY irrigation control with Home Assistant and ESPHome

This is how my control system for 24 volt irrigation valves based on ESPHome works.

With our cistern, which we got last year as a buffer and with the new pipes for rainwater drainage, there is plenty of free water from the roof. At least when it rains again one day.

We have therefore laid pipes for automatic irrigation on the property, using the Gardena irrigation system, as these components were cheaper last year than the alternatives from Hunter or Rainbird.

We control 5 individual circuits via 5 valves:

  • East lawn irrigation
  • West lawn irrigation
  • Raised bed / flower beds
  • Front garden
  • Water outlet in the driveway

The material used was

Gardena Versenkregner

The electronics of the Waterking

The typical irrigation valves are controlled with 24 volts alternating current. According to the data sheet, the switch-on current is 370 mA, the holding current 210 mA. According to my measurements, the actual values were somewhat lower.

As all the valves are never in operation at the same time because otherwise the water pressure would drop too much (even with the 4 bar submersible pressure pump), a 24 volt AC power supply with 1 ampere is sufficient. I still had this from an old outdoor light chain. You can also find suitable power supply units on Ebay.

If the valves were operated with DC voltage, the cores of the electromagnets could become permanently magnetised and the valves would “stick”. In the worst case, the core would become saturated, which could mean a higher current flow and thus the destruction of the coils.

Therefore, MOSFETs cannot be used for simple control. The simplest solution would be relays, but I didn’t want to rely on that. Firstly, the contacts can corrode and the simple Chinese relays, which are available ready-made as 4 or 8-pin boards, are not necessarily suitable for switching low voltage. This sounds paradoxical, but most alloys used for relay contacts (AgCdO for these relays) require a short arc, which only occurs at higher voltages and currents (the so-called frit current). This burns away oxides, which ensures a permanently good contact. As there is also high humidity in the shed, relays were ruled out for me.

The triacs of the 5 channels with the white optocouplers

For my DIY controller, which I christened Waterking, I opted for BTA08-600B triacs, which are controlled via a MOC3041 optocoupler that also contains a zero-crossing triac driver. If you add 5 resistors, an NPN transistor and an LED, one channel costs less than one euro and you will never have contact problems. The LEDs indicate on the hardware side whether a valve is open and the optocoupler is activated.

As you already have the 24 volts, you can also use them to supply the ESP8266 and the display. Rectified, you get 35 V DC, which I convert very efficiently to 5 V using an LM2596S step-down converter.

As is often the case with my projects, a Wemos D1 Mini Pro is used because I bought a large quantity at a good price a long time ago. The Wemos is also utilised down to the (almost) last GPIO. I need 6 outputs to control 5 valves and our submersible pressure pump. Now you could use 6 GPIOs or use a shift register of type SN74HC595. This allows you to switch 8 outputs with 3 control pins. If you need more, you can cascade up to 4 SN74HC595 under ESPHome, for which you then need one more pin, but this gives you 32 outputs.

The pump is switched via a solid state relay (SSR).

TL – 136 Flüssigkeitsstandsender Wasser Ölstandsensor Detektor 24VDC 420mA Signalausgang(0-2m), Sender
  • Mehrschichtiger wasserdichter Schutz IP68, 45-Grad-Schrägwinkel-Anti-Schock, abnehmbarer Anti-Blocking-Schutz.
  • Eingangspegelgeber, hochpräzise Diffusion von Silizium, genauere Überwachung des Wasserstandes.
  • Das gemessene Medium ist Wasser, Öl und andere Flüssigkeiten, die Edelstahl nicht angreifen.
LAOMAO 5X Step-up Boost Power Converter XL6009 für Arduino Raspberry DIY-Projects basteln
  • Es handelt sich hierbei um ein Step-UP, Spannungsregler Aufwärtswandler oder Boost-Converter.
  • Die Ausgangsspannung ist nach dem Umwandlung immer größer als die Eingangsspannung.
  • Die Platine wandelt Spannungen von 3V-32V, einstellbar an dem Potentiometer in 5V – 35V Volt um.
Kemo M167N Füllstandsanzeige für Wassertanks batteriebetrieben. Fernmessung bis 100 Meter. Für Regenwasser, Klärkammern, Gülletanks. Mit LED Anzeigen
  • batteriebetriebene füllstandanzeige
  • misst füllstände von wassertanks wie regenwasser, klärkammern oder gülletanks
  • anzeige erfolgt nach knopfdruck über 10 led’s
Strom zu Spannung Wandler Modul 0-20mA 4-20mA 0-3.3V 0-5V 0-10V Signalumformer DC DC Konverter Platine
  • BREITER AUSGANGSVOLATFE-BEREICH – Der Strom-Spannungs-Wandler hat einen weiten Versorgungsspannungsbereich und die Ausgangsspannung unterstützt mehrere Bereiche
  • EINSTELLBAR – Der Strom-Spannungs-Wandler verwendet eine ausgeklügelte Produktionstechnologie, der 0-Punkt und der Vollbereich können selbst eingestellt werden
  • KUNSTSTOFFMATERIAL – Das Konvertermodul besteht aus hochwertigem Kunststoff und hat eine hohe Stabilität, gute Linearität und Industriequalität

Originally, my controller was supposed to remain “dumb”, i.e. only make the triac outputs switchable via Home Assistant. But if you already have a microcontroller, you might as well add a few extra functions. For example, the valves switch off after a configurable time (10 minutes). If no valve is active, the pump is also deactivated. If the WLAN connection is lost during irrigation or Home Assistant has a bug, the valves and pump switch off automatically and autonomously. I built everything on a strip grid board. I’m “Team Strip Grid” – I just don’t like the messing around with solder on the perfboards.

Please understand that I don’t have a plan for the layout of the board. I do things like this freely and without much pre-planning directly from the circuit diagram. Just test the position of the parts and off you go. Maybe I’ll add a circuit board layout with KiCAD when I get the chance – maybe one of you would like to do that too? The circuit diagram in KiCAD is available.

Circuit diagram without display and encoder

Using a rotary encoder and the LED display, I can not only display the status of the valves (in addition to the LEDs on the optocoupler), but also switch them on and off directly on the controller. Other menu levels show the water level of the cistern in centimetres and litres, the strength of the WLAN signal, today’s water consumption and the time. I have described how to build the water level meter with a TL-136 sensor here: https://staging.nachbelichtet.com/wasserstand-in-zisternen-mit-homeassistant-esphome-und-tl-136-drucksensor-messen/

Water level in the cistern

You can change levels by simply turning the encoder. Pressing the button takes you to the setup menu for the valves. A dash indicates an inactive valve, a box an active one. The decimal point indicates which valve has just been selected and a long press on the button switches it on or off. A short press takes you back to the main menu.

The Waterking in use

I was once tempted to create a convenient menu with a simple 8-digit LED display. It doesn’t always have to be an OLED or LC display and the LED display is very robust, which is not unimportant at -20 to 50 °C in the shed.

The various menus and settings in the display

By checking the water level in the cistern, I can deactivate the pump if the water level drops below 12 cm. A long press on the encoder button switches off all valves and the pump immediately.

Valve control in Home Assistant with cistern water level

Interlocking the outputs prevents certain valves from being active at the same time. This not only prevents a drop in pressure, but also prevents the power pack from being overloaded. A maximum of 3 valves(Gardena Micro Trip dripper raised bed, front garden and water withdrawal) can be active and only one of the two circuits for the lawn area. The controller also provides this safety function directly – regardless of what else I would configure in Home Assistant.

3D-printed housing for the Waterking

To ensure that everything is properly packaged and looks good, I printed a housing from PETG and PLA. The transparent PETG allows the LEDs and the display to shine through.

Housing with abstracted model of the circuit board

For the display cut-out, I chose the layer thickness during construction so that no infill is printed. This makes it look like a transparent film.

The entire housing can be closed without screws and the circuit board is also only held in place by snap fasteners. With a handmade circuit board, the construction of a housing with a lid is not entirely trivial, but it worked on the first attempt and print.

Construction in Fusion 360

A holder for a spare fuse is also included as a gag. Access to the connection terminals is from below. As always, I used Autodesk Fusion 360 for the design. My Creality Ender 3 did the printing in about 10 hours.

Access to the connection terminals

If you don’t want to go to the trouble of controlling the valves, you can also use an SSR board instead of the triacs, which you can get for less than €20. Here you only have to control the channels with the ESP. The code below can also be used for this.

SSR module with 8 channels.

I have annotated the YAML configuration in ESPHome accordingly:

esphome:
  name: waterking
  platform: ESP8266
  board: d1_mini
  # Switch off all outputs at startup for safety reasons
  on_boot: 
    then:
      - switch.turn_off: v1
      - switch.turn_off: v2
      - switch.turn_off: v3
      - switch.turn_off: v4
      - switch.turn_off: v5
      - switch.turn_off: pump
      - sensor.rotary_encoder.set_value:
          id: enc
          value: 0

# Switch off serial logger on RX/TX to be able to use pins for other tasks
logger: 
  baud_rate: 0

# Activate Home Assistant API
api:

ota:
  password: !secret otapass"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  use_address: waterking.local

  # Fallback hotspot
  ap:
    ssid: "Bewaesserung Fallback Hotspot"
    password: !secret hotspot

captive_portal:

# Get time from HA
time:
  - platform: homeassistant
    id: homeassistant_time

# Set global variables
globals: 
  - id: setpage #Setting mode
    type: bool
    restore_value: no
    initial_value: 'false'

  - id: maxtime #maximum valve runtime as protection
    type: int
    restore_value: yes
    initial_value: '600000' # in ms = 10 min

# Set up shift register
sn74hc595:
  - id: 'sn74hc595_hub'
    data_pin: D5
    clock_pin: D6
    latch_pin: D7
    oe_pin: D2
    sr_count: 1

# SPI for display
spi:
  clk_pin: D0
  mosi_pin: D1

# Define valves

switch:
  - platform: gpio
    name: "Valve East"
    id: v1
    pin:
      sn74hc595: sn74hc595_hub
      number: 1
      inverted: false
    interlock: [v2,v4,v5] # Do not switch on certain valves at the same time -> pressure loss
    on_turn_on:
      - switch.turn_on: pump
      - delay: !lambda "return id(maxtime);"
      - switch.turn_off: v1

  - platform: gpio
    name: "Valve West"
    id: v2
    pin:
      sn74hc595: sn74hc595_hub
      number: 2
      inverted: false
    interlock: [v1,v4,v5]
    on_turn_on:
      - switch.turn_on: pump
      - delay: !lambda "return id(maxtime);"
      - switch.turn_off: v2
      
  - platform: gpio
    name: "Ventil Beet"
    id: v3
    pin:
      sn74hc595: sn74hc595_hub
      number: 3
      inverted: false
    interlock: [v2]
    on_turn_on:
      - switch.turn_on: pump
      - delay: !lambda "return id(maxtime);"
      - switch.turn_off: v3
            
  - platform: gpio
    name: "Front garden valve"
    id: v4
    pin:
      sn74hc595: sn74hc595_hub
      number: 4
      inverted: false
    interlock: [v1,v2]
    on_turn_on:
      - switch.turn_on: pump
      - delay: !lambda "return id(maxtime);"
      - switch.turn_off: v4
             
  - platform: gpio
    name: "Valve inlet"
    id: v5
    pin:
      sn74hc595: sn74hc595_hub
      number: 5
      inverted: false
    interlock: [v2,v5]
    on_turn_on:
      - switch.turn_on: pump
      - delay: 1h #Valve 5 may be on for max. 1 hour -> water withdrawal
      - switch.turn_off: v5

# Solid state relay output for pump control
  - platform: gpio
    name: "SSR pump"
    internal: true # Do not display pump in HA
    id: pump
    pin:
      sn74hc595: sn74hc595_hub
      number: 0
      inverted: false

# Set up display
display:
  - platform: max7219
    cs_pin: D3
    num_chips: 1
    update_interval: 500ms
    lambda: |-
      // Display page 5 time
      if ((id(enc).state == 5) && (id(setpage) == false)) {
        it.print(" ");
        it.strftime("%H.%M.%S", id(homeassistant_time).now());
      }

      // Page 4 Wifi Level
      if ((id(enc).state == 4) && (id(setpage) == false)) {
        it.print(" ");
        it.printf("Wi %.0fdB", id(wlan_signal).state);
      }

      // Page 1 Water level Height
      if ((id(enc).state == 1) && (id(setpage) == false)) {
        it.print(" ");
        it.printf("FH %.1fcn", id(cistern_cm).state);
          }

      // Page 2 Water level litres
      if ((id(enc).state == 2) && (id(setpage) == false)) {
        it.print(" ");
        it.printf("FS %.0fL", id(cistern_litre).state);
          }

      // Page 3 Water consumption today
      if ((id(enc).state == 3) && (id(setpage) == false)) {
        it.print(" ");
        it.printf(1,"= %.0f L", id(cistern_delta_today).state);

          } 

      // Display page 0 status valves    
      if ((id(enc).state == 0) && (id(setpage) == false)) {
        it.print(" ");
          if ((id(v1).state)) {
            it.print("o");
            } else {
            it.print("_");
              }
              
          if ((id(v2).state)) {
            it.print(1, "o");
            } else {
            it.print(1,"_");
              }
          
          if ((id(v3).state)) {
            it.print(2, "o");
            } else {
            it.print(2,"_");
              }
          
          if ((id(v4).state)) {
            it.print(3, "o");
            } else {
            it.print(3,"_");
              }
          
          if ((id(v5).state)) {
            it.print(4, "o");
            } else {
            it.print(4,"_");
              }
          if ((id(pump).state)) {
            it.print(6, "P");
            } else {
            it.print(6,"_");
              }
          } 
      
      // First page setup page
      if (id(setpage) == true && id(enc).state == 0) {
        it.print(" S");
        if ((id(v1).state)) {
          it.print("o");
          } else {
          it.print("_");
            }
            
        if ((id(v2).state)) {
          it.print(1, "o");
          } else {
          it.print(1,"_");
            }
        
        if ((id(v3).state)) {
          it.print(2, "o");
          } else {
          it.print(2,"_");
            }
        
        if ((id(v4).state)) {
          it.print(3, "o");
          } else {
          it.print(3,"_");
            }
        
        if ((id(v5).state)) {
          it.print(4, "o");
          } else {
          it.print(4,"_");
            }
        if ((id(pump).state)) {
          it.print(6, "P");
          } else {
          it.print(6,"_");
            }

      }


      // Set valve 1 

      if (id(setpage) == true && id(enc).state == 1) {


        if ((id(v1).state)) {
          it.print("o.");
          } else {
          it.print("_.");
            }
          
        if ((id(v2).state)) {
          it.print(1, "o");
          } else {
          it.print(1,"_");
            }
          
        if ((id(v3).state)) {
          it.print(2, "o");
          } else {
          it.print(2,"_");
            }

        if ((id(v4).state)) {
          it.print(3, "o");
          } else {
          it.print(3,"_");
            }
              
        if ((id(v5).state)) {
          it.print(4, "o");
          } else {
          it.print(4,"_");
            }
               
          } // End V1  

        // Set valve 2 

        if (id(setpage) == true && id(enc).state == 2) {

    
          if ((id(v1).state)) {
            it.print("o");
            } else {
            it.print("_");
              }
            
          if ((id(v2).state)) {
            it.print(1, "o.");
            } else {
            it.print(1,"_.");
              }
            
          if ((id(v3).state)) {
            it.print(2, "o");
            } else {
            it.print(2,"_");
              }
  
          if ((id(v4).state)) {
            it.print(3, "o");
            } else {
            it.print(3,"_");
              }
                
          if ((id(v5).state)) {
            it.print(4, "o");
            } else {
            it.print(4,"_");
              }
              
          }

          if (id(setpage) == true && id(enc).state == 3) {

    
            if ((id(v1).state)) {
              it.print("o");
              } else {
              it.print("_");
                }
              
            if ((id(v2).state)) {
              it.print(1, "o");
              } else {
              it.print(1,"_");
                }
              
            if ((id(v3).state)) {
              it.print(2, "o.");
              } else {
              it.print(2,"_.");
                }
    
            if ((id(v4).state)) {
              it.print(3, "o");
              } else {
              it.print(3,"_");
                }
                  
            if ((id(v5).state)) {
              it.print(4, "o");
              } else {
              it.print(4,"_");
                }
                
            }

            if (id(setpage) == true && id(enc).state == 4) {

    
              if ((id(v1).state)) {
                it.print("o");
                } else {
                it.print("_");
                  }
                
              if ((id(v2).state)) {
                it.print(1, "o");
                } else {
                it.print(1,"_");
                  }
                
              if ((id(v3).state)) {
                it.print(2, "o");
                } else {
                it.print(2,"_");
                  }
      
              if ((id(v4).state)) {
                it.print(3, "o.");
                } else {
                it.print(3,"_.");
                  }
                    
              if ((id(v5).state)) {
                it.print(4, "o");
                } else {
                it.print(4,"_");
                  }
                  
              }
              if (id(setpage) == true && id(enc).state == 5) {

    
                if ((id(v1).state)) {
                  it.print("o");
                  } else {
                  it.print("_");
                    }
                  
                if ((id(v2).state)) {
                  it.print(1, "o");
                  } else {
                  it.print(1,"_");
                    }
                  
                if ((id(v3).state)) {
                  it.print(2, "o");
                  } else {
                  it.print(2,"_");
                    }
        
                if ((id(v4).state)) {
                  it.print(3, "o");
                  } else {
                  it.print(3,"_");
                    }
                      
                if ((id(v5).state)) {
                  it.print(4, "o.");
                  } else {
                  it.print(4,"_.");
                    }
                    
                } 

sensor:

  - platform: rotary_encoder # Set up rotary encoder
    name: "Rotary Encoder"
    id: enc
    publish_initial_value: true
    pin_a: 
      number: TX
      inverted: true
      mode:
        input: true
        pullup: true
    pin_b: 
      number: RX
      inverted: true
      mode:
        input: true
        pullup: true
    max_value: 5
    min_value: 0

# Get fill level in litres from HA
  - platform: homeassistant
    id: cistern_litre
    entity_id: sensor.cistern_litre

# Get consumption today from HA
  - platform: homeassistant
    id: cistern_delta_today
    entity_id: sensor.cistern_delta_today

# Get fill level in cm from HA  
  - platform: homeassistant
    id: cistern_cm
    entity_id: sensor.wasserstandraw # Emergency shutdown water level < 12 cm -> pump protection
    on_value_range:
      - below: 12
        then:
          - switch.turn_off: v1
          - switch.turn_off: v2
          - switch.turn_off: v3
          - switch.turn_off: v4
          - switch.turn_off: v5
          - switch.turn_off: pump

# Get WLAN signal strength ... because you can
  - platform: wifi_signal
    name: "ESP Wifi Signal"
    update_interval: 30s
    id: wlan_signal      

binary_sensor:

  - platform: template
    name: "Any Valve On" # If all valves are off, switch off the pump
    internal: true
    lambda: 'return id(v1).state or id(v2).state or id(v3).state or id(v4).state or id(v5).state ;'
    on_release:
      then:
        - switch.turn_off: pump

  - platform: gpio # Set up buttons on the encoder
    id: encswitch
    pin: 
      number: GPIO2
      mode: INPUT_PULLUP
      inverted: True 
    on_click:
    - min_length: 50ms # Short actuation to enter the valve setup
      max_length: 250ms
      then: 
        - lambda: |-
            if(id(setpage)) {
              id(setpage) = false; 
            } else {
              id(setpage) = true; 
            } 

    - min_length: 600ms # long actuation to switch selected valve on/off
      max_length: 1500ms
      then:
        - if:
            condition:
              lambda: 'return (id(setpage) == true && id(enc).state == 1);'
            then:
              switch.toggle: v1

        - if:
            condition:
              lambda: 'return (id(setpage) == true && id(enc).state == 2);'
            then:
              switch.toggle: v2  
          
        - if:
            condition:
              lambda: 'return (id(setpage) == true && id(enc).state == 3);'
            then:
              switch.toggle: v3  

        - if:
            condition:
              lambda: 'return (id(setpage) == true && id(enc).state == 4);'
            then:
              switch.toggle: v4  
                        
        - if:
            condition:
              lambda: 'return (id(setpage) == true && id(enc).state == 5);'
            then:
              switch.toggle: v5 


    - min_length: 3000ms # very long actuation to switch off all valves and the pump
      max_length: 60000ms
      then:
        - switch.turn_off: v1
        - switch.turn_off: v2
        - switch.turn_off: v3
        - switch.turn_off: v4
        - switch.turn_off: v5
        - switch.turn_off: pump

Code language: YAML (yaml)

Irrigation control with Home Assistant

Home Assistant will take over the actual irrigation control. As various sensors such as the rain sensor of the LCN-WIH weather station, rain quantity sensor and the weather forecast are already available as entities in HA, these can be used for optimal and economical irrigation.

Installation in the shed

The level measurement of the cistern is also an excellent sensor when it comes to rainfall, as it receives the rainfall from 75 square metres of roof area. If the water level in the cistern is low, you can also shorten the watering times, etc.

Bodenfeuchtesensor
New project: The DIY soil moisture sensor

The most important values for irrigation come from a soil moisture sensor. Although capacitive sensors are relatively robust, they only measure the soil moisture in a narrow range of a few square centimetres. I have therefore built a resistive sensor from two 1 metre long stainless steel threaded rods, which are buried 20 mm apart at a depth of 10 cm.

The DIY soil moisture sensor

Stainless steel is corrosion-resistant and robust. The two M5 rods cost 3 euros in the special price DIY store. With this sensor, you can measure a representative area and not just selectively. As the measuring current and the resulting electrolysis could cause salts and minerals to be deposited on the electrodes over time, the measurement is only activated very briefly and at longer intervals. There will be a separate article on this.

Letzte Aktualisierung am 2025-12-14 / Affiliate Links / Bilder von der Amazon Product Advertising API

Abonnieren

Leave a Reply

Your email address will not be published. Required fields are marked *