ESPHome: DIY Irrigation Controller With Internal Scheduler

@VikingBlod , do you mean something like this

input_text:
  ##Time Schedule
  irrigation_zone_1_times:
    name: Time Schedule - Zone 1
    icon: mdi:chart-timeline
    mode: text
    initial: 01:00

Yes and duration

input_number:
  irrigation_zone1_duration:
    name: Zone 1 Duration
    icon: mdi:timer
    min: 0
    max: 60
    step: 2  

the field is still initial?

@Folks, I found the issue !!!

I didnā€™t add the ESPHome integration as part of my restore :frowning:

I spent 2 days of desperate tries and the issue wasnā€™t related to the config. Definitely I have to write better notes

Thank you all for your support and ā€¦ patience

1 Like

Hey guys, is there a wiring guide somewhere for this project?

I have the unit up and running but havenā€™t yet wired in the actual solenoids yet. Iā€™ve got the solenoids and the 24v ac supply but Iā€™m not sure about the how to connect them.

Any tips or a diagram about that anywhere handy?

Hey guys, I used the tutorial here to work out the wiring. Thought Iā€™d post the link here in case anyone else comes lookingā€¦

1 Like

I had a similar problem, though mine didnā€™t reboot, just disconnected every minute, then reconnected. Didnā€™t know what was happening till I saw your post.

Has anyone implemented a ā€˜rain delayā€™ so it doesnā€™t water if the forecast predicts rain?

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

@sreknob What happens in your code with the ā€˜zoneX_remainingā€™ when a watering session is running while the ESP is reset? If I put @BrianHanifinā€™s code on an ESP32, the remaining time goes to indefinite after a power-cycle or reset, which is the last thing you want for an irrigation system. I saw there was a issue about this problem opened on github as well.

Hi guyā€™s
Iā€™ve tried several options but I canā€™t run the scheduleā€¦

  • i have a sonoff 4ch r2
  • Iā€™m mainly using @joaquin68 configuration with 3 zones
  • Iā€™ve translated into italian both esp yaml and irrigation.h
  • Iā€™v changed the word ā€œHoyā€ to ā€œOggā€ (Oggi it is the Italian word for Today)
  • If manually turned on every zones work.
    Looking at esphome logs everythings seem to work except for this message i canā€™t understand.

The result is that if i set a schedule, the watering wonā€™t startā€¦
This is how my lovelace look.

Any help would be really appreciatedā€¦

Discussion of your issue is continued on Github.

Hey @Ticktock - From a functional point, I donā€™t see the same issue. I just tried now to reset the controller with a scheduled zone running and the zone turned off during the reset and stayed off. The time_remaining was 0 when it came back up. Iā€™m also running it on an ESP32. Mind you, my board is outside so I didnā€™t serial log it to see specifics, just looked at my homeassistant sensors.

Looking at the logs posted for this issue on github, I wonder if the issue is that the setup of the ESP is not done and the interval: code starts to run and interprets the null value wrong.

I suppose to see if this is the cause, if you either change the interval to a longer duration (say 10 seconds rather than 5 seconds) OR set the interval only to run after the esphome is fully set up (by adding a condition that esphome is fully set up) that it might solve this behaviour?

We donā€™t have access to the boot loop counter, but if you add a global to be set after setup is done, then use it as a condition for the interval, it should have a similar effect.

esphome:
  on_boot:
    priority: -100
    then:
      - logger.log:
          format: "Finished booting, now enabling countdown timers"
          level: INFO
      - globals.set:
          id: finished_booting
          value: 'true'

and

globals:
  - id: finished_booting
    type: bool
    restore_value: no
    initial_value: 'false'

then

interval:
  - interval: 5s
    then:
      - if:
          condition:
            lambda: return id(finished_booting);
          then:
            - lambda: |-
# and your countdown lambda....

then maybe it might correct this?

If it does, then it probably should be reported to the esphome project to adjust the node setup to avoid triggering intervals (only an issue when the interval is very short) before processing it.

Just throwing some ideas out there! Iā€™ll try and setup another board and see if I can reproduce.

@Lucabiri Looking at your history graph, it appears that your ESP is rebooting constantly.

You should double check your config and the schedule sensors helpers you have in HA.

If everything looks right to you, post your includes and configsā€¦

Thank you for your help! I tried it with a 10 and 15 second interval but that gives te same behaviour. This is so weird.

Hi @sreknob , awesome job customizing the Irrigation Controls card. This is my first attempt at creating something with Home Assistant. I used your changes to configure my ESP8266 and I am able to turn my irrigation zones on and off manually, set the day and time, but due to the fact that I am unsure where to use the code for ā€œAll Zonesā€ / ā€œFront Yardā€ / ā€œBack Yardā€ buttons, the switch code to toggle the input_booleans and the scenes code you created these items are not working on the card. Any guidance will be appreciated.

Hi @Leon137 - glad youā€™re found it useful. Those buttons I documented in my post are just to call scripts that run the sequence of the zones in case I want to start a manual run. They use the same durations as are set for each zone but ignore the fact that the schedule is disabled as it calls the zones from Home Assistant. See below for the script to set in Home Assistant. Let me know if you need more specifics!

Hi - it looks like there have been quite a few improvements to Brians initial work. Does anyone have a updated set of files on Github or elsewhere to share? I am more interested in the ESPHome config at first, but I am sure the Lovelace config will be next!

Thanks, Andy

Hi @qurm - feel free to poke around my configs on github here.
Just be aware that I make use of packages with much of the mundane configs in a common folder to avoid having to change things in multiple places.
Good luck!

1 Like

HI a Big Thanks To Brian and raberrio
I have just one issue It seems like the ESP 32 is resetting after 60 seconds (After trigger) and on a manual trigger even faster. Did add "initial: " to my irrigation.yaml file but it doesnt seem to make any differance .
Any suggestions where I might look?
Regards

Hi @ockie try this https://community.home-assistant.io/t/esphome-diy-irrigation-controller-with-internal-scheduler/171844/41?u=joaquin68

Be carefull when using Tod as today or Hoy, today in spanish