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?