First I want to thank Brian for taking the time to post his project. It’s pretty cool.
As I didn’t have a Sonoff I went this route. https://khaz.me/cheap-and-easy-control-of-8-relays-through-home-assistant/
I modified Rob’s YAML and .h (posted above as he had included the days option) so that it would work with 3 Zones and changed it to English.
Here’s my changes if anyone is interested;
Configuration YAML
input_text:
irrigation_zone1_times:
name: Zone 1 Time Periods
icon: mdi:chart-timeline
irrigation_zone2_times:
name: Zone 2 Time Periods
icon: mdi:chart-timeline
irrigation_zone3_times:
name: Zone 3 Time Periods
icon: mdi:chart-timeline
irrigation_zone4_times:
name: Zone 4 Time Periods
icon: mdi:chart-timeline
irrigation_zone1_days:
name: Zone 1 Days of the Week
icon: mdi:calendar-week
irrigation_zone2_days:
name: Zone 2 Days of the Week
icon: mdi:calendar-week
irrigation_zone3_days:
name: Zone 3 Days of the Week
icon: mdi:calendar-week
irrigation_zone4_days:
name: Zone 4 Days of the Week
icon: mdi:calendar-week
input_number:
irrigation_zone1_duration:
name: Zone 1 Duration
icon: mdi:timer
min: 2
max: 60
step: 2
irrigation_zone2_duration:
name: Zone 2 Duration
icon: mdi:timer
min: 2
max: 60
step: 2
irrigation_zone3_duration:
name: Zone 3 Duration
icon: mdi:timer
min: 2
max: 60
step: 2
irrigation_zone4_duration:
name: Zone 4 Duration
icon: mdi:timer
min: 2
max: 60
step: 2
Irrigation Yaml
esphome:
name: irrigationbackyard
platform: ESP8266
board: d1_mini
includes:
- irrigation.h
wifi:
ssid: "xxxx"
password: "xxxx"
power_save_mode: none
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: " Irrigation Fallback Hotspot"
password: "xxxx"
captive_portal:
# Enable logging
logger:
# Enable Home Assistant API
api:
password: "xxxx"
ota:
password: "xxxx"
web_server:
port: 80
globals:
# ============================================================================= #
# Irrigation time remaining
- id: remaining_time1
type: int
restore_value: no
initial_value: "300"
- id: remaining_time2
type: int
restore_value: no
initial_value: "300"
- id: remaining_time3
type: int
restore_value: no
initial_value: "300"
# ============================================================================= #
# Store previous values to verify change.
- id: remaining_time1_previous
type: int
restore_value: no
initial_value: "0"
- id: remaining_time2_previous
type: int
restore_value: no
initial_value: "0"
- id: remaining_time3_previous
type: int
restore_value: no
initial_value: "0"
i2c:
sda: D2
scl: D1
scan: True
mcp23017:
- id: 'mcp23017_hub'
address: 0x20
#- platform: gpio
# name: relay3
# icon: mdi:electric-switch
# pin:
# mcp23017: mcp23017_hub
# number: 13
# mode: OUTPUT
# inverted: True
#- platform: gpio
# name: relay4
# icon: mdi:electric-switch
# pin:
# mcp23017: mcp23017_hub
# number: 12
# mode: OUTPUT
# inverted: True
#- platform: gpio
# name: relay5
# icon: mdi:electric-switch
# pin:
# mcp23017: mcp23017_hub
# number: 11
# mode: OUTPUT
# inverted: True
#- platform: gpio
# name: relay6
# icon: mdi:electric-switch
# pin:
# mcp23017: mcp23017_hub
# number: 10
# mode: OUTPUT
# inverted: True
#- platform: gpio
# name: relay7
# icon: mdi:electric-switch
# pin:
# mcp23017: mcp23017_hub
# number: 9
# mode: OUTPUT
# inverted: True
#- platform: gpio
# name: relay8
# icon: mdi:electric-switch
# pin:
# mcp23017: mcp23017_hub
# number: 8
# mode: OUTPUT
# inverted: True
time:
- platform: homeassistant
id: homeassistant_time
timezone: EST+5EDT,M3.2.0/2,M11.1.0/2
# Time based automations.
on_time:
- seconds: 0
minutes: /1
then:
- lambda: |-
ESP_LOGD("Time based", "zone 1 next: %s", id(irrigation_zone1_next).state.c_str());
if (scheduled_runtime(id(irrigation_zone1_next).state.c_str())) {
id(irrigation_zone1).turn_on();
}
if (scheduled_runtime(id(irrigation_zone2_next).state.c_str())) {
id(irrigation_zone2).turn_on();
}
if (scheduled_runtime(id(irrigation_zone3_next).state.c_str())) {
id(irrigation_zone3).turn_on();
}
binary_sensor:
- platform: gpio
pin:
number: 14
# inverted: True
mode: INPUT_PULLUP
filters:
- delayed_on: 1000ms
name: "Shed Door"
device_class: door
- platform: status
name: Irrigation Project
switch:
# ============================================================================= #
# Virtual Zone Switches which toggle the relay, and store the current state.
- platform: restart
name: "Relay Channel Board REBOOT"
- platform: template
name: Irrigation Zone 1
id: irrigation_zone1
icon: mdi:sprinkler
lambda: return id(relay1).state;
optimistic: true
turn_on_action:
# Turn on if not disabled.
if:
condition:
lambda: return id(irrigation_zone1_duration).state >= 1;
then:
- switch.turn_on: relay1
turn_off_action:
- switch.turn_off: relay1
- platform: template
name: Irrigation Zone 2
id: irrigation_zone2
icon: mdi:sprinkler
lambda: return id(relay2).state;
optimistic: true
turn_on_action:
# Turn on if not disabled.
if:
condition:
lambda: return id(irrigation_zone2_duration).state >= 1;
then:
- switch.turn_on: relay2
turn_off_action:
- switch.turn_off: relay2
- platform: template
name: Irrigation Zone 3
id: irrigation_zone3
icon: mdi:sprinkler
lambda: return id(relay3).state;
optimistic: true
turn_on_action:
# Turn on if not disabled.
if:
condition:
lambda: return id(irrigation_zone3_duration).state >= 1;
then:
- switch.turn_on: relay3
turn_off_action:
- switch.turn_off: relay3
- platform: gpio
id: relay1
name: Relay1
icon: mdi:electric-switch
pin:
mcp23017: mcp23017_hub
number: 15
mode: OUTPUT
inverted: true
restore_mode: ALWAYS_OFF
on_turn_on:
then:
# Start the countdown timer.
- globals.set:
id: remaining_time1
value: !lambda return id(irrigation_zone1_duration).state * 60;
# Show the remaining time.
- sensor.template.publish:
id: irrigation_zone1_remaining
state: !lambda return id(irrigation_zone1_duration).state;
# Show the "Next Time" as "now".
- text_sensor.template.publish:
id: irrigation_zone1_next
state: "now"
# state NOW on original code, change to your preferred language
on_turn_off:
then:
- sensor.template.publish:
id: irrigation_zone1_remaining
state: "0"
# Update the next scheduled run time.
- text_sensor.template.publish:
id: irrigation_zone1_next
state: !lambda |-
return update_next_runtime(id(irrigation_zone1_times).state, id(irrigation_zone1_days).state);
- platform: gpio
id: relay2
name: Relay2
icon: mdi:electric-switch
pin:
mcp23017: mcp23017_hub
number: 14
mode: OUTPUT
inverted: True
restore_mode: ALWAYS_OFF
on_turn_on:
then:
# Start the countdown timer.
- globals.set:
id: remaining_time2
value: !lambda return id(irrigation_zone2_duration).state * 60;
# Show the remaining time.
- sensor.template.publish:
id: irrigation_zone2_remaining
state: !lambda return id(irrigation_zone2_duration).state;
# Show the "Next Time" as "now".
- text_sensor.template.publish:
id: irrigation_zone2_next
state: "now"
on_turn_off:
then:
- sensor.template.publish:
id: irrigation_zone2_remaining
state: "0"
# Update the next scheduled run time.
- text_sensor.template.publish:
id: irrigation_zone2_next
state: !lambda |-
return update_next_runtime(id(irrigation_zone2_times).state, id(irrigation_zone2_days).state);
- platform: gpio
id: relay3
name: Relay3
icon: mdi:electric-switch
pin:
mcp23017: mcp23017_hub
number: 13
mode: OUTPUT
inverted: True
restore_mode: ALWAYS_OFF
on_turn_on:
then:
# Start the countdown timer.
- globals.set:
id: remaining_time3
value: !lambda return id(irrigation_zone3_duration).state * 60;
# Show the remaining time.
- sensor.template.publish:
id: irrigation_zone3_remaining
state: !lambda return id(irrigation_zone3_duration).state;
# Show the "Next Time" as "now".
- text_sensor.template.publish:
id: irrigation_zone3_next
state: "now"
on_turn_off:
then:
- sensor.template.publish:
id: irrigation_zone3_remaining
state: "0"
# Update the next scheduled run time.
- text_sensor.template.publish:
id: irrigation_zone3_next
state: !lambda |-
return update_next_runtime(id(irrigation_zone3_times).state, id(irrigation_zone3_days).state);
sensor:
- platform: uptime
name: Irrigation Controller Uptime
unit_of_measurement: h
filters:
- lambda: return int((x + 1800.0) / 3600.0);
- platform: wifi_signal
name: Irrigation Controller WiFi Signal
update_interval: 30s
# ============================================================================= #
# Retrieve durations settings from the Home Assistant UI.
- platform: homeassistant
id: ui_zone1_duration
entity_id: input_number.irrigation_zone1_duration
on_value:
#if:
# condition:
# api.connected:
then:
- sensor.template.publish:
id: irrigation_zone1_duration
state: !lambda return id(ui_zone1_duration).state;
- platform: homeassistant
id: ui_zone2_duration
entity_id: input_number.irrigation_zone2_duration
on_value:
#if:
# condition:
# api.connected:
then:
- sensor.template.publish:
id: irrigation_zone2_duration
state: !lambda return id(ui_zone2_duration).state;
- platform: homeassistant
id: ui_zone3_duration
entity_id: input_number.irrigation_zone3_duration
on_value:
#if:
# condition:
# api.connected:
then:
- sensor.template.publish:
id: irrigation_zone3_duration
state: !lambda return id(ui_zone3_duration).state;
# ============================================================================= #
# Store durations.
- platform: template
name: Irrigation Duration Zone 1
id: irrigation_zone1_duration
accuracy_decimals: 0
unit_of_measurement: min
icon: mdi:camera-timer
- platform: template
name: Irrigation Duration Zone 2
id: irrigation_zone2_duration
accuracy_decimals: 0
unit_of_measurement: min
icon: mdi:camera-timer
- platform: template
name: Irrigation Duration Zone 3
id: irrigation_zone3_duration
accuracy_decimals: 0
unit_of_measurement: min
icon: mdi:camera-timer
# ============================================================================= #
# Countdown sensors.
- platform: template
name: Zone 1 Time Remaining
id: irrigation_zone1_remaining
lambda: "return 0;"
accuracy_decimals: 0
unit_of_measurement: min
icon: mdi:timer
on_value:
then:
- if:
condition:
lambda: return id(remaining_time1) == 0;
then:
- switch.turn_off: relay1
- platform: template
name: Zone 2 Time Remaining
id: irrigation_zone2_remaining
lambda: "return 0;"
accuracy_decimals: 0
unit_of_measurement: min
icon: mdi:timer
on_value:
then:
- if:
condition:
lambda: return id(remaining_time2) == 0;
then:
- switch.turn_off: relay2
- platform: template
name: Zone 3 Time Remaining
id: irrigation_zone3_remaining
lambda: "return 0;"
accuracy_decimals: 0
unit_of_measurement: min
icon: mdi:timer
on_value:
then:
- if:
condition:
lambda: return id(remaining_time3) == 0;
then:
- switch.turn_off: relay3
text_sensor:
# ============================================================================= #
# Retrieve list of times from the Home Assistant UI.
- platform: homeassistant
id: ui_zone1_times
entity_id: input_text.irrigation_zone1_times
on_value:
#if:
# condition:
# api.connected:
then:
#- delay: 10sec
- text_sensor.template.publish:
id: irrigation_zone1_times
state: !lambda return id(ui_zone1_times).state;
- platform: homeassistant
id: ui_zone2_times
entity_id: input_text.irrigation_zone2_times
on_value:
#if:
# condition:
# api.connected:
then:
#- delay: 10sec
- text_sensor.template.publish:
id: irrigation_zone2_times
state: !lambda return id(ui_zone2_times).state;
- platform: homeassistant
id: ui_zone3_times
entity_id: input_text.irrigation_zone3_times
on_value:
#if:
# condition:
# api.connected:
then:
#- delay: 10sec
- text_sensor.template.publish:
id: irrigation_zone3_times
state: !lambda return id(ui_zone3_times).state;
# ============================================================================= #
# Store time lists.
- platform: template
name: Zone 1 Schedule
id: irrigation_zone1_times
on_value:
then:
# Update the next scheduled run time.
- text_sensor.template.publish:
id: irrigation_zone1_next
state: !lambda |-
return update_next_runtime(id(irrigation_zone1_times).state, id(irrigation_zone1_days).state);
- platform: template
name: Zone 2 Schedule
id: irrigation_zone2_times
on_value:
then:
# Update the next scheduled run time.
- text_sensor.template.publish:
id: irrigation_zone2_next
state: !lambda |-
return update_next_runtime(id(irrigation_zone2_times).state, id(irrigation_zone2_days).state);
- platform: template
name: Zone 3 Schedule
id: irrigation_zone3_times
on_value:
then:
# Update the next scheduled run time.
- text_sensor.template.publish:
id: irrigation_zone3_next
state: !lambda |-
return update_next_runtime(id(irrigation_zone3_times).state, id(irrigation_zone3_days).state);
# ============================================================================= #
# Retrieve list of days from the Home Assistant UI.
- platform: homeassistant
id: ui_zone1_days
entity_id: input_text.irrigation_zone1_days
on_value:
#if:
# condition:
# api.connected:
then:
#- delay: 10sec
- text_sensor.template.publish:
id: irrigation_zone1_days
state: !lambda return id(ui_zone1_days).state;
- platform: homeassistant
id: ui_zone2_days
entity_id: input_text.irrigation_zone2_days
on_value:
#if:
# condition:
# api.connected:
then:
#- delay: 10sec
- text_sensor.template.publish:
id: irrigation_zone2_days
state: !lambda return id(ui_zone2_days).state;
- platform: homeassistant
id: ui_zone3_days
entity_id: input_text.irrigation_zone3_days
on_value:
#if:
# condition:
# api.connected:
then:
#- delay: 10sec
- text_sensor.template.publish:
id: irrigation_zone3_days
state: !lambda return id(ui_zone3_days).state;
# ============================================================================= #
# Store time lists.
- platform: template
name: Zone 1 Days
id: irrigation_zone1_days
on_value:
then:
# Update the next scheduled run time.
- text_sensor.template.publish:
id: irrigation_zone1_next
state: !lambda |-
return update_next_runtime(id(irrigation_zone1_times).state, id(irrigation_zone1_days).state);
- platform: template
name: Zone 2 Days
id: irrigation_zone2_days
on_value:
then:
# Update the next scheduled run time.
- text_sensor.template.publish:
id: irrigation_zone2_next
state: !lambda |-
return update_next_runtime(id(irrigation_zone2_times).state, id(irrigation_zone2_days).state);
- platform: template
name: Zone 3 Days
id: irrigation_zone3_days
on_value:
then:
# Update the next scheduled run time.
- text_sensor.template.publish:
id: irrigation_zone3_next
state: !lambda |-
return update_next_runtime(id(irrigation_zone3_times).state, id(irrigation_zone3_days).state);
# ============================================================================= #
# Set the next scheduled time.
- platform: template
name: Zone 1 Next Watering
id: irrigation_zone1_next
icon: mdi:calendar-clock
- platform: template
name: Zone 2 Next Watering
id: irrigation_zone2_next
icon: mdi:calendar-clock
- platform: template
name: Zone 3 Next Watering
id: irrigation_zone3_next
icon: mdi:calendar-clock
# Update the countdown timers every 5 seconds.
interval:
- interval: 5s
then:
- lambda: |-
if (id(remaining_time1) > 0) {
// Store the previous time.
id(remaining_time1_previous) = id(remaining_time1);
// When the relay is on.
if (id(relay1).state) {
// Decrement the timer.
id(remaining_time1) -= 5;
// Turn off the relay when the time reaches zero... or the remaining time fails a sanity check!
//if (id(remaining_time1) <= 0 || id(irrigation_zone1_remaining).state > id(irrigation_zone1_duration).state){
if (id(remaining_time1) <= 0) {
id(relay1).turn_off();
id(remaining_time1) = 0;
}
}
// Update the remaining time display.
if (id(remaining_time1_previous) != id(remaining_time1)) {
id(irrigation_zone1_remaining).publish_state( (id(remaining_time1)/60) + 1 );
}
}
if (id(remaining_time2) > 0) {
id(remaining_time2_previous) = id(remaining_time2);
if (id(relay2).state) {
id(remaining_time2) -= 5;
if (id(remaining_time2) <= 0) {
id(relay2).turn_off();
id(remaining_time2) = 0;
}
}
if (id(remaining_time2_previous) != id(remaining_time2)) {
id(irrigation_zone2_remaining).publish_state( (id(remaining_time2)/60) + 1 );
}
}
if (id(remaining_time3) > 0) {
id(remaining_time3_previous) = id(remaining_time3);
if (id(relay3).state) {
id(remaining_time3) -= 5;
if (id(remaining_time3) <= 0) {
id(relay3).turn_off();
id(remaining_time3) = 0;
}
}
if (id(remaining_time3_previous) != id(remaining_time3)) {
id(irrigation_zone3_remaining).publish_state( (id(remaining_time3)/60) + 1 );
}
}
status_led:
pin:
number: GPIO2
Irrigation.h (goes in ESPHOME Directory)
#include "esphome.h"
using namespace std;
// Declare functions before calling them.
bool scheduled_runtime(string);
string update_next_runtime(string, string);
bool scheduled_runtime(string time) {
// Retrieve the current time.
auto time_now = id(homeassistant_time).now();
int time_hour = time_now.hour;
int time_minute = time_now.minute;
int time_wday = time_now.day_of_week; //added for day scheduling
//Prevent program crash - functions were created specting a time formated string
// if you pass "now", program crashes in certain cases.
if (time == "now") { //"now" in English, customize according to your prefs.
return false;
}
// Split the hour and minutes.
int next_hour = atoi(time.substr(0,2).c_str());
int next_minute = atoi(time.substr(3,2).c_str()); int next_wday = 0;
string day = time.substr(6,3).c_str(); //day text is added to original string
// Converting days to week numbers, change text based on your Language
if (day == "Mon" || day == "mon" || day == "MON") {
next_wday = 2;
} else if (day == "Tue" || day == "tue" || day == "TUE") {
next_wday = 3;
} else if (day == "Wed" || day == "wed" || day == "WED") {
next_wday = 4;
} else if (day == "The" || day == "the" || day == "THU") {
next_wday = 5;
} else if (day == "Fri" || day == "fri" || day == "FRI") {
next_wday = 6;
} else if (day == "Sat" || day == "sat" || day == "SAT") {
next_wday = 7;
} else if (day == "Sun" || day == "sun" || day == "SUN") {
next_wday = 1;
} else if (day == "Tod") { //Today in English
next_wday = time_wday;
}
//ESP_LOGD("scheduled_runtime()", "now: %i:%i, wday: %i", next_hour, next_minute, time_wday);
// return (time_hour == next_hour && time_minute == next_minute);
return (time_hour == next_hour && time_minute == next_minute && time_wday == next_wday); //added wday to condition
}
string update_next_runtime(string time_list, string days_list) {
// Initialize variables.
vector<string> times;
vector<string> next_time;
vector<string> days; //added for day scheduling
vector<string> next_day; //added for day scheduling
char * token;
char * token2; //to work on day string
//bool single_time = false;
//bool single_day = false;
string updated_next_time;
string updated_next_day;
// Split the list of run times into an array.
token = strtok(&time_list[0], ",");
while (token != NULL) {
times.push_back(token);
token = strtok(NULL, ",");
}
// Split the list of run days into an array.
token2 = strtok(&days_list[0], ",");
while (token2 != NULL) {
days.push_back(token2);
token2 = strtok(NULL, ",");
}
// Need to delete this in order to day-time integration works
// Stop now if the list does not contain more than one time.
//if (times.size() <= 1) {
//return time_list;
//updated_next_time = time_list;
//single_time = true;
//}
// Stop now if the list does not contain more than one day.
//if (days.size() <= 1) {
//updated_next_day = days_list;
//single_day = true;
//}
// Retrieve the current time.
auto time_now = id(homeassistant_time).now();
int time_hour = time_now.hour;
int time_minute = time_now.minute;
int time_wday = time_now.day_of_week;
// Initialize variables.
int next_hour = 0;
int next_minute = 0;
int index = 0;
int loop_count = 0;
int time_count = times.size()-1;
// Compare the list of times with the current time, and return the next in the list.
//ESP_LOGD("update_next_runtime", "now: %i:%i", hour, minute);
//if (!single_time) {
for (string time : times) {
// Retrieve the next scheduled time from the list.
next_hour = atoi(time.substr(0,2).c_str());
next_minute = atoi(time.substr(3,2).c_str());
//ESP_LOGD("update_next_runtime", "next_hour: %s", time.c_str());
if (time_hour < next_hour || (time_hour == next_hour && time_minute < next_minute)) {
// Return this time if the next hour is greater than the current hour.
//return times[loop_count].c_str();
//break;
updated_next_time = times[loop_count].c_str();
break;
// When we reach the end of our schedule for the day, return the first time of tomorrow.
} else if (time_count == loop_count) {
//return times[0].c_str();
//break;
updated_next_time = times[0].c_str();
break;
}
// Increment the loop counter and array index.
loop_count += 1;
index += 2;
}
//}
int loop2_count = 0;
int day_count = days.size()-1;
int next_wday = 0;
int index2 = 0;
//if (!single_day) {
for (string day : days) {
// Retrieve the next scheduled day from the list. Set your preferred language. Check correct correlations with day numbers
if (day == "Mon" || day == "mon" || day == "MON") {
next_wday = 2;
} else if (day == "Tue" || day == "tue" || day == "TUE") {
next_wday = 3;
} else if (day == "Wed" || day == "wed" || day == "WED") {
next_wday = 4;
} else if (day == "Thu" || day == "thu" || day == "THU") {
next_wday = 5;
} else if (day == "Fri" || day == "fri" || day == "FRI") {
next_wday = 6;
} else if (day == "Sat" || day == "sat" || day == "SAT") {
next_wday = 7;
} else if (day == "Sun" || day == "sun" || day == "SUN") {
next_wday = 1;
}
//ESP_LOGD("update_next_runtime", "next_hour: %s", time.c_str());
if (time_wday == next_wday && (time_hour < next_hour || (time_hour == next_hour && time_minute < next_minute))) {
// Return this day if the next day is today AND there is still a scheduled time for today.
//updated_next_day = days[loop2_count].c_str();
updated_next_day = "Hoy"; //Today
break;
// If the next day is not today, also the next time is the first of day
} else if (time_wday < next_wday) {
updated_next_day = days[loop2_count].c_str(); updated_next_time = times[0].c_str();
break;
// When we reach the end of our schedule for the week, return the first day of next week.
} else if (day_count == loop2_count) {
updated_next_day = days[0].c_str();
break;
}
// Increment the loop counter and array index.
loop2_count += 1;
index2 += 2;
}
//}
return updated_next_time + " " + updated_next_day;
}