DIY 8 station irrigation based on ESPhome on KC868-A8 relay board

Hi,
Great work. I am looking for a solution where the controller can connect to Home Assistant via ie. ethernet. I see this board has ethernet connector. Could this be connected to Home Assistant with ethernet or the eth. connector is for other purposes?

I believe so yes. ESPhome supports a good number of Ethernet chips and the one on this board is the first one they mention in the ESPhome list.
I have used Ethernet with ESPhome fine on other boards, just not this one yet.

hello, I’m trying to build an automatic watering system based on slightly modified your code, unfortunately I keep getting the error “class esphome: :sprinkler : :Sprinkler:” has no member named “time_remaining”. Do you know what could be causing this?

My YAML

# Establish Substitutions
substitutions:
  device_name: sprinkler
  friendly_name: "Sprinkler"
  device_platform: ESP32
  device_board: esp32dev
  sensor_update_frequency: 1s
  sprinkler_name: esp32_sprinkler_ctrlr

esphome:
  name: sprinkler
  friendly_name: Sprinkler

esp32:
  board: esp32dev
  framework:
    type: arduino


# Enable logging
logger:
###################################################################################################
#####  I/O expander hub definition
###################################################################################################
mcp23017:
  - id: 'mcp23017_hub'
    address: 0x26
#nodemcu devkit v4.0
i2c:
  sda: 33
  scl: 32
  scan: True
  frequency: 400kHz

ethernet:
  type: LAN8720
  mdc_pin: GPIO23
  mdio_pin: GPIO18
  clk_mode: GPIO17_OUT
  phy_addr: 0
  power_pin: GPIO4
###################################################################################################
# 10x4 LCD config
display:
  - platform: lcd_pcf8574
    dimensions: 20x4
    address: 0x27
    update_interval: 1s
    lambda: |-
        switch (id(esp32_sprinkler_ctrlr).active_valve().value_or(-1)) {

          case -1:
          it.strftime(0, 0, "%a", id(homeassistant_time).now());
          it.strftime(10, 0, "%d-%m-%Y", id(homeassistant_time).now());
          it.strftime(12, 1,"%H:%M:%S", id(homeassistant_time).now());
          it.printf(0, 3, "Status: %s", id(esp32_sprinkler_ctrlr_status).state.c_str());
          
          if (id(esp32_sprinkler_ctrlr_status).state == "Paused") {
          it.printf(0, 2, "Zone %s", id(zone_active_sensor).state.c_str());
          it.printf(10, 2, "T: %s", id(zone_time_remaining_sensor).state.c_str());
          }

          break;
          
          default:
          it.printf(0, 0, "Zone %s:", id(zone_active_sensor).state.c_str());
          it.printf(8, 0, "%s", id(zone_active_sensor).state.c_str() ? "ON" : "OFF");
          it.printf(0, 1, "Time Letf: %s", id(zone_time_remaining_sensor).state.c_str());
          break;
        }

output:
  - platform: ledc
    pin: GPIO14
    id: sprinkler_backlight

light:
  - platform: monochromatic
    output: sprinkler_backlight
    name: "LCD Display Sprinkler Backlight"
    id: light_backlight
    restore_mode: ALWAYS_ON

###################################################################################################
###################################################################################################
# Enable Home Assistant APIs
api:
  reboot_timeout: 0s
  encryption:
    key: "FkNGBbOhmbWTgfvvZuVbM51UuFlb4EIb+dFi7whRA/g="
  services:
    - service: set_multiplier
      variables:
        multiplier: float
      then:
        - sprinkler.set_multiplier:
            id: esp32_sprinkler_ctrlr
            multiplier: !lambda 'return multiplier;'
    - service: start_full_cycle
      then:
        - sprinkler.start_full_cycle: esp32_sprinkler_ctrlr
    - service: start_single_valve
      variables:
        valve: int
      then:
        - sprinkler.start_single_valve:
            id: esp32_sprinkler_ctrlr
            valve_number: !lambda 'return valve;'
    - service: next_valve
      then:
        - sprinkler.next_valve: esp32_sprinkler_ctrlr
    - service: previous_valve
      then:
        - sprinkler.previous_valve: esp32_sprinkler_ctrlr
    - service: shutdown
      then:
        - sprinkler.shutdown: esp32_sprinkler_ctrlr
    - service: pause
      then:
        - sprinkler.pause: esp32_sprinkler_ctrlr
    - service: resume
      then:
        - sprinkler.resume: esp32_sprinkler_ctrlr
    - service: resume_or_full_cycle
      then:
        - sprinkler.resume_or_start_full_cycle: esp32_sprinkler_ctrlr
    - service: repeat_2
      then:
        - sprinkler.set_repeat:
            id: esp32_sprinkler_ctrlr
            repeat: 2  # would run three cycles
    - service: repeat_3
      then:
        - sprinkler.set_repeat:
            id: esp32_sprinkler_ctrlr
            repeat: 3  # would run three cycles
      
ota:
  password: "7001db87bfa399a3e551f7206250c3dd"

# Main sprinkler code
sprinkler:
  - id: esp32_sprinkler_ctrlr
    main_switch: "Master Run/Stop"
    auto_advance_switch: "Zones Auto Advance"
    reverse_switch: "Zones Reverse"
    valve_open_delay: 2s
    valves:
      - valve_switch: "Zone 1"
        enable_switch: "Zone 1 Enable"
        run_duration: 900s
        valve_switch_id: zone_valve_sw1
      - valve_switch: "Zone 2"
        enable_switch: "Zone 2 Enable"
        run_duration: 900s
        valve_switch_id: zone_valve_sw2
      - valve_switch: "Zone 3"
        enable_switch: "Zone 3 Enable"
        run_duration: 900s
        valve_switch_id: zone_valve_sw3
      - valve_switch: "Zone 4"
        enable_switch: "Zone 4 Enable"
        run_duration: 900s
        valve_switch_id: zone_valve_sw4
      - valve_switch: "Zone 5"
        enable_switch: "Zone 5 Enable"
        run_duration: 900s
        valve_switch_id: zone_valve_sw5
      - valve_switch: "Zone 6"
        enable_switch: "Zone 6 Enable"
        run_duration: 900s
        valve_switch_id: zone_valve_sw6

# Valve control outputs config via I/O expander       
switch:
#################################################################################################
####### CONTROL SWITCH
#################################################################################################
  - platform: template
    id: esp32_sprinkler_ctrlr_run
    name: "Sprinkler Controller Run"
    optimistic: true
    on_turn_on:
      - text_sensor.template.publish:
          id: esp32_sprinkler_ctrlr_status
          state: "Running"
      - sprinkler.resume_or_start_full_cycle: esp32_sprinkler_ctrlr
      - switch.turn_off: esp32_sprinkler_ctrlr_pause
      - switch.turn_off: esp32_sprinkler_ctrlr_stop
  - platform: template
    id: esp32_sprinkler_ctrlr_stop
    name: "Sprinkler Controller Stop"
    optimistic: true
    on_turn_on:
      - text_sensor.template.publish:
          id: esp32_sprinkler_ctrlr_status
          state: "Stopped"
      - sprinkler.shutdown: esp32_sprinkler_ctrlr
      - switch.turn_off: esp32_sprinkler_ctrlr_pause
      - switch.turn_off: esp32_sprinkler_ctrlr_run
  - platform: template
    id: esp32_sprinkler_ctrlr_pause
    name: "Sprinkler Controller Pause"
    optimistic: true
    
    turn_on_action:
      - delay: 500ms
      - lambda: |-
          if(id(esp32_sprinkler_ctrlr_status).state != "Running")
          {
            id(esp32_sprinkler_ctrlr_pause).turn_off();
          }
      - lambda: |-
          if(id(esp32_sprinkler_ctrlr_status).state == "Running")
          {
            id(esp32_sprinkler_ctrlr_status).publish_state("Paused");
            id(esp32_sprinkler_ctrlr).pause();
          }
    on_turn_off:
      lambda: |-
        if(id(esp32_sprinkler_ctrlr_status).state == "Paused")
        {
          id(esp32_sprinkler_ctrlr_status).publish_state("Running");
          id(esp32_sprinkler_ctrlr).resume();
        } 
  - platform: template
    id: esp32_sprinkler_ctrlr_resume
    name: "Sprinkler Controller Resume"
    optimistic: true
     
#################################################################################################
####### I/0  SWITCH
#################################################################################################
  - platform: gpio
    id: zone_valve_sw1
    name: "MCP23017 Pin B2"
    pin:
      mcp23xxx: mcp23017_hub
      # Use pin B2
      number: 10
      # One of INPUT or OUTPUT
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw2
    name: "MCP23017 Pin B3"
    pin:
      mcp23xxx: mcp23017_hub
      # Use pin B3
      number: 11
      # One of INPUT or OUTPUT
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw3
    name: "MCP23017 Pin B4"
    pin:
      mcp23xxx: mcp23017_hub
      # Use pin B4
      number: 12
      # One of INPUT or OUTPUT
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw4
    name: "MCP23017 Pin B5"
    pin:
      mcp23xxx: mcp23017_hub
      # Use pin B5
      number: 13
      # One of INPUT or OUTPUT
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw5
    name: "MCP23017 Pin B6"
    pin:
      mcp23xxx: mcp23017_hub
      # Use pin B6
      number: 14
      # One of INPUT or OUTPUT
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw6
    name: "MCP23017 Pin B7"
    pin:
      mcp23xxx: mcp23017_hub
      # Use pin B7
      number: 15
      # One of INPUT or OUTPUT
      mode:
        output: true
      inverted: true
    internal: true
  #- platform: template #this switch doesn't work properly. Can pause via HA frontend, but will not resume...  Via Services does work...
  #  id: pause_switch
  #  name: "Pause Irrigation Switch"
  #  turn_on_action:
  #    then:
  #      - sprinkler.pause: esp32_sprinkler_ctrlr
  #  turn_off_action:
  #    then:
  #      - sprinkler.resume: esp32_sprinkler_ctrlr    

number:
# Configuration to set multiplier via number
  - platform: template
    id: sprinkler_ctrlr_multiplier
    name: "Run Duration Multiplier"
    min_value: 1.0
    max_value: 3.0
    step: 0.1
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).multiplier();"
    set_action:
      - sprinkler.set_multiplier:
          id: esp32_sprinkler_ctrlr
          multiplier: !lambda 'return x;'  
          
# Configure repeat
  - platform: template
    id: sprinkler_ctrlr_repeat_cycles
    name: "Sprinkler Repeat Cycles"
    min_value: 0
    max_value: 300
    step: 1
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).repeat();"
    set_action:
      - sprinkler.set_repeat:
          id: esp32_sprinkler_ctrlr
          repeat: !lambda 'return x;'

# Configuration to set valve run duration via number
  - platform: template
    id: sprinkler_valve_1_duration
    name: "Zone 1 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    update_interval: $sensor_update_frequency
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(0) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 0
          run_duration: !lambda "return x * 60;"
          
  - platform: template
    id: sprinkler_valve_2_duration
    name: "Zone 2 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    update_interval: $sensor_update_frequency
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(1) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 1
          run_duration: !lambda "return x * 60;" 

  - platform: template
    id: sprinkler_valve_3_duration
    name: "Zone 3 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    update_interval: $sensor_update_frequency
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(2) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 2
          run_duration: !lambda "return x * 60;" 
          
  - platform: template
    id: sprinkler_valve_4_duration
    name: "Zone 4 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    update_interval: $sensor_update_frequency
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(3) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 3
          run_duration: !lambda "return x * 60;"  
          
  - platform: template
    id: sprinkler_valve_5_duration
    name: "Zone 5 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    update_interval: $sensor_update_frequency
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(4) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 4
          run_duration: !lambda "return x * 60;"
          
  - platform: template
    id: sprinkler_valve_6_duration
    name: "Zone 6 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    update_interval: $sensor_update_frequency
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(5) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 5
          run_duration: !lambda "return x * 60;" 
          
sensor:
### SENSORS
  - platform: template
    name: "Cycle Total Time Sensor"
    icon: mdi:progress-clock    
    unit_of_measurement: 'Min'
    accuracy_decimals: 0
    update_interval: $sensor_update_frequency  
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(0)/60 + id(esp32_sprinkler_ctrlr).valve_run_duration(1)/60;"
    #lambda: |-
    #  {% set ns = namespace(states=[]) %}
    #  {% for s in states.sensor %}
    #    {% if s.object_id.startswith('valve_run_') and s.object_id.endswith('_duration') %}
    #      {% set ns.states = ns.states + [ s.state | float ] %}
    #    {% endif %}
    #  {% endfor %}
    #  {{ ns.states | sum | round(2) }}
#  - platform: template
#    name: "Zone Active Sensor"
#    id: zone_active_sensor
#    unit_of_measurement: ''
#    accuracy_decimals: 0
#    #Valves are numbered from 0-7 internally which is an issue when displaying !
#    lambda: |-
#      if(id(esp32_sprinkler_ctrlr_status).state == "Stopped")
#      {
#        return id(esp32_sprinkler_ctrlr).active_valve().value_or(NAN);
#      }
#      else
#      {
#        return id(esp32_sprinkler_ctrlr).active_valve().value_or(NAN) + 1;
#      }
#    update_interval: $sensor_update_frequency
  - platform: homeassistant
    id: homeassistant_sprinklercountdown
    entity_id: timer.sprinklercountdown
    internal: false

text_sensor:
    - platform: template
      id: esp32_sprinkler_ctrlr_status
      name: "Sprinklers Status"
      update_interval: $sensor_update_frequency
    - platform: template
      name: "Zone Active Sensor"
      id: zone_active_sensor
      #unit_of_measurement: ''
      #accuracy_decimals: 0
      #Valves are numbered from 0-7 internally which is an issue when displaying !
      lambda: |-
        if(id(esp32_sprinkler_ctrlr_status).state == "Stopped")
        {
          int zone_active = id(esp32_sprinkler_ctrlr).active_valve().value_or(NAN);
          std::string zone_active_as_string = esphome::to_string(zone_active);
          return zone_active_as_string;
        }
        else
        {
          int zone_active = id(esp32_sprinkler_ctrlr).active_valve().value_or(NAN) + 1;
          std::string zone_active_as_string = esphome::to_string(zone_active);
          return zone_active_as_string;
        }
      update_interval: $sensor_update_frequency

#  # Expose Valve Progress Percent as a sensor.
    - platform: template
      id: progress_percent_valve
      name: "Zone Progress"
      #unit_of_measurement: '%'
      #accuracy_decimals: 0
      update_interval: $sensor_update_frequency
      icon: "mdi:progress-clock"
      lambda: |-
        int valve_progress_percent = round(((id(esp32_sprinkler_ctrlr).valve_run_duration_adjusted(id(esp32_sprinkler_ctrlr).active_valve().value_or(0)) - id(esp32_sprinkler_ctrlr).time_remaining().value_or(0)) * 100 / id(esp32_sprinkler_ctrlr).valve_run_duration_adjusted(id(esp32_sprinkler_ctrlr).active_valve().value_or(0))));
        std::string valve_progress_percent_as_string = esphome::to_string(valve_progress_percent);
        return valve_progress_percent_as_string;

# # Expose Zone Time Remaining
    - platform: template
      name: "Zone Time Remaining Sensor"
      id: zone_time_remaining_sensor
      icon: mdi:progress-clock    
      #unit_of_measurement: 'Min'
      #accuracy_decimals: 0
      update_interval: $sensor_update_frequency
      lambda: |-
        if(id(esp32_sprinkler_ctrlr_status).state != "Paused")
        {
          int seconds = round(id(esp32_sprinkler_ctrlr).time_remaining().value_or(0));
          int days = seconds / (24 * 3600);
          seconds = seconds % (24 * 3600);
          int hours = seconds / 3600;
          seconds = seconds % 3600;
          int minutes = seconds /  60;
          seconds = seconds % 60;
          return {
            ((days ? String(days) + "d " : "") +
            (hours ? String(hours) + "h " : "") +
            (minutes ? String(minutes) + "m " : "") +
            (String(seconds) + "s")
            ).c_str()};
        }
        else
        {
          return {};
        }

### Input to managet the display backlight
    - platform: homeassistant    
      id: display_sprinkler_backlight
      entity_id: input_number.sprinklerbacklightlevel
      internal: true
      on_value:
        then:
          - output.turn_on: sprinkler_backlight
          - output.set_level:
              id: sprinkler_backlight
              level: !lambda |-
                return atoi(id(display_sprinkler_backlight).state.c_str()) / 100.0;

# Time source config
time:
  - platform: homeassistant
    id: homeassistant_time
    timezone: Europe/Rome
    ```

Anyone can share an automated schedule?
Do you do the automation built in the esphome yaml or do you build it via HA automations? I want it to be on the esphome so if the network is down it wont fail
Thanks

I use HA automations to trigger the schedules via a HA Calender. My understanding is the zone times are stored local to the controller in ESPH, I can trigger a schedule locally via a button if the network is down (or not). Not sure if schedule timing works after the network is down. Mostly the issue here would be power failure on the mains, and that means the controller power is down also.

I use a template sensor to measure the daily mean temperature as a condition to allow trigger of a second schedule that can occur inbetween the main schedule days.

image

Did you ever fixed the time.remaining issue? I am getting the same error and prevents me from running this.

The Sprinkler component was changed from time_remaining() to time_remaining_active_valve() to return the value of the zone remaining time.

Update all the instances of time_remaining in the yaml and it will compile, I think there are 9.

This is my current code. Compiled under ESPhome 2024.12.4

# 8 Zone Version using KC868-A8 ESP32 relay board update 2-2-2025

esphome:
  name: irrigation-controller

esp32:
  board: nodemcu-32s
  framework:
    type: arduino

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  output_power: 8.5db
  fast_connect: False
  manual_ip:
# Set this to the IP of the ESP
    static_ip: !secret static_ip
# Set this to the IP address of the router. Often ends with .1
    gateway: !secret gateway_ip
# The subnet of the network. 255.255.255.0 works for most home networks.
    subnet: 255.255.255.0  
#  use_address: irrigation-controller.local
  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Esp32-Irrigation"
    password: !secret ap_password

# Enable logging
logger:

# Enable Home Assistant API
api:
  reboot_timeout: 3600s

  services:  #services for device 'irrigation controller' in Home Assistant
    - service: start_full_cycle
      then:
        - sprinkler.start_full_cycle: esp32_sprinkler_ctrlr

    - service: pause
      then:
        - sprinkler.pause: esp32_sprinkler_ctrlr  
    - service: resume
      then:
        - sprinkler.resume: esp32_sprinkler_ctrlr 

    - service: start_single_valve
      variables:
        valve: int
      then:
        - sprinkler.start_single_valve:
            id: esp32_sprinkler_ctrlr
            valve_number: !lambda 'return valve;'

    - service: next_valve
      then:
        - sprinkler.next_valve: esp32_sprinkler_ctrlr

    - service: previous_valve
      then:
        - sprinkler.previous_valve: esp32_sprinkler_ctrlr

    - service: shutdown
      then:
        - sprinkler.shutdown: esp32_sprinkler_ctrlr

    - service: resume_or_full_cycle
      then:
        - sprinkler.resume_or_start_full_cycle: esp32_sprinkler_ctrlr

ota:
  - platform: esphome
  
# I2C bus config
i2c:
  sda: 4
  scl: 5
  scan: true
  id: bus_a
  frequency: 800kHz

# I2C I/O expander config
pcf8574:
  - id: 'pcf8574_output_hub'
    address: 0x24
    pcf8575: false
  - id: 'pcf8574_input_hub'
    address: 0x22
    
# 16x2 LCD config
display:
  - platform: lcd_pcf8574
    dimensions: 16x2
    address: 0x27
    update_interval: 1s
    lambda: |-

        switch (id(esp32_sprinkler_ctrlr).active_valve().value_or(-1)) {

          case 0: //valve0, zone1, internal valve numbers not zone_valve_swX numbers
          it.printf(0, 0, "Front Lawn: %s", id(zone_valve_sw1).state ? "ON" : "OFF");
          if (id(zone_valve_sw1).state) {
          it.printf(0, 1, "Mins: %2d of", id(esp32_sprinkler_ctrlr).time_remaining_active_valve().value_or(0) / 60);
          it.printf(12, 1, "%2d", id(esp32_sprinkler_ctrlr).valve_run_duration_adjusted(0) / 60);
          } else {
          it.printf(0, 1, "Mins Set %2d     ", id(esp32_sprinkler_ctrlr).valve_run_duration_adjusted(0) / 60);
          }
          break;
          
          case 1: //valve1, zone2, 
          it.printf(0, 0, "Back Lawn: %s", id(zone_valve_sw2).state ? "ON" : "OFF");
          if (id(zone_valve_sw2).state) {
          it.printf(0, 1, "Mins: %2d of", id(esp32_sprinkler_ctrlr).time_remaining_active_valve().value_or(0) / 60);
          it.printf(12, 1, "%2d", id(esp32_sprinkler_ctrlr).valve_run_duration_adjusted(1) / 60);
          } else {
          it.printf(0, 1, "Mins Set %2d     ", id(esp32_sprinkler_ctrlr).valve_run_duration_adjusted(1) / 60);
          }
          break;

          case 2:  //valve2, zone3,
          it.printf(0, 0, "Front middle: %s", id(zone_valve_sw3).state ? "ON" : "OFF");
          if (id(zone_valve_sw3).state) {
          it.printf(0, 1, "Mins: %2d of", id(esp32_sprinkler_ctrlr).time_remaining_active_valve().value_or(0) / 60);
          it.printf(12, 1, "%2d", id(esp32_sprinkler_ctrlr).valve_run_duration_adjusted(2) / 60);
          } else {
          it.printf(0, 1, "Mins Set %2d     ", id(esp32_sprinkler_ctrlr).valve_run_duration_adjusted(2) / 60);
          }
          break;
          
          case 3:  //valve3, zone4
          it.printf(0, 0, "West Fence: %s", id(zone_valve_sw4).state ? "ON" : "OFF");
          if (id(zone_valve_sw4).state) {
          it.printf(0, 1, "Mins: %2d of", id(esp32_sprinkler_ctrlr).time_remaining_active_valve().value_or(0) / 60);
          it.printf(12, 1, "%2d", id(esp32_sprinkler_ctrlr).valve_run_duration_adjusted(3) / 60);
          } else {
          it.printf(0, 1, "Mins Set %2d     ", id(esp32_sprinkler_ctrlr).valve_run_duration_adjusted(3) / 60);
          }
          break;

          case 4: //valve4, zone5
          it.printf(0, 0, "Front South: %s", id(zone_valve_sw5).state ? "ON" : "OFF");
          if (id(zone_valve_sw5).state) {
          it.printf(0, 1, "Mins: %2d of", id(esp32_sprinkler_ctrlr).time_remaining_active_valve().value_or(0) / 60);
          it.printf(12, 1, "%2d", id(esp32_sprinkler_ctrlr).valve_run_duration_adjusted(4) / 60);
          } else {
          it.printf(0, 1, "Mins Set %2d     ", id(esp32_sprinkler_ctrlr).valve_run_duration_adjusted(4) / 60);
          }
          break;

          case 5:  //valve5, zone6
          it.printf(0, 0, "Back Wall W: %s", id(zone_valve_sw6).state ? "ON" : "OFF");
          if (id(zone_valve_sw6).state) {
          it.printf(0, 1, "Mins: %2d of", id(esp32_sprinkler_ctrlr).time_remaining_active_valve().value_or(0) / 60);
          it.printf(12, 1, "%2d", id(esp32_sprinkler_ctrlr).valve_run_duration_adjusted(5) / 60);
          } else {
          it.printf(0, 1, "Mins Set %2d     ", id(esp32_sprinkler_ctrlr).valve_run_duration_adjusted(5) / 60);
          }
          break;

          case 6: //valve6, zone7
          it.printf(0, 0, "Back Wall E: %s", id(zone_valve_sw7).state ? "ON" : "OFF");
          if (id(zone_valve_sw7).state) {
          it.printf(0, 1, "Mins: %2d of", id(esp32_sprinkler_ctrlr).time_remaining_active_valve().value_or(0) / 60);
          it.printf(12, 1, "%2d", id(esp32_sprinkler_ctrlr).valve_run_duration_adjusted(6) / 60);
          } else {
          it.printf(0, 1, "Mins Set %2d     ", id(esp32_sprinkler_ctrlr).valve_run_duration_adjusted(6) / 60);
          }
          break;          

          case 7:  //valve7, zone8
          it.printf(0, 0, "Verandas: %s", id(zone_valve_sw8).state ? "ON" : "OFF");
          if (id(zone_valve_sw8).state) {
          it.printf(0, 1, "Mins: %2d of", id(esp32_sprinkler_ctrlr).time_remaining_active_valve().value_or(0) / 60);
          it.printf(12, 1, "%2d", id(esp32_sprinkler_ctrlr).valve_run_duration_adjusted(7) / 60);
          } else {
          it.printf(0, 1, "Mins Set %2d     ", id(esp32_sprinkler_ctrlr).valve_run_duration_adjusted(7) / 60);
          }
          break;

          case -1:
          if ((id(esp32_sprinkler_ctrlr).paused_valve().value_or(-1) +1) == 0) {  //first valve is 0 so assign -1 if no valve paused
          it.printf(0, 0, "Watering OFF");
          it.strftime(0, 1,"Time is %H:%M:%S", id(homeassistant_time).now()); 
          } else {
          it.printf(0, 0, "Watering PAUSED");
          it.printf(0, 1, "Zone %2d", id(esp32_sprinkler_ctrlr).paused_valve().value_or(0) +1);  
          }

          break;

          default:
          break;         
        }
      
##        static int current_page_num = 1;
##        int number_of_pages = 4;
#        current_page_num += 1;
#        if (current_page_num > number_of_pages) {
#          current_page_num = 1;
#        }


# Main sprinkler code
sprinkler:
  - id: esp32_sprinkler_ctrlr
    main_switch: "Master Run/Stop"
    auto_advance_switch: "Zones Auto Advance"
#    next_prev_ignore_disabled: true
#    reverse_switch: "Zones Reverse"
    valve_overlap: 1s
    valves:
      - valve_switch: "Zone 1"
        enable_switch: "Zone 1 Enable"
        run_duration: 600s
        valve_switch_id: zone_valve_sw1
      - valve_switch: "Zone 2"
        enable_switch: "Zone 2 Enable"
        run_duration: 300s
        valve_switch_id: zone_valve_sw2
      - valve_switch: "Zone 3"
        enable_switch: "Zone 3 Enable"
        run_duration: 600s
        valve_switch_id: zone_valve_sw3
      - valve_switch: "Zone 4"
        enable_switch: "Zone 4 Enable"
        run_duration: 300s
        valve_switch_id: zone_valve_sw4
      - valve_switch: "Zone 5"
        enable_switch: "Zone 5 Enable"
        run_duration: 300s
        valve_switch_id: zone_valve_sw5
      - valve_switch: "Zone 6"
        enable_switch: "Zone 6 Enable"
        run_duration: 300s
        valve_switch_id: zone_valve_sw6
      - valve_switch: "Zone 7"
        enable_switch: "Zone 7 Enable"
        run_duration: 600s
        valve_switch_id: zone_valve_sw7
      - valve_switch: "Zone 8"
        enable_switch: "Zone 8 Enable"
        run_duration: 300s
        valve_switch_id: zone_valve_sw8

# Valve control outputs config via I/O expander       
switch:
  - platform: gpio
    id: zone_valve_sw1
    pin:
      pcf8574: pcf8574_output_hub
      # Use pin number 0
      number: 0
      # One of INPUT or OUTPUT
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw2
    pin:
      pcf8574: pcf8574_output_hub
      # Use pin number 1
      number: 1
      # One of INPUT or OUTPUT
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw3
    pin:
      pcf8574: pcf8574_output_hub
      # Use pin number 2
      number: 2
      # One of INPUT or OUTPUT
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw4
    pin:
      pcf8574: pcf8574_output_hub
      # Use pin number 3
      number: 3
      # One of INPUT or OUTPUT
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw5
    pin:
      pcf8574: pcf8574_output_hub
      # Use pin number 4
      number: 4
      # One of INPUT or OUTPUT
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw6
    pin:
      pcf8574: pcf8574_output_hub
      # Use pin number 5
      number: 5
      # One of INPUT or OUTPUT
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw7
    pin:
      pcf8574: pcf8574_output_hub
      # Use pin number 6
      number: 6
      # One of INPUT or OUTPUT
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw8
    pin:
      pcf8574: pcf8574_output_hub
      # Use pin number 7
      number: 7
      # One of INPUT or OUTPUT
      mode:
        output: true
      inverted: true
    internal: true

  - platform: template #this switch doesn't work properly. Can pause via HA frontend, but will not resume...  Via Services does work...
    id: pause_switch
    name: "Pause Irrigation Switch"
    turn_on_action:
      then:
        - sprinkler.pause: esp32_sprinkler_ctrlr
    turn_off_action:
      then:
        - sprinkler.resume: esp32_sprinkler_ctrlr    



# Configuration to set multiplier via number
number:
  - platform: template
    id: esp32_ctrlr_multiplier
    name: "Run Duration Multiplier"
    min_value: 1.0
    max_value: 3.0
    step: 0.1
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).multiplier();"
    set_action:
      - sprinkler.set_multiplier:
          id: esp32_sprinkler_ctrlr
          multiplier: !lambda 'return x;'  
          
# Configuration to set valve run duration via number
  - platform: template
    id: sprinkler_valve_1_duration
    name: "Zone 1 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
#    update_interval: 10s
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(0) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 0
          run_duration: !lambda "return x * 60;"
          
  - platform: template
    id: sprinkler_valve_2_duration
    name: "Zone 2 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(1) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 1
          run_duration: !lambda "return x * 60;" 

  - platform: template
    id: sprinkler_valve_3_duration
    name: "Zone 3 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(2) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 2
          run_duration: !lambda "return x * 60;" 
          
  - platform: template
    id: sprinkler_valve_4_duration
    name: "Zone 4 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(3) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 3
          run_duration: !lambda "return x * 60;"  
          
  - platform: template
    id: sprinkler_valve_5_duration
    name: "Zone 5 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(4) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 4
          run_duration: !lambda "return x * 60;"
          
  - platform: template
    id: sprinkler_valve_6_duration
    name: "Zone 6 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(5) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 5
          run_duration: !lambda "return x * 60;" 
          
  - platform: template
    id: sprinkler_valve_7_duration
    name: "Zone 7 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(6) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 6
          run_duration: !lambda "return x * 60;" 
          
  - platform: template
    id: sprinkler_valve_8_duration
    name: "Zone 8 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(7) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 7
          run_duration: !lambda "return x * 60;"   
          
#Rain Sensor from my HA
sensor:
  - platform: homeassistant
    id: rain_today
    entity_id: sensor.daily_rain_rate
    
  - platform: wifi_signal
    name: "ESP Irrigation WiFi Signal Sensor"
    update_interval: 300s  
    
  - platform: template
    name: "Zone Time Remaining Sensor"
    icon: mdi:progress-clock    
    unit_of_measurement: 'Min'
    lambda: "return id(esp32_sprinkler_ctrlr).time_remaining_active_valve().value_or(0) / 60;"
    update_interval: 30s   


binary_sensor:
  - platform: gpio
    id: button_1 #push button on front of controller enclosure
    pin:
      pcf8574: pcf8574_input_hub
      # Use pin number 4 (starting at 0)
      number: 4
      # One of INPUT or OUTPUT
      mode:
        input: true
      inverted: true
    internal: true
    on_click:
    - min_length: 50ms
      max_length: 350ms
      then:
        - sprinkler.resume_or_start_full_cycle: esp32_sprinkler_ctrlr

  - platform: gpio
    id: button_2 #push button on front of controller enclosure
    pin:
      pcf8574: pcf8574_input_hub
      # Use pin number 5 (starting at 0)
      number: 5
      # One of INPUT or OUTPUT
      mode:
        input: true
      inverted: true
    internal: true
    on_click:
    - min_length: 50ms
      max_length: 350ms
      then:
        - sprinkler.next_valve: esp32_sprinkler_ctrlr

  - platform: gpio
    id: button_3 #push button on front of controller enclosure
    pin:
      pcf8574: pcf8574_input_hub
      # Use pin number 6 (starting at 0)
      number: 6
      # One of INPUT or OUTPUT
      mode:
        input: true
      inverted: true
    internal: true
    on_click:
    - min_length: 50ms
      max_length: 350ms
      then:
        - sprinkler.pause: esp32_sprinkler_ctrlr

  - platform: gpio
    id: button_4 #push button on front of controller enclosure
    pin:
      pcf8574: pcf8574_input_hub
      # Use pin number 7 (starting at 0)
      number: 7
      # One of INPUT or OUTPUT
      mode:
        input: true
      inverted: true
    internal: true
    on_click:
    - min_length: 50ms
      max_length: 350ms
      then:
        - sprinkler.shutdown: esp32_sprinkler_ctrlr

#  - platform: template
#    name: "Zone Active Sensor"
##    unit_of_measurement: ''
#    lambda: "return id(esp32_sprinkler_ctrlr).active_valve().value_or(NAN);" #Valves are numbered from 0-7 internally which is an issue when displaying !
#    update_interval: 30s


# Time source config
time:
  - platform: homeassistant
    id: homeassistant_time
    timezone: Australia/Adelaide
    #timezone: CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00  

I’ve had some WiFi connecting issues for a few months, shown up using a Ping monitor in HA. Trying to resolve them currently, although the irrigation schedules seem to be running as normal.
One thing that has helped is increasing the I2C clock frequency (800kHz) and also increasing the display update time to 2s. It could well be that I have a board issues though, like 3.3V regulator etc. I don’t think I had any connection issues in the past in the logs so that’s why I think it could be the board rather than coding…

Hello, can you share yaml for this card and automation?

Just to give back, I have expanded the original version:
Schedule build in so the unit can run without HA (Daily morning / evening)
I use the KinCony KC868-A16 so added 8 more channels
Make the rain threshold show in dashboard
Info now on a 4x20 LCD display

# 16 Zone Version using KC868-A16 ESP32 relay board with Auto Schedule
# Updated 2-3-2025

esphome:
  name: irrigation-controller

# Global variables for scheduling
globals:
  - id: morning_schedule_enabled
    type: bool
    restore_value: true
    initial_value: 'false'
  - id: evening_schedule_enabled
    type: bool
    restore_value: true
    initial_value: 'false'
  - id: morning_hour
    type: int
    restore_value: true
    initial_value: '6'
  - id: morning_minute
    type: int
    restore_value: true
    initial_value: '0'
  - id: evening_hour
    type: int
    restore_value: true
    initial_value: '18'
  - id: evening_minute
    type: int
    restore_value: true
    initial_value: '0'
  - id: last_run_day
    type: int
    restore_value: true
    initial_value: '-1'
  - id: rain_threshold_mm
    type: float
    restore_value: true
    initial_value: '5.0'

esp32:
  board: nodemcu-32s
  framework:
    type: arduino

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  output_power: 8.5db
  fast_connect: False
  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Esp32-Irrigation"
    password: !secret ap_password

# Enable logging
logger:

# Enable Home Assistant API
api:
  reboot_timeout: 3600s

  services:  #services for device 'irrigation controller' in Home Assistant
    - service: start_full_cycle
      then:
        - sprinkler.start_full_cycle: esp32_sprinkler_ctrlr

    - service: pause
      then:
        - sprinkler.pause: esp32_sprinkler_ctrlr  
    - service: resume
      then:
        - sprinkler.resume: esp32_sprinkler_ctrlr 

    - service: start_single_valve
      variables:
        valve: int
      then:
        - sprinkler.start_single_valve:
            id: esp32_sprinkler_ctrlr
            valve_number: !lambda 'return valve;'

    - service: next_valve
      then:
        - sprinkler.next_valve: esp32_sprinkler_ctrlr

    - service: previous_valve
      then:
        - sprinkler.previous_valve: esp32_sprinkler_ctrlr

    - service: shutdown
      then:
        - sprinkler.shutdown: esp32_sprinkler_ctrlr

    - service: resume_or_full_cycle
      then:
        - sprinkler.resume_or_start_full_cycle: esp32_sprinkler_ctrlr

ota:
  - platform: esphome
  
# I2C bus config
i2c:
  sda: 4
  scl: 5
  scan: true
  id: bus_a
  frequency: 800kHz

# I2C I/O expander config for 16 zones (using 2 PCF8574 chips)
pcf8574:
  - id: 'pcf8574_output_hub1'
    address: 0x24
    pcf8575: false
  - id: 'pcf8574_output_hub2'
    address: 0x25
    pcf8575: false
  - id: 'pcf8574_input_hub'
    address: 0x22
    
# 20x4 LCD config for more zones display
display:
  - platform: lcd_pcf8574
    dimensions: 20x4
    address: 0x27
    update_interval: 1s
    lambda: |-
        auto active_valve = id(esp32_sprinkler_ctrlr).active_valve();
        if (active_valve.has_value()) {
          int zone_num = active_valve.value();
          const char* zone_names[] = {
            "Front Lawn", "Back Lawn", "Front Mid", "West Fence",
            "Front South", "Back Wall W", "Back Wall E", "Verandas",
            "Side North", "Side South", "Garden Bed 1", "Garden Bed 2",
            "Drip Line 1", "Drip Line 2", "Greenhouse", "Pool Area"
          };
          
          // Get valve state based on zone number
          bool valve_state = false;
          switch (zone_num) {
            case 0: valve_state = id(zone_valve_sw1).state; break;
            case 1: valve_state = id(zone_valve_sw2).state; break;
            case 2: valve_state = id(zone_valve_sw3).state; break;
            case 3: valve_state = id(zone_valve_sw4).state; break;
            case 4: valve_state = id(zone_valve_sw5).state; break;
            case 5: valve_state = id(zone_valve_sw6).state; break;
            case 6: valve_state = id(zone_valve_sw7).state; break;
            case 7: valve_state = id(zone_valve_sw8).state; break;
            case 8: valve_state = id(zone_valve_sw9).state; break;
            case 9: valve_state = id(zone_valve_sw10).state; break;
            case 10: valve_state = id(zone_valve_sw11).state; break;
            case 11: valve_state = id(zone_valve_sw12).state; break;
            case 12: valve_state = id(zone_valve_sw13).state; break;
            case 13: valve_state = id(zone_valve_sw14).state; break;
            case 14: valve_state = id(zone_valve_sw15).state; break;
            case 15: valve_state = id(zone_valve_sw16).state; break;
            default: valve_state = false; break;
          }
          
          if (zone_num < 16) {
            it.printf(0, 0, "Zone %2d: %s", zone_num + 1, zone_names[zone_num]);
            it.printf(0, 1, "Status: %s", valve_state ? "ON" : "OFF");
            
            if (valve_state) {
              it.printf(0, 2, "Time: %2d of %2d min", 
                id(esp32_sprinkler_ctrlr).time_remaining_active_valve().value_or(0) / 60,
                id(esp32_sprinkler_ctrlr).valve_run_duration_adjusted(zone_num) / 60);
            } else {
              it.printf(0, 2, "Duration: %2d min", 
                id(esp32_sprinkler_ctrlr).valve_run_duration_adjusted(zone_num) / 60);
            }
            
            // Show schedule status on line 4
            if (id(morning_schedule_enabled) || id(evening_schedule_enabled)) {
              it.printf(0, 3, "Auto: %02d:%02d %02d:%02d", 
                id(morning_hour), id(morning_minute),
                id(evening_hour), id(evening_minute));
            } else {
              it.printf(0, 3, "Auto Schedule: OFF");
            }
          }
        } else {
          auto paused_valve = id(esp32_sprinkler_ctrlr).paused_valve();
          if (!paused_valve.has_value()) {
            it.printf(0, 0, "Irrigation System");
            it.printf(0, 1, "Status: OFF");
            it.strftime(0, 2, "Time: %H:%M:%S", id(homeassistant_time).now());
            it.printf(0, 3, "Rain Limit: %.1fmm", id(rain_threshold_mm));
          } else {
            it.printf(0, 0, "System PAUSED");
            it.printf(0, 1, "Zone: %2d", paused_valve.value() + 1);
            it.strftime(0, 2, "Time: %H:%M:%S", id(homeassistant_time).now());
            it.printf(0, 3, "Press Resume");
          }
        }

# Main sprinkler code for 16 zones
sprinkler:
  - id: esp32_sprinkler_ctrlr
    main_switch: "Master Run/Stop"
    auto_advance_switch: "Zones Auto Advance"
    valve_overlap: 1s
    valves:
      - valve_switch: "Zone 1"
        enable_switch: "Zone 1 Enable"
        run_duration: 600s
        valve_switch_id: zone_valve_sw1
      - valve_switch: "Zone 2"
        enable_switch: "Zone 2 Enable"
        run_duration: 300s
        valve_switch_id: zone_valve_sw2
      - valve_switch: "Zone 3"
        enable_switch: "Zone 3 Enable"
        run_duration: 600s
        valve_switch_id: zone_valve_sw3
      - valve_switch: "Zone 4"
        enable_switch: "Zone 4 Enable"
        run_duration: 300s
        valve_switch_id: zone_valve_sw4
      - valve_switch: "Zone 5"
        enable_switch: "Zone 5 Enable"
        run_duration: 300s
        valve_switch_id: zone_valve_sw5
      - valve_switch: "Zone 6"
        enable_switch: "Zone 6 Enable"
        run_duration: 300s
        valve_switch_id: zone_valve_sw6
      - valve_switch: "Zone 7"
        enable_switch: "Zone 7 Enable"
        run_duration: 600s
        valve_switch_id: zone_valve_sw7
      - valve_switch: "Zone 8"
        enable_switch: "Zone 8 Enable"
        run_duration: 300s
        valve_switch_id: zone_valve_sw8
      - valve_switch: "Zone 9"
        enable_switch: "Zone 9 Enable"
        run_duration: 400s
        valve_switch_id: zone_valve_sw9
      - valve_switch: "Zone 10"
        enable_switch: "Zone 10 Enable"
        run_duration: 400s
        valve_switch_id: zone_valve_sw10
      - valve_switch: "Zone 11"
        enable_switch: "Zone 11 Enable"
        run_duration: 500s
        valve_switch_id: zone_valve_sw11
      - valve_switch: "Zone 12"
        enable_switch: "Zone 12 Enable"
        run_duration: 500s
        valve_switch_id: zone_valve_sw12
      - valve_switch: "Zone 13"
        enable_switch: "Zone 13 Enable"
        run_duration: 800s
        valve_switch_id: zone_valve_sw13
      - valve_switch: "Zone 14"
        enable_switch: "Zone 14 Enable"
        run_duration: 800s
        valve_switch_id: zone_valve_sw14
      - valve_switch: "Zone 15"
        enable_switch: "Zone 15 Enable"
        run_duration: 1200s
        valve_switch_id: zone_valve_sw15
      - valve_switch: "Zone 16"
        enable_switch: "Zone 16 Enable"
        run_duration: 600s
        valve_switch_id: zone_valve_sw16

# Valve control outputs config via I/O expanders (Zones 1-8 on first chip)    
switch:
  # First PCF8574 - Zones 1-8
  - platform: gpio
    id: zone_valve_sw1
    pin:
      pcf8574: pcf8574_output_hub1
      number: 0
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw2
    pin:
      pcf8574: pcf8574_output_hub1
      number: 1
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw3
    pin:
      pcf8574: pcf8574_output_hub1
      number: 2
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw4
    pin:
      pcf8574: pcf8574_output_hub1
      number: 3
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw5
    pin:
      pcf8574: pcf8574_output_hub1
      number: 4
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw6
    pin:
      pcf8574: pcf8574_output_hub1
      number: 5
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw7
    pin:
      pcf8574: pcf8574_output_hub1
      number: 6
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw8
    pin:
      pcf8574: pcf8574_output_hub1
      number: 7
      mode:
        output: true
      inverted: true
    internal: true

  # Second PCF8574 - Zones 9-16
  - platform: gpio
    id: zone_valve_sw9
    pin:
      pcf8574: pcf8574_output_hub2
      number: 0
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw10
    pin:
      pcf8574: pcf8574_output_hub2
      number: 1
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw11
    pin:
      pcf8574: pcf8574_output_hub2
      number: 2
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw12
    pin:
      pcf8574: pcf8574_output_hub2
      number: 3
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw13
    pin:
      pcf8574: pcf8574_output_hub2
      number: 4
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw14
    pin:
      pcf8574: pcf8574_output_hub2
      number: 5
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw15
    pin:
      pcf8574: pcf8574_output_hub2
      number: 6
      mode:
        output: true
      inverted: true
    internal: true
  - platform: gpio
    id: zone_valve_sw16
    pin:
      pcf8574: pcf8574_output_hub2
      number: 7
      mode:
        output: true
      inverted: true
    internal: true

  # Control switches
  - platform: template
    id: pause_switch
    name: "Pause Irrigation Switch"
    turn_on_action:
      then:
        - sprinkler.pause: esp32_sprinkler_ctrlr
    turn_off_action:
      then:
        - sprinkler.resume: esp32_sprinkler_ctrlr    

  # Auto schedule switches
  - platform: template
    id: auto_schedule_morning
    name: "Auto Schedule Morning"
    restore_mode: RESTORE_DEFAULT_OFF
    turn_on_action:
      - globals.set:
          id: morning_schedule_enabled
          value: 'true'
    turn_off_action:
      - globals.set:
          id: morning_schedule_enabled
          value: 'false'
    lambda: "return id(morning_schedule_enabled);"
    
  - platform: template
    id: auto_schedule_evening
    name: "Auto Schedule Evening"
    restore_mode: RESTORE_DEFAULT_OFF
    turn_on_action:
      - globals.set:
          id: evening_schedule_enabled
          value: 'true'
    turn_off_action:
      - globals.set:
          id: evening_schedule_enabled
          value: 'false'
    lambda: "return id(evening_schedule_enabled);"

# Number inputs for multiplier, durations, schedule times, and rain threshold
number:
  # System multiplier
  - platform: template
    id: esp32_ctrlr_multiplier
    name: "Run Duration Multiplier"
    min_value: 0.1
    max_value: 3.0
    step: 0.1
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).multiplier();"
    set_action:
      - sprinkler.set_multiplier:
          id: esp32_sprinkler_ctrlr
          multiplier: !lambda 'return x;'
          
  # Rain threshold setting
  - platform: template
    id: rain_threshold_input
    name: "Rain Threshold (mm)"
    icon: mdi:weather-rainy
    unit_of_measurement: 'mm'
    min_value: 0.0
    max_value: 50.0
    step: 0.5
    mode: box
    lambda: "return id(rain_threshold_mm);"
    set_action:
      - globals.set:
          id: rain_threshold_mm
          value: !lambda 'return x;'

  # Schedule time inputs
  - platform: template
    id: morning_start_hour
    name: "Morning Start Hour"
    icon: mdi:clock-time-four-outline
    min_value: 0
    max_value: 23
    step: 1
    mode: box
    lambda: "return id(morning_hour);"
    set_action:
      - globals.set:
          id: morning_hour
          value: !lambda 'return (int)x;'
          
  - platform: template
    id: morning_start_minute
    name: "Morning Start Minute"
    icon: mdi:clock-time-four-outline
    min_value: 0
    max_value: 59
    step: 5
    mode: box
    lambda: "return id(morning_minute);"
    set_action:
      - globals.set:
          id: morning_minute
          value: !lambda 'return (int)x;'
          
  - platform: template
    id: evening_start_hour
    name: "Evening Start Hour"
    icon: mdi:clock-time-eight-outline
    min_value: 0
    max_value: 23
    step: 1
    mode: box
    lambda: "return id(evening_hour);"
    set_action:
      - globals.set:
          id: evening_hour
          value: !lambda 'return (int)x;'
          
  - platform: template
    id: evening_start_minute
    name: "Evening Start Minute"
    icon: mdi:clock-time-eight-outline
    min_value: 0
    max_value: 59
    step: 5
    mode: box
    lambda: "return id(evening_minute);"
    set_action:
      - globals.set:
          id: evening_minute
          value: !lambda 'return (int)x;'

  # Zone duration settings (Zones 1-16)
  - platform: template
    id: sprinkler_valve_1_duration
    name: "Zone 1 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(0) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 0
          run_duration: !lambda "return x * 60;"
          
  - platform: template
    id: sprinkler_valve_2_duration
    name: "Zone 2 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(1) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 1
          run_duration: !lambda "return x * 60;" 

  - platform: template
    id: sprinkler_valve_3_duration
    name: "Zone 3 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(2) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 2
          run_duration: !lambda "return x * 60;" 
          
  - platform: template
    id: sprinkler_valve_4_duration
    name: "Zone 4 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(3) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 3
          run_duration: !lambda "return x * 60;"  
          
  - platform: template
    id: sprinkler_valve_5_duration
    name: "Zone 5 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(4) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 4
          run_duration: !lambda "return x * 60;"
          
  - platform: template
    id: sprinkler_valve_6_duration
    name: "Zone 6 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(5) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 5
          run_duration: !lambda "return x * 60;" 
          
  - platform: template
    id: sprinkler_valve_7_duration
    name: "Zone 7 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(6) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 6
          run_duration: !lambda "return x * 60;" 
          
  - platform: template
    id: sprinkler_valve_8_duration
    name: "Zone 8 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(7) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 7
          run_duration: !lambda "return x * 60;"

  - platform: template
    id: sprinkler_valve_9_duration
    name: "Zone 9 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(8) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 8
          run_duration: !lambda "return x * 60;"
          
  - platform: template
    id: sprinkler_valve_10_duration
    name: "Zone 10 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(9) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 9
          run_duration: !lambda "return x * 60;"

  - platform: template
    id: sprinkler_valve_11_duration
    name: "Zone 11 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(10) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 10
          run_duration: !lambda "return x * 60;"
          
  - platform: template
    id: sprinkler_valve_12_duration
    name: "Zone 12 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(11) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 11
          run_duration: !lambda "return x * 60;"

  - platform: template
    id: sprinkler_valve_13_duration
    name: "Zone 13 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(12) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 12
          run_duration: !lambda "return x * 60;"
          
  - platform: template
    id: sprinkler_valve_14_duration
    name: "Zone 14 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(13) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 13
          run_duration: !lambda "return x * 60;"

  - platform: template
    id: sprinkler_valve_15_duration
    name: "Zone 15 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(14) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 14
          run_duration: !lambda "return x * 60;"
          
  - platform: template
    id: sprinkler_valve_16_duration
    name: "Zone 16 Duration"
    icon: mdi:timer
    unit_of_measurement: Min
    min_value: 1
    max_value: 120
    step: 1.0
    mode: box
    lambda: "return id(esp32_sprinkler_ctrlr).valve_run_duration(15) / 60;"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: esp32_sprinkler_ctrlr
          valve_number: 15
          run_duration: !lambda "return x * 60;"

# Sensors
sensor:
  # Rain sensor from Home Assistant
  - platform: homeassistant
    id: rain_today
    entity_id: sensor.daily_rain_rate
    
  - platform: wifi_signal
    name: "ESP Irrigation WiFi Signal"
    update_interval: 300s  
    
  - platform: template
    name: "Zone Time Remaining"
    icon: mdi:progress-clock    
    unit_of_measurement: 'Min'
    lambda: |-
      auto remaining = id(esp32_sprinkler_ctrlr).time_remaining_active_valve();
      if (remaining.has_value()) {
        return remaining.value() / 60.0;
      }
      return 0.0;
    update_interval: 10s

# Text sensors for schedule display
text_sensor:
  - platform: template
    name: "Next Morning Schedule"
    icon: mdi:clock-outline
    lambda: |-
      if (id(morning_schedule_enabled)) {
        char time_str[10];
        snprintf(time_str, sizeof(time_str), "%02d:%02d", id(morning_hour), id(morning_minute));
        return std::string(time_str);
      } else {
        return std::string("Disabled");
      }
    update_interval: 60s
    
  - platform: template
    name: "Next Evening Schedule"
    icon: mdi:clock-outline
    lambda: |-
      if (id(evening_schedule_enabled)) {
        char time_str[10];
        snprintf(time_str, sizeof(time_str), "%02d:%02d", id(evening_hour), id(evening_minute));
        return std::string(time_str);
      } else {
        return std::string("Disabled");
      }
    update_interval: 60s

# Binary sensors for buttons and status
binary_sensor:
  - platform: gpio
    id: button_1
    pin:
      pcf8574: pcf8574_input_hub
      number: 4
      mode:
        input: true
      inverted: true
    internal: true
    on_click:
    - min_length: 50ms
      max_length: 350ms
      then:
        - sprinkler.resume_or_start_full_cycle: esp32_sprinkler_ctrlr

  - platform: gpio
    id: button_2
    pin:
      pcf8574: pcf8574_input_hub
      number: 5
      mode:
        input: true
      inverted: true
    internal: true
    on_click:
    - min_length: 50ms
      max_length: 350ms
      then:
        - sprinkler.next_valve: esp32_sprinkler_ctrlr

  - platform: gpio
    id: button_3
    pin:
      pcf8574: pcf8574_input_hub
      number: 6
      mode:
        input: true
      inverted: true
    internal: true
    on_click:
    - min_length: 50ms
      max_length: 350ms
      then:
        - sprinkler.pause: esp32_sprinkler_ctrlr

  - platform: gpio
    id: button_4
    pin:
      pcf8574: pcf8574_input_hub
      number: 7
      mode:
        input: true
      inverted: true
    internal: true
    on_click:
    - min_length: 50ms
      max_length: 350ms
      then:
        - sprinkler.shutdown: esp32_sprinkler_ctrlr

  - platform: template
    name: "Auto Schedule Active"
    icon: mdi:calendar-clock
    lambda: |-
      return id(morning_schedule_enabled) || id(evening_schedule_enabled);

# Automatic scheduling logic
interval:
  - interval: 60s  # Check every minute
    then:
      - lambda: |-
          auto time = id(homeassistant_time).now();
          if (!time.is_valid()) return;
          
          int current_hour = time.hour;
          int current_minute = time.minute;
          int current_day = time.day_of_year;
          
          // Reset daily run tracker at midnight
          if (current_hour == 0 && current_minute == 0) {
            id(last_run_day) = -1;
            ESP_LOGI("scheduler", "Daily run tracker reset");
          }
          
          // Prevent multiple runs on the same day
          if (id(last_run_day) != current_day) {
            // Check morning schedule
            if (id(morning_schedule_enabled) && 
                current_hour == id(morning_hour) && 
                current_minute == id(morning_minute)) {
              
              // Check rain sensor before starting
              float rain_amount = id(rain_today).state;
              if (isnan(rain_amount)) rain_amount = 0.0;  // Handle invalid sensor reading
              
              if (rain_amount < id(rain_threshold_mm)) {
                ESP_LOGI("scheduler", "Starting morning irrigation cycle (Rain: %.1fmm < %.1fmm)", rain_amount, id(rain_threshold_mm));
                id(esp32_sprinkler_ctrlr).start_full_cycle();
                id(last_run_day) = current_day;
              } else {
                ESP_LOGI("scheduler", "Skipping morning cycle due to rain (%.1fmm >= %.1fmm)", rain_amount, id(rain_threshold_mm));
              }
            }
            
            // Check evening schedule  
            if (id(evening_schedule_enabled) && 
                current_hour == id(evening_hour) && 
                current_minute == id(evening_minute)) {
              
              // Check rain sensor before starting
              float rain_amount = id(rain_today).state;
              if (isnan(rain_amount)) rain_amount = 0.0;  // Handle invalid sensor reading
              
              if (rain_amount < id(rain_threshold_mm)) {
                ESP_LOGI("scheduler", "Starting evening irrigation cycle (Rain: %.1fmm < %.1fmm)", rain_amount, id(rain_threshold_mm));
                id(esp32_sprinkler_ctrlr).start_full_cycle();
                id(last_run_day) = current_day;
              } else {
                ESP_LOGI("scheduler", "Skipping evening cycle due to rain (%.1fmm >= %.1fmm)", rain_amount, id(rain_threshold_mm));
              }
            }
          }

# Time source config
time:
  - platform: homeassistant
    id: homeassistant_time
    timezone: Asia/Bangkok
1 Like

Thanks for the contribution. I’ll have to try it out here when I have time.

An update on my KC868-A8 watering station. Early last summer here I noted the wifi dropping out on HA at odd times of the day, sometimes for hours. During these times I could not start a watering cycle. After a lot of trial and error at board level I decided to replace the ESP32 module on the KC868 board. Got a Wroom ESP32 from AliExpress and replaced the module. Since then I’ve had no connection issues. Getting the module off was harder than expected because there is a thermal pad right in the middle underneath that can’t be seen.
I had to remove the top tin shield to get enough heat into the middle of the module to remove it without damaging the KC board.

Here are some screenshots of my version. you can see the schedule added but I also added a rain factor. With this I can adjust the sensitive of the minimal amount of rain before it stops the irrigation. Next to the mulitplier some real finetuning tools. I live in the tropics so it is usually too much or not enough rain and the irrigation keeps my garden alive.


Display:

1 Like