Simple, Reliable Sprinkler Controller with ESPHome

What device did you purchase specifically? Most Dev boards these Days have on-board serial USB connections but you mention a 3.3 volt programmer setting. Not typical on my experience.

i bought this model:
ESP8266 Módulo de relé inalåmbrico de 1/2/4/8/16 canales AC/DC ESP ESP-12F Placa de desarrollo WIFI - AliExpress 502

and use a FTDI programer that have a jumper to set 5v or 3.3v.

ESP-12F_Relay_X4 | ESPHome Devices If you’ve connected 5V to it you may have already killed it.

That’s a really old board. Personally, I’d stick with buying ESP32 boards in 2026.

A lot of the ESPs are sorta 5v tolerant. Keep your fingers crossed.
Are you sure that esp32dev is the right board type in your yaml?
If in doubt, post your yaml code (forum </> formatting please)

Works fine. If i quit the jumper, it starts correcto and generate the wifi.

Not start the process of first program.
The yaml now is not importante because not pass the “conecting”

I would say the yaml is very important, as your ‘connecting’ will rely on what you have there.

the yaml is basically the same as first post, with some litle changes in not core sections.

I would encourage you to start with the YAML in the link I last posted. This is basic YAML for the board. Once the board is working, add in the sprinkler config.

solved, the problem is not the yaml. because as i said the problem is in the first steep, previously to the preparing instalation.

In my case with my board:

  • connected ONLY the programer in 5V setup
  • jumper into io0 and gnd
  • push the reset button and keep it
  • connect the programer to the PC
  • push install en esphome and select the programer.
  • timeout
  • push retry, push only one time the reset button in the board. ONLY ONE TIME. NOT keeping pushed.
  • and no, conected, preparing to install, and installed

maybe this is not the correct procedure, but IN MY CASE, worked.

and now I will play :slight_smile:

Hi Jack , is your code available, i would really like to see your menu with the rotary ,to have control on the panel

Thanks

Sure,

  name: irrigation-control
  friendly_name: Irrigation control

esp32:
  board: esp32-s3-devkitc-1
  flash_size: 8MB
  framework:
    type: esp-idf

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "key"

ota:
  - platform: esphome
    password: "password"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Irrigation-Control"
    password: "rDfXc4gU5ro7"

captive_portal:

i2c:
  sda: GPIO8
  scl: GPIO9  
  frequency: 400kHz
  
# --- Display and Menu Configuration ---
font:
  - file: "fonts/arial.ttf"
    id: my_font
    size: 16

display:
  - platform: ssd1306_i2c
    id: oled_display
    model: "SSD1306 128x64"
    address: 0x3C
    pages:
      - id: page1
        lambda: |-
          if (id(backyard_sprinklers).active_valve().has_value()) {
            // the controller is running, get the active valve into running_valve and print it
            auto running_valve = id(backyard_sprinklers).active_valve().value() + 1;
            it.print(0, 0, id(my_font), "Cycle Active");
            it.printf(0, 16, id(my_font), "Zone %u", running_valve);
            it.printf(0, 32, id(my_font), "Remaining: %u", id(backyard_sprinklers).time_remaining_active_valve().value());
          } else {
            // the controller is NOT running
            it.print(0, 0, id(my_font), "");
          }
            
 # The display_menu component, set to rotary mode
graphical_display_menu:
  id: sprnklr_menu
  display: oled_display
  font: my_font
  active: false
  mode: rotary

  items:
    - type: command
      text: 'Exit Menu'
      on_value:
        then:
          - display_menu.hide: sprnklr_menu
    - type: switch
      immediate_edit: true
      text: 'Full Cycle'
      switch: sprinkler_ctrlr_main_switch
      on_enter:
        then:
        - switch.toggle: sprinkler_ctrlr_main_switch          
    - type: switch
      immediate_edit: true
      text: 'Rain Delay'
      switch: raindelay_24h_enabled
      on_enter:
        then:
        - logger.log: "Selected Rain Delay"
        - switch.toggle:  raindelay_24h_enabled
    - type: switch
      immediate_edit: True
      text: 'Zone 1'
      switch: zone_1_aux
      on_enter: 
        then:
          - switch.toggle: zone_1_aux
    - type: switch
      immediate_edit: True
      text: 'Zone 2'
      switch: zone_2_aux
      on_enter: 
        then:
          - switch.toggle: zone_2_aux
    - type: switch
      immediate_edit: True
      text: 'Zone 3'
      switch: zone_3_aux
      on_enter: 
        then:
          - switch.toggle: zone_3_aux
    - type: switch
      immediate_edit: True
      text: 'Zone 4'
      switch: zone_4_aux
      on_enter: 
        then:
          - switch.toggle: zone_4_aux

# --- Rotary Encoder and Button Integration ---
# Integrate the rotary encoder sensor
sensor:
  - platform: rotary_encoder
    id: my_encoder
    name: "Rotary Encoder"
    pin_a: GPIO4
    pin_b: GPIO5
    
    # Send actions to the display_menu component
    on_clockwise:
      - display_menu.up: sprnklr_menu
    on_anticlockwise:
      - display_menu.down: sprnklr_menu

  - platform: uptime
    name: Uptime
    entity_category: "diagnostic"
    
# Integrate the button on the rotary encoder
binary_sensor:
  - platform: gpio
    id: rotary_button
    pin:
      number: GPIO10
      mode: INPUT
    name: "Rotary Encoder Button"
    filters:
      - delayed_on: 10ms
      - delayed_off: 10ms
    on_press:
    - if:
        condition:
          display_menu.is_active: sprnklr_menu
        then:
          - display_menu.enter: sprnklr_menu
        else:
          - display_menu.show: sprnklr_menu

  - platform: status
    name: "Status"

  - platform: gpio
    name: "Boot Button"
    pin:
      number: 0
      ignore_strapping_warning: true
      mode:
        input: true
      inverted: true
    disabled_by_default: true
    on_press:
      then:
        - button.press: restart_button

sprinkler:
  - id: backyard_sprinklers
    main_switch:
      name: "Sprinklers Main"
      id: sprinkler_ctrlr_main_switch
    auto_advance_switch: "Auto Advance"
    valves:
      - valve_switch: "Backyard Zone 1"
        enable_switch: "Enable_Backyard Zone 1"
        run_duration_number: 
          id: zone_1_run_duration
          name: "Zone 1 Run Duration"
          icon: "mdi:timer-outline"
          initial_value: 1
          unit_of_measurement: min
        valve_switch_id: relay1
      - valve_switch: "Backyard Zone 2"    
        enable_switch: "Enable Backyard Zone 2"
        run_duration_number: 
          id: zone_2_run_duration
          name: "Zone 2 Run Duration"
          icon: "mdi:timer-outline"
          initial_value: 1
          unit_of_measurement: min
        valve_switch_id: relay2
      - valve_switch: "Backyard Zone 3"     
        enable_switch: "Enable Backyard Zone 3"
        run_duration_number: 
          id: zone_3_run_duration
          name: "Zone 3 Run Duration"
          icon: "mdi:timer-outline"
          initial_value: 1
          unit_of_measurement: min
        valve_switch_id: relay3
      - valve_switch: "Backyard Zone 4"
        enable_switch: "Enable Backyard Zone 4"
        run_duration_number: 
          id: zone_4_run_duration
          name: "Zone 4 Run Duration"
          icon: "mdi:timer-outline"
          initial_value: 1
          unit_of_measurement: min
        valve_switch_id: relay4

switch:
  - platform: gpio
    pin: GPIO1
    id: relay1
    restore_mode: ALWAYS_OFF
  - platform: gpio
    pin: GPIO2
    id: relay2
    restore_mode: ALWAYS_OFF
  - platform: gpio
    pin: GPIO41
    id: relay3
    restore_mode: ALWAYS_OFF
  - platform: gpio
    pin: GPIO42
    id: relay4
    restore_mode: ALWAYS_OFF
 # day of week toggle switches
  - platform: template
    id: dow_sunday
    name:  Sunday
    icon: "mdi:calendar-range"
    entity_category: config
    restore_mode: RESTORE_DEFAULT_ON
    optimistic: true
  - platform: template
    id: dow_monday
    name:  Monday
    icon: "mdi:calendar-range"
    entity_category: config
    restore_mode: RESTORE_DEFAULT_ON
    optimistic: true
  - platform: template
    id: dow_tuesday
    name:  Tuesday
    icon: "mdi:calendar-range"
    entity_category: config
    restore_mode: RESTORE_DEFAULT_ON
    optimistic: true
  - platform: template
    id: dow_wednesday
    name:  Wednesday
    icon: "mdi:calendar-range"
    entity_category: config
    restore_mode: RESTORE_DEFAULT_ON
    optimistic: true
  - platform: template
    id: dow_thursday
    name:  Thursday
    icon: "mdi:calendar-range"
    entity_category: config
    restore_mode: RESTORE_DEFAULT_ON
    optimistic: true
  - platform: template
    id: dow_friday
    name:  Friday
    icon: "mdi:calendar-range"
    entity_category: config
    restore_mode: RESTORE_DEFAULT_ON
    optimistic: true
  - platform: template
    id: dow_saturday
    name:  Saturday
    icon: "mdi:calendar-range"
    entity_category: config
    restore_mode: RESTORE_DEFAULT_ON
    optimistic: true
  # scheduled time enable switches
  - platform: template
    id: schedule1_enabled
    name: Enable Schedule 1
    icon: "mdi:clock-outline"
    entity_category: config
    restore_mode: RESTORE_DEFAULT_ON
    optimistic: true
  - platform: template
    id: schedule2_enabled
    name: Enable Schedule 2
    icon: "mdi:clock-outline"
    entity_category: config
    restore_mode: RESTORE_DEFAULT_ON
    optimistic: true
  - platform: template
    id: schedule3_enabled
    name: Enable Schedule 3
    icon: "mdi:clock-outline"
    entity_category: config
    restore_mode: RESTORE_DEFAULT_ON
    optimistic: true
  # rain delay switches (can be cancelled, auto-resetting)
  - platform: template
    id: raindelay_24h_enabled
    name: Enable 24h Rain Delay
    icon: "mdi:weather-cloudy-clock"
    restore_mode: RESTORE_DEFAULT_OFF
    optimistic: true
    turn_on_action:
      then:
        - delay: 
            hours: 24
        - switch.turn_off: raindelay_24h_enabled
  - platform: template
    id: raindelay_48h_enabled
    name: Enable 48h Rain Delay
    icon: "mdi:weather-cloudy-clock"
    restore_mode: RESTORE_DEFAULT_OFF
    optimistic: true
    turn_on_action:
      then:
        - delay: 
            hours: 48
        - switch.turn_off: raindelay_48h_enabled
  - platform: template
    id: main_switch
    optimistic: true
    name: "Sprinkler Main"
    on_turn_off:
      - switch.turn_off: sprinkler_ctrlr_main_switch
    on_turn_on:
      - switch.turn_on: sprinkler_ctrlr_main_switch               
  - platform: template
    id: zone_1_aux
    optimistic: True
    name: "Zone 1 Aux"
    on_turn_off: 
       then:
        - sprinkler.shutdown:
            id: backyard_sprinklers
    on_turn_on: 
      then:
        - sprinkler.start_single_valve:
            id: backyard_sprinklers
            valve_number: 0
  - platform: template
    id: zone_2_aux
    optimistic: True
    name: "Zone 2 Aux"
    on_turn_off: 
      then:
        - sprinkler.shutdown:
            id: backyard_sprinklers
    on_turn_on: 
      then:
        - sprinkler.start_single_valve:
            id: backyard_sprinklers
            valve_number: 1
  - platform: template
    id: zone_3_aux
    optimistic: True
    name: "Zone 3 Aux"
    on_turn_off:
      then: 
        - sprinkler.shutdown:
            id: backyard_sprinklers
    on_turn_on: 
      then:
        - sprinkler.start_single_valve:
            id: backyard_sprinklers
            valve_number: 2            
  - platform: template
    id: zone_4_aux
    optimistic: True
    name: "Zone 4 Aux"
    on_turn_off: 
      then:
        - sprinkler.shutdown:
            id: backyard_sprinklers
    on_turn_on: 
      then:
        - sprinkler.start_single_valve:
            id: backyard_sprinklers
            valve_number: 3

datetime:
  - platform: template
    name: Schedule 1 Start Time
    entity_category: config
    id: sched_s1t
    type: time
    optimistic: true
    restore_value: true
  - platform: template
    name: Schedule 2 Start Time
    entity_category: config
    id: sched_s2t
    type: time
    optimistic: true
    restore_value: true
  - platform: template
    name: Schedule 3 Start Time
    entity_category: config
    id: sched_s3t
    type: time
    optimistic: true
    restore_value: true

time:
  - platform: homeassistant
    id: ha_time
    on_time:
      # Every 1 minute
      - seconds: 0
        minutes: /1
        then:
          - script.execute: script1
    on_time_sync:
      then:
        - logger.log: "Synchronized system clock"

script:
    # check what day of the week it is currently, and if we have the schedule enabled today
    - id: script1 
      then:
        lambda: |-
          int dow = id(ha_time).now().day_of_week;
          if      ((dow == 1 && id(dow_sunday).state == true)
          ||       (dow == 2 && id(dow_monday).state == true)
          ||       (dow == 3 && id(dow_tuesday).state == true)
          ||       (dow == 4 && id(dow_wednesday).state == true)
          ||       (dow == 5 && id(dow_thursday).state == true)
          ||       (dow == 6 && id(dow_friday).state == true)
          ||       (dow == 7 && id(dow_saturday).state == true)) {
            id(script2).execute();
          }
    # if current time is equal to any of the scheduled start times, start the sprinkler cycle
    - id: script2
      then:
        lambda: |-
          int hour = id(ha_time).now().hour;
          int minute = id(ha_time).now().minute;
          if      ((hour == id(sched_s1t).hour && minute == id(sched_s1t).minute && id(schedule1_enabled).state == true && id(raindelay_24h_enabled).state == false && id(raindelay_48h_enabled).state == false)
          ||      (hour == id(sched_s2t).hour && minute == id(sched_s2t).minute && id(schedule2_enabled).state == true && id(raindelay_24h_enabled).state == false && id(raindelay_48h_enabled).state == false)
          ||      (hour == id(sched_s3t).hour && minute == id(sched_s3t).minute && id(schedule3_enabled).state == true && id(raindelay_24h_enabled).state == false && id(raindelay_48h_enabled).state == false)) {
            id(script3).execute();
          }
    # start the sprinkler cycle
    - id: script3
      then:
        - sprinkler.start_full_cycle: backyard_sprinklers
        - lambda: ESP_LOGI("main", "Sprinkler cycle has begun!");

button:
  - platform: restart
    name: "Restart"
    id: restart_button
    entity_category: config

  - platform: factory_reset
    name: "Factory Reset"
    id: reset
    entity_category: config

  - platform: safe_mode
    name: "Safe Mode"
    internal: false
    entity_category: config  

I decided to make a custom PCB, massively modified YAML and a colour screen. I’ve made 12 versions on the PCB, hopefully I’ll be happy with this one.

The LED strip changes colour with the valve status, the pump can be fixed or PWM output too.

Wow that looks really cool!

Any help on an exact working config? I have built it with the same board, but my relays are not firing.

Hello,

I plan on following the OP. since i am located in NL, i am not sure if i can find the same PCB of the relay board. can you give me measurements for the PCB so I can check? that way I can use the included 3D model to print a case.

Sorry for the stupid question but what is the benefit of that board? The way I understand how to do this for people who need 24VAC is how @roc1479 did it by using a 24VAC power supply and jumping the relays with that voltage. And then stepping it down and converting to some suitable voltage for the board itself (e.g. 5VDC), that should work the same on the original 8-relay board, no?

Even if this 4-relay board can be operated with mains voltage, I don't see a benefit here for controlling the valves? A transformer still is needed from mains -> 24VAC.