ESPHome: DIY Irrigation Controller With Internal Scheduler

Update March 4, 2021
Updated versions of this information is available on my website as a 3 part series of articles.

  1. Hardware, Electronics, and ESPHome code
  2. Lovelace User Interface
  3. Entities & Simplified User Interface

I programmed a Sonoff 4 Channel Pro R2 with ESPHome to replace a flaky off-the-shelf battery powered irrigation controller. I am especially proud of the lambda functions I was able to put together to run the schedule on the Sonoff. Avoiding reliance on Home Assistant automations allows me to program and restart Home Assistant all I want without worrying about interrupting an hour long pool fill, or even worse causing the water valve to remain open indefinitely!

The buttons on the Sonoff can be used to manually start a zone, but I also created a use Interface for monitoring and editing the schedule for each zone.

Anyway, if you are interested in learning more about how I did it, I go into a lot of detail in an article on my blog (link below). If you prefer you can go straight to the source code on Github (irrigation.yaml & irrigation.h).

Thank you to Home Assistant Community members: @jlax47, @nickrout, @glmnet, and @risk, as well as @broxy70’s countdown code.

10 Likes

Thanks for the thanks, but I don’t think I did all that much to help!

Pretty cool project. I found it via web search then found all the links to the usual places I haunt (esphome, this forum, github).
It’s also pretty cool that you learned enough coding to make the lambda’s and header file. Even inspirational, I’d say.

I took your code off github and expanded it to support 4 zones. I had an old 4ch lying around so I loaded it up and ran it for a while, then installed it all in a nice case, mounted on the wall, conduit to the valves, etc. I hooked up the last valve today and found the interface on home assistant constantly changing to ‘unavailable’. Then when I went outside, even manual presses of the buttons on the 4ch were laggy and often did not work. The unit would appear to lock up and reboot regularly too.

I saw in your github issues you were testing stability. Did you have stability issues with it ever? I’m wondering if 4 zones is more than it can handle. It seems like it worked better before I expanded your code out from 2 zones to 4 zones. But it’s also a hot day (high of 105 deg F) and even though the controller is in the shade, it may have overheated. I’ve left the cover on it’s box open to provide some airflow but that’s little help with outside temps so high.

1 Like

This thing has been rock solid, 295 hours of uptime since my last code update! It never seems to go down unless I push a code update to it. I may have an unfair advantage as I have a commercial grade UniFi wireless network setup with 3 wireless access points, including one inside a closet about 10 feet from my Sonoff. For reference mine is reporting a -76db signal strength.

You could always remark out all of the code for 3 of the zones to test your theory about 4 zones being too much. I don’t know. I left that code out to make maintenance easier, since I didn’t need any more yet.

Thanks for the reply. I did exactly what you suggested and commented out the extra code, just turning it into a basic set of relays and it still had problems falling offline. In fact, I have a sonoff basic that runs an exhaust fan in my server room and it was having the same problem. After moving it to tasmota, all problems went away on both units. Maybe something going on with my esphome setup? For now I’m happy just using them with tasmota.
I have a unifi ap’s as well and signal strength was very similar.
It’s still an awesome setup. Thanks for publishing it all. My garden loves you. Seriously, the plants look better than ever.

1 Like

Hi, based on Brian’s work, i added support for scheduling irrigation for different days of the week.

Here is the modified irrigation.h (i modified other parts too based on a border-case test)

#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 == "ahora") { //"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 == "Lun" || day == "lun" || day == "LUN") {
      next_wday = 2;
    } else if (day == "Mar" || day == "mar" || day == "MAR") {
      next_wday = 3;
    } else if (day == "Mie" || day == "mie" || day == "MIE") {
      next_wday = 4;
    } else if (day == "Jue" || day == "jue" || day == "JUE") {
      next_wday = 5;
    } else if (day == "Vie" || day == "vie" || day == "VIE") {
      next_wday = 6;
    } else if (day == "Sab" || day == "sab" || day == "SAB") {
      next_wday = 7;
    } else if (day == "Dom" || day == "dom" || day == "DOM") {
      next_wday = 1;
    } else if (day == "Hoy") { //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 == "Lun" || day == "lun" || day == "LUN") {
      next_wday = 2;
    } else if (day == "Mar" || day == "mar" || day == "MAR") {
      next_wday = 3;
    } else if (day == "Mie" || day == "mie" || day == "MIE") {
      next_wday = 4;
    } else if (day == "Jue" || day == "jue" || day == "JUE") {
      next_wday = 5;
    } else if (day == "Vie" || day == "vie" || day == "VIE") {
      next_wday = 6;
    } else if (day == "Sab" || day == "sab" || day == "SAB") {
      next_wday = 7;
    } else if (day == "Dom" || day == "dom" || day == "DOM") {
      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;

}
2 Likes

Now i am sharing the yaml that uses the modified irrigation.h

time:
  - platform: homeassistant
    id: homeassistant_time
    timezone: CST+4CDT,M9.1.0/0,M4.1.0/0  
    # 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();
              }

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"

  # ============================================================================= #
  # 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"

binary_sensor:
 - platform: status
   name: Estado $project

switch:
  # ============================================================================= #
  # Virtual Zone Switches which toggle the relay, and store the current state.
  - platform: template
    name: Riego Zona 1
    id: irrigation_zone1
    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: Riego Zona 2
    id: irrigation_zone2
    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

  # ============================================================================= #
  # Relays which trigger solenoids - restore_mode prevents a failure if device resets when switchs are on. I strongly recommend to use it.
  - platform: gpio
    id: relay1
    name: "Relay1"
    pin: D1
    inverted: yes
    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: "ahora"
            # 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"
    pin: D2
    inverted: yes
    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: "ahora"
    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);
                  
sensor:
  - platform: uptime
    name: Controlador Riego Uptime
    unit_of_measurement: h
    filters:
      - lambda: return int((x + 1800.0) / 3600.0);
  - platform: wifi_signal
    name: Controlador Riego Señal WiFi
    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;
  # ============================================================================= #
  # Store durations.
  - platform: template
    name: Duración riego Zona 1
    id: irrigation_zone1_duration
    accuracy_decimals: 0
    unit_of_measurement: min
    icon: mdi:camera-timer
  - platform: template
    name: Duración riego Zona 2
    id: irrigation_zone2_duration
    accuracy_decimals: 0
    unit_of_measurement: min
    icon: mdi:camera-timer
  # ============================================================================= #
  # Countdown sensors.
  - platform: template
    name: Zona 1 tiempo restante
    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: Zona 2 tiempo restante
    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

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;
  # ============================================================================= #
  # Store time lists.
  - platform: template
    name: Zona 1 Horarios
    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: Zona 2 Horarios
    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);
# ============================================================================= #
  # 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;
  # ============================================================================= #
  # Store time lists.
  - platform: template
    name: Zona 1 Días
    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: Zona 2 Días
    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);
  # ============================================================================= #
  # Set the next scheduled time.
  - platform: template
    name: Zona 1 siguiente riego
    id: irrigation_zone1_next
    icon: mdi:calendar-clock
  - platform: template
    name: Zona 2 siguiente riego
    id: irrigation_zone2_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 );
            }
          }

2 Likes

This work like this on HA

Capture

2 Likes

I had the same problem rebooting every min, dug and looked very intently at ESPhome and the yaml but guess what, the only problem I found was I didn’t add duration times or start times to the sensors so when the time component in ESPhome was looping @1min it would crash. The good part was I took the time to move it away from API: and use MQTT like I wanted. :slightly_smiling_face:

Expanding to 12 zones then maybe I’ll attempt the date thing @raberrio did but I’m dumb and only know English so google translate will be my friend. :crazy_face:

Thanks @BrianHanifin Nice Job!! I read this write up when you did it at first but didn’t want it in the API side but now got what I want just gotta clean up what I did and I’ll share what it is. Not much different!

2 Likes

Jeff i mentioned that crash in my post, according to my research is because you pass the value “now” to the custom function that is waiting a time formatted string.

I fixed it in my code.

good luck

1 Like

Hi Rob! How are you? I have used your code, but I am having problems with the device rebooting and I cannot configure the duration time, it says that it is not available. Could you please indicate which is the change I must make for this to work?
and I also have doubts with the format of the time zone. I am from Argentina
greetings and thank you very much

yalm

# Forked from: https://github.com/bruxy70/Irrigation-with-display
# MIT License: https://github.com/brianhanifin/Irrigation-with-display/blob/45b8e1217af6025e962c00ddd094d06cfc92a993/LICENSE
#
# Credit: @bruxy70 thank you for the significant head start!
# Personal project goals: https://github.com/brianhanifin/Home-Assistant-Config/issues/37
#
substitutions:
  project: Irrigation Controller2
  id: irrigation2

  <<: !include common/substitutions/gpio/sonoff4chpror2.yaml

esphome:
  name: riego_sonoff
  platform: ESP8266
  board: esp01_1m
  includes:
    - irrigation.h



wifi:
  ssid: "user"
  password: "pass"

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Hotspot"
    password: "password"

captive_portal:

<<: !include common/logger.yaml

# Enable Home Assistant API
api:

ota:

time:
  - platform: homeassistant
    id: homeassistant_time
    timezone: UTC-3CDT,M9.1.0/0,M4.1.0/0  
    # 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();
              }

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"

  # ============================================================================= #
  # 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"

binary_sensor:
 - platform: status
   name: Estado $project

switch:
  # ============================================================================= #
  # Virtual Zone Switches which toggle the relay, and store the current state.
  - platform: template
    name: Riego Zona 1
    id: irrigation_zone1
    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: Riego Zona 2
    id: irrigation_zone2
    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

  # ============================================================================= #
  # Relays which trigger solenoids - restore_mode prevents a failure if device resets when switchs are on. I strongly recommend to use it.
  - platform: gpio
    id: relay1
    name: "Relay1"
    pin: GPIO12
    inverted: yes
    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: "ahora"
            # 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"
    pin: GPIO5
    inverted: yes
    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: "ahora"
    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);
                  
sensor:
  - platform: uptime
    name: Controlador Riego Uptime
    unit_of_measurement: h
    filters:
      - lambda: return int((x + 1800.0) / 3600.0);
  - platform: wifi_signal
    name: Controlador Riego Señal WiFi
    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;
  # ============================================================================= #
  # Store durations.
  - platform: template
    name: Duración riego Zona 1
    id: irrigation_zone1_duration
    accuracy_decimals: 0
    unit_of_measurement: min
    icon: mdi:camera-timer
  - platform: template
    name: Duración riego Zona 2
    id: irrigation_zone2_duration
    accuracy_decimals: 0
    unit_of_measurement: min
    icon: mdi:camera-timer
  # ============================================================================= #
  # Countdown sensors.
  - platform: template
    name: Zona 1 tiempo restante
    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: Zona 2 tiempo restante
    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

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;
  # ============================================================================= #
  # Store time lists.
  - platform: template
    name: Zona 1 Horarios
    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: Zona 2 Horarios
    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);
# ============================================================================= #
  # 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;
  # ============================================================================= #
  # Store time lists.
  - platform: template
    name: Zona 1 Días
    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: Zona 2 Días
    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);
  # ============================================================================= #
  # Set the next scheduled time.
  - platform: template
    name: Zona 1 siguiente riego
    id: irrigation_zone1_next
    icon: mdi:calendar-clock
  - platform: template
    name: Zona 2 siguiente riego
    id: irrigation_zone2_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 );
            }
          }

irrigation.h

#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 == "ahora") { //"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 == "Lun" || day == "lun" || day == "LUN") {
      next_wday = 2;
    } else if (day == "Mar" || day == "mar" || day == "MAR") {
      next_wday = 3;
    } else if (day == "Mie" || day == "mie" || day == "MIE") {
      next_wday = 4;
    } else if (day == "Jue" || day == "jue" || day == "JUE") {
      next_wday = 5;
    } else if (day == "Vie" || day == "vie" || day == "VIE") {
      next_wday = 6;
    } else if (day == "Sab" || day == "sab" || day == "SAB") {
      next_wday = 7;
    } else if (day == "Dom" || day == "dom" || day == "DOM") {
      next_wday = 1;
    } else if (day == "Hoy") { //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 == "Lun" || day == "lun" || day == "LUN") {
      next_wday = 2;
    } else if (day == "Mar" || day == "mar" || day == "MAR") {
      next_wday = 3;
    } else if (day == "Mie" || day == "mie" || day == "MIE") {
      next_wday = 4;
    } else if (day == "Jue" || day == "jue" || day == "JUE") {
      next_wday = 5;
    } else if (day == "Vie" || day == "vie" || day == "VIE") {
      next_wday = 6;
    } else if (day == "Sab" || day == "sab" || day == "SAB") {
      next_wday = 7;
    } else if (day == "Dom" || day == "dom" || day == "DOM") {
      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;

}

riego

1 Like

I had the same problem as Joaquin. Looking at it further made me realize you need to define the input fields in your configuration.yaml. Here’s what I created.

input_text:
irrigation_zone1_times:
name: Zone 1 Time Periods
initial: 01:00
irrigation_zone2_times:
name: Zone 2 Time Periods
initial: 01:00
irrigation_zone1_days:
name: Zone 1 Days of the Week
initial: Mon,Wed,Fri
irrigation_zone2_days:
name: Zone 2 Days of the Week
initial: Mon,Wed,Fri

input_number:
irrigation_zone1_duration:
name: Zone 1 Duration
initial: 3
min: 1
max: 30
step: 2
irrigation_zone2_duration:
name: Zone 2 Duration
initial: 3
min: 1
max: 30
step: 2

2 Likes

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;

}
2 Likes

Hi Viking I’ will try later this afternoon.
Thank a lot

HI! I made the corrections of imput_text and imput_um and now the values ​​appear, but I cannot modify them. any ideas?

hassio

Well!! i made it but…timer is work, days i think is working to but time…humm i dont know. it wont start at configured time :cry:

irrigation.h

#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 == "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;
    } 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 = "Today"; //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;

}

sonoff.yalm

# Forked from: https://github.com/bruxy70/Irrigation-with-display
# MIT License: https://github.com/brianhanifin/Irrigation-with-display/blob/45b8e1217af6025e962c00ddd094d06cfc92a993/LICENSE
#
# Credit: @bruxy70 thank you for the significant head start!
# Personal project goals: https://github.com/brianhanifin/Home-Assistant-Config/issues/37
#
substitutions:
  project: Irrigation Controller2
  id: irrigation2

  <<: !include common/substitutions/gpio/sonoff4chpror2.yaml

esphome:
  name: riego_sonoff
  platform: ESP8266
  board: esp01_1m
  includes:
    - irrigation.h



wifi:
  ssid: "ssid"
  password: "password"

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: " Fallback Hotspot"
    password: "password"

captive_portal:

<<: !include common/logger.yaml

# Enable Home Assistant API
api:

ota:

time:
  - platform: homeassistant
    id: homeassistant_time
    timezone: America/Argentina/Buenos_Aires
    # 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();
              }

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"

binary_sensor:
 - platform: status
   name: Estado $project

switch:
  # ============================================================================= #
  # Virtual Zone Switches which toggle the relay, and store the current state.
  - platform: template
    name: Riego Zona 1
    id: irrigation_zone1
    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: Riego Zona 2
    id: irrigation_zone2
    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: Riego Zona 3
    id: irrigation_zone3
    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

  # ============================================================================= #
  # Relays which trigger solenoids - restore_mode prevents a failure if device resets when switchs are on. I strongly recommend to use it.
  - platform: gpio
    id: relay1
    name: "Relay1"
    pin: GPIO12
    inverted: no
    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"
    pin: GPIO5
    inverted: no
    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"
    pin: GPIO4
    inverted: no
    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"
            # state NOW on original code, change to your preferred language
    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: Controlador Riego Uptime
    unit_of_measurement: h
    filters:
      - lambda: return int((x + 1800.0) / 3600.0);
  - platform: wifi_signal
    name: Controlador Riego Señal WiFi
    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: Duración riego Zona 1
    id: irrigation_zone1_duration
    accuracy_decimals: 0
    unit_of_measurement: min
    icon: mdi:camera-timer
  - platform: template
    name: Duración riego Zona 2
    id: irrigation_zone2_duration
    accuracy_decimals: 0
    unit_of_measurement: min
    icon: mdi:camera-timer
  - platform: template
    name: Duración riego Zona 3
    id: irrigation_zone3_duration
    accuracy_decimals: 0
    unit_of_measurement: min
    icon: mdi:camera-timer    
  # ============================================================================= #
  # Countdown sensors.
  - platform: template
    name: Zona 1 tiempo restante
    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: Zona 2 tiempo restante
    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: Zona 3 tiempo restante
    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: Zona 1 Horarios
    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: Zona 2 Horarios
    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: Zona 3 Horarios
    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: Zona 1 Días
    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: Zona 2 Días
    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: Zona 3 Días
    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: Zona 1 siguiente riego
    id: irrigation_zone1_next
    icon: mdi:calendar-clock
  - platform: template
    name: Zona 2 siguiente riego
    id: irrigation_zone2_next
    icon: mdi:calendar-clock
  - platform: template
    name: Zona 3 siguiente riego
    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_time2);
            if (id(relay3).state) {
              id(remaining_time3) -= 5;
              if (id(remaining_time2) <= 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 );
            }
          }

and config.yalm

input_text:
  irrigation_zone1_times:
    name: Horarios Zona 1
    initial: 01:00
  irrigation_zone2_times:
    name: Horarios Zona 2
    initial: 01:00
  irrigation_zone3_times:
    name: Horarios Zona 3
    initial: 01:00
  irrigation_zone1_days:
    name: Dias de la Semana Zona 1
    initial: Mon,Wed,Fri
  irrigation_zone2_days:
    name: Dias de la Semana Zona 2
    initial: Mon,Wed,Fri
  irrigation_zone3_days:
    name: Dias de la Semana Zona 3
    initial: Mon,Wed,Fri

input_number:
  irrigation_zone1_duration:
    name: Duración Zona 1
    initial: 3
    min: 1
    max: 30
    step: 2
    mode: slider
  irrigation_zone2_duration:
    name: Duración Zona 2
    initial: 3
    min: 1
    max: 30
    step: 2
    mode: slider
  irrigation_zone3_duration:
    name: Duración Zona 3
    initial: 3
    min: 1
    max: 30
    step: 2
    mode: slider

I suspect your problem might be the timezone setting. I had the same initial problem but changed mine to timezone: EST+5EDT,M3.2.0/2,M11.1.0/2 (I updated my previous post).

I referred to the ESPHOME Documentation and it says this,

  • timezone ( Optional , string): Manually tell ESPHome what time zone to use with this format (warning: the format is quite complicated) or the simpler TZ database name in the form /. ESPHome tries to automatically infer the time zone string based on the time zone of the computer that is running ESPHome, but this might not always be accurate.

I suspect you could comment it out so it picks it up from HA.

A couple of things I realized when testing this out. I put initialize values on the input fields that I defined in the config. Not a good idea as these get set every time you restart HA. I also changed a few ICONS to make things a little easier to understand. Here’s an example of my display in HA.

2 Likes

Hi Viking! I realized that the values ​​I was entering in the days field were in English and I configured it in Spanish, by mistake it never started :blush: . So now it works :smiley: .

For the record, time_zone was corrected too.

time:
  - platform: homeassistant
    id: homeassistant_time
    timezone: America/Buenos_Aires

But … How did you solve the problem that the fields were not saved when the HA restarts? Same here :frowning:

Once again…THANKS

Hi it´s me again, well…i remove initials values and it worked!

Do you mind share the entities card code and where did you change the icons?

thanks again

Here’s my Lovelace YAML for Zone 2

type: vertical-stack
cards:
  - type: entities
    entities:
      - entity: input_text.irrigation_zone2_days
      - entity: input_text.irrigation_zone2_times
      - entity: input_number.irrigation_zone2_duration
      - entity: sensor.zone_2_next_watering
    title: Backyard Zone 2 - Schedule
  - type: entities
    entities:
      - entity: switch.irrigation_zone_2
        secondary_info: last-changed
        name: Zone 2 Watering
    title: Backyard Zone 2 - Watering
  - type: history-graph
    entities:
      - entity: switch.irrigation_zone_2
    hours_to_show: 24
    refresh_interval: 0

On the input parameters you can define a icon see my example below.

input_text:
  irrigation_zone2_times:
    name: Zone 2 Time Periods
    icon: mdi:chart-timeline
  irrigation_zone1_days:
    name: Zone 1 Days of the Week
    icon: mdi:calendar-week

input_number:
  irrigation_zone2_duration:
    name: Zone 2 Duration
    icon: mdi:timer
    min: 2
    max: 60
    step: 2  

Within the ESPHOME Irrigation Yaml you can also define ICONS, see example

  - 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
1 Like