ESPHome: DIY Irrigation Controller With Internal Scheduler

Thanks @BrianHanifin, @raberrio, @VikingBlod @joaquin68 and everyone in this thread for putting this together!
I took the code from @joaquin68 here, modified to use 5 zones on an ESP32 (I tried with 5 zones on ESP8266 but it kept crashing - memory issue maybe? - the ESP32 works like a treat though on the same code). The irrigation.h include doesn’t need to be modified beyond language changes.

I’m using 5 zones and made a more minimalist interface (inspired by @VikingBlod and @bremby) for use on my phone that I thought I’d share.

I also made some changes to the code to get some visual feedback for disabling the irrigation schedule and moved the check for disabling to the scheduler rather than the switch itself so that they can still be turned on manually if they are ‘disabled’.

Here’s my card working. It includes two custom (although fairly common and amazing) mods by @thomasloven to make it compact and pretty - template-entity-row and fold-entity-row. The only issue is that adding buttons with tap-action defined seems to break the visual editor at the moment (I opened a front-end issue), so I needed to set up the card, then add in the tap_action in the raw configuration editor. Despite this, the buttons function perfectly, however.

Below I will document my changes for anyone interested. I also noticed that during testing you need to be careful to make sure that your list of days and times need to be in order for things to work as expected. You need to start at Sunday and times must be earliest to latest for the parsing and schedule to work properly.

This is my card config to achieve the above:

type: entities
title: Irrigation Controls
show_header_toggle: false
entities:
  - type: custom:template-entity-row
    icon: mdi:sprinkler-variant
    name: Irrigation System Status
    entity: binary_sensor.irrigation_controller_status
    state: >-
      {% if is_state('binary_sensor.irrigation_controller_status', 'off')
      %}OFFLINE {% else %} {% if is_state('switch.irrigation_zone_5', 'on') or
      is_state('switch.irrigation_zone_4', 'on') or
      is_state('switch.irrigation_zone_3', 'on') or
      is_state('switch.irrigation_zone_2', 'on') or
      is_state('switch.irrigation_zone_1', 'on') or
      is_state('script.water_everything', 'on') or
      is_state('script.water_front_yard', 'on') or
      is_state('script.water_back_yard', 'on') %}ACTIVE{% else %}IDLE {% endif
      %} {% endif %}
    active: >-
      {% if is_state('switch.irrigation_zone_5', 'on') or
      is_state('switch.irrigation_zone_4', 'on') or
      is_state('switch.irrigation_zone_3', 'on') or
      is_state('switch.irrigation_zone_2', 'on') or
      is_state('switch.irrigation_zone_1', 'on') or
      is_state('script.water_everything', 'on') or
      is_state('script.water_front_yard', 'on') or
      is_state('script.water_back_yard', 'on') %}true{% endif %}
    secondary: >-
      {% if is_state('switch.irrigation_zone_1', 'on') %}Zone 1 has
      {{states('sensor.zone_1_time_remaining')}} min remaining{% endif %} {% if
      is_state('switch.irrigation_zone_2', 'on') %}Zone 2 has
      {{states('sensor.zone_2_time_remaining')}} min remaining{% endif %} {% if
      is_state('switch.irrigation_zone_3', 'on') %}Zone 3 has
      {{states('sensor.zone_3_time_remaining')}} min remaining{% endif %} {% if
      is_state('switch.irrigation_zone_4', 'on') %}Zone 4 has
      {{states('sensor.zone_4_time_remaining')}} min remaining{% endif %} {% if
      is_state('switch.irrigation_zone_5', 'on') %}Zone 5 has
      {{states('sensor.zone_5_time_remaining')}} min remaining{% endif %}
  - type: divider
  - entity: switch.irrigation_zone_1
    secondary_info: last-changed
    name: Zone 1
  - type: custom:fold-entity-row
    head:
      entity: sensor.zone_1_next_watering
      name: Zone 1 Next Run
    entities:
      - input_text.irrigation_zone1_days
      - input_text.irrigation_zone1_times
      - input_number.irrigation_zone1_duration
      - sensor.zone_1_time_remaining
      - type: buttons
        entities:
          - entity: input_boolean.irrigation_disable_schedule_zone1
            name: Disable Zone 1 Schedule
  - entity: switch.irrigation_zone_2
    secondary_info: last-changed
    name: Zone 2
  - type: custom:fold-entity-row
    head:
      entity: sensor.zone_2_next_watering
      name: Zone 2 Next Run
    entities:
      - input_text.irrigation_zone2_days
      - input_text.irrigation_zone2_times
      - input_number.irrigation_zone2_duration
      - sensor.zone_2_time_remaining
      - type: buttons
        entities:
          - entity: input_boolean.irrigation_disable_schedule_zone2
            name: Disable Zone 2 Schedule
  - entity: switch.irrigation_zone_3
    secondary_info: last-changed
    name: Zone 3
  - type: custom:fold-entity-row
    head:
      entity: sensor.zone_3_next_watering
      name: Zone 3 Next Run
    entities:
      - input_text.irrigation_zone3_days
      - input_text.irrigation_zone3_times
      - input_number.irrigation_zone3_duration
      - sensor.zone_3_time_remaining
      - type: buttons
        entities:
          - entity: input_boolean.irrigation_disable_schedule_zone3
            name: Disable Zone 3 Schedule
  - entity: switch.irrigation_zone_4
    secondary_info: last-changed
    name: Zone 4
  - type: custom:fold-entity-row
    head:
      entity: sensor.zone_4_next_watering
      name: Zone 4 Next Run
    entities:
      - input_text.irrigation_zone4_days
      - input_text.irrigation_zone4_times
      - input_number.irrigation_zone4_duration
      - sensor.zone_4_time_remaining
      - type: buttons
        entities:
          - entity: input_boolean.irrigation_disable_schedule_zone4
            name: Disable Zone 4 Schedule
  - entity: switch.irrigation_zone_5
    secondary_info: last-changed
    name: Zone 5
  - type: custom:fold-entity-row
    head:
      entity: sensor.zone_5_next_watering
      name: Zone 5 Next Run
    entities:
      - input_text.irrigation_zone5_days
      - input_text.irrigation_zone5_times
      - input_number.irrigation_zone5_duration
      - sensor.zone_5_time_remaining
      - type: buttons
        entities:
          - entity: input_boolean.irrigation_disable_schedule_zone5
            name: Disable Zone 5 Schedule
  - type: divider
  - type: buttons
    entities:
      - entity: script.water_everything
        name: All Zones
        tap_action:
          action: toggle
      - entity: script.water_front_yard
        name: Front Yard
        tap_action:
          action: toggle
      - entity: script.water_back_yard
        name: Back Yard
        tap_action:
          action: toggle
  - type: divider
  - type: buttons
    entities:
      - entity: scene.once_daily_irrigation
        name: Once Daily Irrigation
        tap_action:
          action: call-service
          service: scene.turn_on
          service_data:
            entity_id: scene.once_daily_irrigation
      - entity: scene.twice_daily_irrigation
        name: Twice Daily Irrigation
        tap_action:
          action: call-service
          service: scene.turn_on
          service_data:
            entity_id: scene.twice_daily_irrigation
  - type: divider
  - type: buttons
    entities:
      - entity: switch.irrigation_disable_schedule_all_zones
        name: Disable Schedules
        icon: mdi:timer-off-outline
      - entity: switch.stop_active_irrigation_zones
        name: Stop Irrigating
        icon: mdi:close-octagon-outline

At the top, you have a status panel that just shows whether irrigation is active, idle or offline based on templates. (looking at this now, I might add another status if all zone schedules are disabled). I included my scripts in there to prevent the status from flashing between zones and also to remind me if I turn off a valve and the script is still running.

For each zone, I set a toggle then a folded row to hold the configuration and disable button for that specific zone.

The disable button is a helper input_boolean input_boolean.irrigation_disable_schedule_zone1 that I use for the ESP to read and disable the schedule (not the zone switch). This also allows me to change the text to “Schedule Disabled” based upon that. To achieve this, I imported these to the ESP and changed the scheduler to check this before initiating a zone.

Import the input_boolean disable switch and also use it also to update the next runtime text:

binary_sensor:
  # ============================================================================= #
  # Bring input_boolean buttons from HA to disable schedules
  - platform: homeassistant
    id: ui_disable_zone1
    entity_id: input_boolean.irrigation_disable_schedule_zone1
    on_state:
      then:
        # Update the next scheduled run time.
        - text_sensor.template.publish:
            id: irrigation_zone1_next
            state: !lambda |-
              if (id(ui_disable_zone1).state) {
                return {"Schedule Disabled"};
              } else {
                return update_next_runtime(id(irrigation_zone1_times).state, id(irrigation_zone1_days).state);
              }

# and repeat for each zone

My ESP’s time: block looks like this now to take advantage of the disabled schedule input:

time:
  - platform: homeassistant
    id: homeassistant_time
    # Time based automations.
    on_time:
      - seconds: 0
        minutes: /1
        then:
          - if:
              condition:
                - binary_sensor.is_off: ui_disable_zone1
              then:
                - lambda: |-
                    if (scheduled_runtime(id(irrigation_zone1_next).state.c_str())) {
                      id(irrigation_zone1).turn_on(); }

# and repeat for each zone...

Also make sure the disabled text doesn’t get updated when you change the days, times, or turn off a manual irrigation run:

DAYS:

  # ============================================================================= #
  # Store day 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 |-
              if (id(ui_disable_zone1).state) {
                return {"Schedule Disabled"};
              }
              else {
              return update_next_runtime(id(irrigation_zone1_times).state, id(irrigation_zone1_days).state);
              }

# and repeat for each zone...

TIMES:

  # ============================================================================= #
  # 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 |-
              if (id(ui_disable_zone1).state) {
                return {"Schedule Disabled"};
              } else {
                return update_next_runtime(id(irrigation_zone1_times).state, id(irrigation_zone1_days).state);
              }

Update at end of a manual run with the changes in on_turn_off for your gpio switch:

  - platform: gpio
    id: relay1
    name: "Irrigation Zone 1 Relay"
.
.
.
    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 |- 
              if (id(ui_disable_zone1).state) {
                return {"Schedule Disabled"};
              } else {
                return update_next_runtime(id(irrigation_zone1_times).state, id(irrigation_zone1_days).state);
              }

It’s all the same lambda, just in the three spots that update the next run sensor.

Next, with the “All Zones” / “Front Yard” / “Back Yard” buttons, I set my zone cycle scripts to use the set values for the zones using delay templates, like this:

alias: Water Everything
mode: single
icon: mdi:sprinkler
sequence:
  - service: switch.turn_on
    target:
      entity_id: switch.irrigation_zone_1
  - delay:
      minutes: '{{ states(''input_number.irrigation_zone1_duration'') | int }}'
  - service: switch.turn_off
    target:
      entity_id: switch.irrigation_zone_1
  - service: switch.turn_on
    target:
      entity_id: switch.irrigation_zone_2
  - delay:
      minutes: '{{ states(''input_number.irrigation_zone2_duration'') | int }}'
  - service: switch.turn_off
    target:
      entity_id: switch.irrigation_zone_2

# and repeat for each zone...

For a stop irrigation switch, I added a toggle like @bremby did here but with a shorter turn off delay of 2s instead of 10.

Then, for the disable all schedules toggle, I created a switch that toggles all the input_booleans

switch:
  - platform: template
    switches:
      irrigation_disable_schedule_all_zones:
        friendly_name: Disable All Irrigation Schedules
        icon_template: mdi:timer-off-outline
        turn_on:
          service: input_boolean.turn_on
          target:
            entity_id:
              - input_boolean.irrigation_disable_schedule_zone1
              - input_boolean.irrigation_disable_schedule_zone2
              - input_boolean.irrigation_disable_schedule_zone3
              - input_boolean.irrigation_disable_schedule_zone4
              - input_boolean.irrigation_disable_schedule_zone5
        turn_off:
          service: input_boolean.turn_off
          target:
            entity_id:
              - input_boolean.irrigation_disable_schedule_zone1
              - input_boolean.irrigation_disable_schedule_zone2
              - input_boolean.irrigation_disable_schedule_zone3
              - input_boolean.irrigation_disable_schedule_zone4
              - input_boolean.irrigation_disable_schedule_zone5

And lastly, I created scenes with my usual times, days and durations to easily toggle between once and twice daily irrigation, but could be whatever you wanted - spring, summer, fall…

- id: '1234565278981'
  name: Twice Daily Irrigation
  icon: mdi:numeric-2-box-multiple-outline
  entities:
    input_text.irrigation_zone1_days:
      editable: false
      min: 0
      max: 100
      pattern:
      mode: text
      friendly_name: Zone 1 Days
      icon: mdi:calendar-week
      state: Sun,Mon,Tue,Wed,Thu,Fri,Sat
    input_text.irrigation_zone1_times:
      editable: false
      min: 0
      max: 100
      pattern:
      mode: text
      friendly_name: Zone 1 Times
      icon: mdi:chart-timeline
      state: 05:00,20:00
    input_text.irrigation_zone2_days:
      editable: false
      min: 0
      max: 100
      pattern:
      mode: text
      friendly_name: Zone 2 Days
      icon: mdi:calendar-week
      state: Sun,Mon,Tue,Wed,Thu,Fri,Sat
    input_text.irrigation_zone2_times:
      editable: false
      min: 0
      max: 100
      pattern:
      mode: text
      friendly_name: Zone 2 Times
      icon: mdi:chart-timeline
      state: 05:10,20:10

# and repeat for each zone...

Again, thanks to everyone in this thread! Next step is just some logic to disable all zones based upon rain today or tomorrow. Will probably start with ideas from these threads here or here

EDIT - also thinking that instead of just disabling the irrigation, I can use rainfall thresholds to set a scene with different durations as well… Just a thought I need to write down before I forget :slight_smile:

Cheers!

4 Likes