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.

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




