ESPHome Smart Irrigation

Image by Hamed Taha | Unsplash

When I installed an irrigation system in my yard quite some years ago it was clear to me that I wasn’t going with the “official” controller. It was expensive, didn’t come with WiFi out of the box (that would have cost a hefty premium), thus was difficult or even impossible to integrate and it just wasn’t flexible enough.

Here’s my setup:

  • 8 Valves
  • 13 sprinklers
  • 2 drip hoses
  • 5 zones each comprised of one or more sprinklers, hoses or a combination

In addition I wanted an automation to iterate over the zones in a predefined order with predefined timings. Given those requirements, I then settled for 2 Sonoff 4CH Pro R2 flashed with Tasmota which worked flawlessly ever since. However, time has passed and the Smart Home landscape has evolved. This led me to re-evaluate my setup and I came to the conclusion that ESPHome was now easy and yet powerful enough to replace Tasmota on all my devices with it.

The Software

ESPHome is absolutely fantastic both for its simplicity, flexibility and not least its declarative nature.

Even though my architecture up to this point worked quite well it was essentially built as a collection of helpers which were all over the place and most of all lacked a critical feature: I built an automation for each valve to force-stop it after a certain time. Both as a way to just start a valve and have it shut down automatically, but also as sort of a safety if something went wrong with the automations to not have them running indefinitely.

This led to an interesting problem, though: What if Home Assistant triggered a valve but then restarted and lost track of what was going on? I fixed this by introducing another Automation to stop all valves on Home Assistant startup. But what if Home Assistant did not restart but stayed offline? To be honest, this never happened during sprinkler runtime, but there’s always the possibility and I didn’t have an answer for it - until I discovered ESPHome. This made it possible to have most of the logic on the board itself, so connectivity issues don’t pose a threat anymore.

So I went on, installed the configuration from ESPHome’s library and all was well. But then I discovered the sprinkler controller and it blew my mind! All those years I’ve been creating various Automations, Scripts and other helpers in Home Assistant to provide good control over the individual valves as well as the automated iteration. The sprinkler controller looked like everything I’ve manually built and fine tuned over the years with some additional perks like the option to dynamically set the sprinkler runtime. And what’s more is that it does all of this in a much nicer package with even more possibilities - or so I thought.

Using the built-in sprinkler component

Let’s have a look at my initial attempt:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
esphome:
  name: irrigation
  friendly_name: Irrigation

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: 'my-key'

ota:
  platform: esphome
  password: 'my-password'

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

# Device Specific Config
binary_sensor:
  - platform: status
    name: 'Status'

sensor:
  - platform: wifi_signal
    name: "WiFi Signal Sensor"
    update_interval: 60s

sprinkler:
  # Valve controller
  - id: valve_controller
    main_switch: 'Alle Sprenger'
    auto_advance_switch: 'Alle Sprenger Automatik'
    valve_open_delay: 5s
    valves:
      - valve_switch: 'Sprenger 1'
        enable_switch: 'Sprenger 1 aktiv'
        run_duration_number:
          name: 'Sprenger 1 Dauer'
          initial_value: 20
          unit_of_measurement: min
          min_value: 1
          max_value: 60
        valve_switch_id: valve_1
      - valve_switch: 'Sprenger 2'
        enable_switch: 'Sprenger 2 aktiv'
        run_duration_number:
          name: 'Sprenger 2 Dauer'
          initial_value: 20
          unit_of_measurement: min
          min_value: 1
          max_value: 60
        valve_switch_id: valve_2
      - valve_switch: 'Sprenger 3'
        enable_switch: 'Sprenger 3 aktiv'
        run_duration_number:
          name: 'Sprenger 3 Dauer'
          initial_value: 20
          unit_of_measurement: min
          min_value: 1
          max_value: 60
        valve_switch_id: valve_3
      - valve_switch: 'Sprenger 4'
        enable_switch: 'Sprenger 4 aktiv'
        run_duration_number:
          name: 'Sprenger 4 Dauer'
          initial_value: 20
          unit_of_measurement: min
          min_value: 1
          max_value: 60
        valve_switch_id: valve_4
      - valve_switch: 'Sprenger 5'
        enable_switch: 'Sprenger 5 aktiv'
        run_duration_number:
          name: 'Sprenger 5 Dauer'
          initial_value: 20
          unit_of_measurement: min
          min_value: 1
          max_value: 60
        valve_switch_id: valve_5
      - valve_switch: 'Sprenger 6'
        enable_switch: 'Sprenger 6 aktiv'
        run_duration_number:
          name: 'Sprenger 6 Dauer'
          initial_value: 20
          unit_of_measurement: min
          min_value: 1
          max_value: 60
        valve_switch_id: valve_6
      - valve_switch: 'Sprenger 7'
        enable_switch: 'Sprenger 7 aktiv'
        run_duration_number:
          name: 'Sprenger 7 Dauer'
          initial_value: 20
          unit_of_measurement: min
          min_value: 1
          max_value: 60
        valve_switch_id: valve_7
      - valve_switch: 'Sprenger 8'
        enable_switch: 'Sprenger 8 aktiv'
        run_duration_number:
          name: 'Sprenger 8 Dauer'
          initial_value: 20
          unit_of_measurement: min
          min_value: 1
          max_value: 60
        valve_switch_id: valve_8

  # Zone controller
  - id: zone_controller
    main_switch: 'Alle Zonen'
    auto_advance_switch: 'Alle Zonen Automatik'
    valve_open_delay: 5s
    valves:
      - valve_switch: 'Zone 1'
        enable_switch: 'Zone 1 aktiv'
        run_duration_number:
          name: 'Zone 1 Dauer'
          initial_value: 15
          unit_of_measurement: min
          min_value: 1
          max_value: 60
        valve_switch_id: zone_1
      - valve_switch: 'Zone 2'
        enable_switch: 'Zone 2 aktiv'
        run_duration_number:
          name: 'Zone 2 Dauer'
          initial_value: 15
          unit_of_measurement: min
          min_value: 1
          max_value: 60
        valve_switch_id: zone_2
      - valve_switch: 'Zone 3'
        enable_switch: 'Zone 3 aktiv'
        run_duration_number:
          name: 'Zone 3 Dauer'
          initial_value: 10
          unit_of_measurement: min
          min_value: 1
          max_value: 60
        valve_switch_id: zone_3
      - valve_switch: 'Zone 4'
        enable_switch: 'Zone 4 aktiv'
        run_duration_number:
          name: 'Zone 4 Dauer'
          initial_value: 15
          unit_of_measurement: min
          min_value: 1
          max_value: 60
        valve_switch_id: zone_4
      - valve_switch: 'Zone 5'
        enable_switch: 'Zone 5 aktiv'
        run_duration_number:
          name: 'Zone 5 Dauer'
          initial_value: 30
          unit_of_measurement: min
          min_value: 1
          max_value: 60
        valve_switch_id: zone_5

switch:
  # Individual valves
  - platform: gpio
    id: valve_1
    name: 'Sprenger 1'
    icon: 'mdi:sprinkler'
    pin: GPIO16
    on_turn_on:
      - delay: 60min
      - switch.turn_off: valve_1
  - platform: gpio
    id: valve_2
    name: 'Sprenger 2'
    icon: 'mdi:sprinkler'
    pin: GPIO14
    on_turn_on:
      - delay: 60min
      - switch.turn_off: valve_2
  - platform: gpio
    id: valve_3
    name: 'Sprenger 3'
    icon: 'mdi:sprinkler'
    pin: GPIO12
    on_turn_on:
      - delay: 60min
      - switch.turn_off: valve_3
  - platform: gpio
    id: valve_4
    name: 'Sprenger 4'
    icon: 'mdi:sprinkler'
    pin: GPIO13
    on_turn_on:
      - delay: 60min
      - switch.turn_off: valve_4
  - platform: gpio
    id: valve_5
    name: 'Sprenger 5'
    icon: 'mdi:sprinkler'
    pin: GPIO15
    on_turn_on:
      - delay: 60min
      - switch.turn_off: valve_5
  - platform: gpio
    id: valve_6
    name: 'Sprenger 6'
    icon: 'mdi:sprinkler'
    pin: GPIO0
    on_turn_on:
      - delay: 60min
      - switch.turn_off: valve_6
  - platform: gpio
    id: valve_7
    name: 'Sprenger 7'
    icon: 'mdi:sprinkler'
    pin: GPIO4
    on_turn_on:
      - delay: 60min
      - switch.turn_off: valve_7
  - platform: gpio
    id: valve_8
    name: 'Sprenger 8'
    icon: 'mdi:sprinkler'
    pin: GPIO5
    on_turn_on:
      - delay: 60min
      - switch.turn_off: valve_8

  # Valves grouped in zones
  - platform: template
    id: zone_1
    name: 'Zone 1'
    lambda: |-
      return id(valve_1).state || id(valve_8).state;      
    turn_on_action:
      - switch.turn_on: valve_1
      - switch.turn_on: valve_8
    turn_off_action:
      - switch.turn_off: valve_1
      - switch.turn_off: valve_8
  - platform: template
    id: zone_2
    name: 'Zone 2'
    lambda: |-
      return id(valve_2).state;      
    turn_on_action:
      - switch.turn_on: valve_2
    turn_off_action:
      - switch.turn_off: valve_2
  - platform: template
    id: zone_3
    name: 'Zone 3'
    lambda: |-
      return id(valve_3).state;      
    turn_on_action:
      - switch.turn_on: valve_3
    turn_off_action:
      - switch.turn_off: valve_3
  - platform: template
    id: zone_4
    name: 'Zone 4'
    lambda: |-
      return id(valve_4).state || id(valve_5).state;      
    turn_on_action:
      - switch.turn_on: valve_4
      - switch.turn_on: valve_5
    turn_off_action:
      - switch.turn_off: valve_4
      - switch.turn_off: valve_5
  - platform: template
    id: zone_5
    name: 'Zone 5'
    lambda: |-
      return id(valve_6).state || id(valve_7).state;      
    turn_on_action:
      - switch.turn_on: valve_6
      - switch.turn_on: valve_7
    turn_off_action:
      - switch.turn_off: valve_6
      - switch.turn_off: valve_7

Below the regular “framework” at the top there’s the sprinkler controllers. First one for all individual valves, then another for the zones. The valve switches all directly link to their corresponding GPIO switch while the zone switches link to template switches I used to group valves together.

At first this looked really nice but the closer I looked the more it became clear that this was not very well suited for my needs.

The zone switches for example were a necessary “hack” to circumvent the sprinkler controller itself not being able to operate multiple switches in parallel.

Another drawback was the fact that even with those virtual zone switches you couldn’t stop one valve in a zone without stopping all valves in that zone. Since we often need this feature for various reasons (e.g. having laundry in reach of a particular sprinkler) this wasn’t feasible. There were many minor details about this approach that didn’t quite fit which caused me to give it a complete overhaul.

All manual

Determined to get this up and running with ESPHome I invested many more hours into crafting the following:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
esphome:
  name: irrigation
  friendly_name: Irrigation

esp32:
  board: esp32dev
  framework:
    type: esp-idf

substitutions:
  max_runtime: '60'
  valve_open_delay: 5s

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: 'my-key'

ota:
  platform: esphome
  password: 'my-password'

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

binary_sensor:
  - platform: status
    name: 'Status'

sensor:
  - platform: wifi_signal
    name: "WiFi Signal Sensor"
    update_interval: 60s

script:
  # Execution helpers to allow for delays to be cancelled
  - id: valve_1_on_off
    then:
      - switch.turn_on: valve_1
      - delay: !lambda "return id(valve_1_duration).state * 1000 * 60;"
      - switch.turn_off: valve_1
  - id: valve_2_on_off
    then:
      - switch.turn_on: valve_2
      - delay: !lambda "return id(valve_2_duration).state * 1000 * 60;"
      - switch.turn_off: valve_2
  - id: valve_3_on_off
    then:
      - switch.turn_on: valve_3
      - delay: !lambda "return id(valve_3_duration).state * 1000 * 60;"
      - switch.turn_off: valve_3
  - id: valve_4_on_off
    then:
      - switch.turn_on: valve_4
      - delay: !lambda "return id(valve_4_duration).state * 1000 * 60;"
      - switch.turn_off: valve_4
  - id: valve_5_on_off
    then:
      - switch.turn_on: valve_5
      - delay: !lambda "return id(valve_5_duration).state * 1000 * 60;"
      - switch.turn_off: valve_5
  - id: valve_6_on_off
    then:
      - switch.turn_on: valve_6
      - delay: !lambda "return id(valve_6_duration).state * 1000 * 60;"
      - switch.turn_off: valve_6
  - id: valve_7_on_off
    then:
      - switch.turn_on: valve_7
      - delay: !lambda "return id(valve_7_duration).state * 1000 * 60;"
      - switch.turn_off: valve_7
  - id: valve_8_on_off
    then:
      - switch.turn_on: valve_8
      - delay: !lambda "return id(valve_8_duration).state * 1000 * 60;"
      - switch.turn_off: valve_8

switch:
  # Valves
  - platform: gpio
    id: valve_1
    name: "Sprenger 1"
    icon: "mdi:sprinkler"
    pin: GPIO32
    on_turn_on:
      then:
        - script.execute: valve_1_on_off
    on_turn_off: 
      then:
        - script.stop: valve_1_on_off
  - platform: gpio
    id: valve_2
    name: "Sprenger 2"
    icon: "mdi:sprinkler"
    pin: GPIO33
    on_turn_on:
      then:
        - script.execute: valve_2_on_off
    on_turn_off: 
      then:
        - script.stop: valve_2_on_off
  - platform: gpio
    id: valve_3
    name: "Sprenger 3"
    icon: "mdi:sprinkler"
    pin: GPIO25
    on_turn_on:
      then:
        - script.execute: valve_3_on_off
    on_turn_off: 
      then:
        - script.stop: valve_3_on_off
  - platform: gpio
    id: valve_4
    name: "Sprenger 4"
    icon: "mdi:sprinkler"
    pin: GPIO26
    on_turn_on:
      then:
        - script.execute: valve_4_on_off
    on_turn_off: 
      then:
        - script.stop: valve_4_on_off
  - platform: gpio
    id: valve_5
    name: "Sprenger 5"
    icon: "mdi:sprinkler"
    pin: GPIO27
    on_turn_on:
      then:
        - script.execute: valve_5_on_off
    on_turn_off: 
      then:
        - script.stop: valve_5_on_off
  - platform: gpio
    id: valve_6
    name: "Sprenger 6"
    icon: "mdi:sprinkler"
    pin: GPIO14
    on_turn_on:
      then:
        - script.execute: valve_6_on_off
    on_turn_off: 
      then:
        - script.stop: valve_6_on_off
  - platform: gpio
    id: valve_7
    name: "Sprenger 7"
    icon: "mdi:sprinkler"
    pin: GPIO12
    on_turn_on:
      then:
        - script.execute: valve_7_on_off
    on_turn_off: 
      then:
        - script.stop: valve_7_on_off
  - platform: gpio
    id: valve_8
    name: "Sprenger 8"
    icon: "mdi:sprinkler"
    pin: GPIO13
    on_turn_on:
      then:
        - script.execute: valve_8_on_off
    on_turn_off: 
      then:
        - script.stop: valve_8_on_off

  # Valves Enabled
  - platform: template
    id: valve_1_enabled
    name: "Sprenger 1 aktiv"
    icon: "mdi:check-circle-outline"
    optimistic: True
    restore_mode: RESTORE_DEFAULT_ON
  - platform: template
    id: valve_2_enabled
    name: "Sprenger 2 aktiv"
    icon: "mdi:check-circle-outline"
    optimistic: True
    restore_mode: RESTORE_DEFAULT_ON
  - platform: template
    id: valve_3_enabled
    name: "Sprenger 3 aktiv"
    icon: "mdi:check-circle-outline"
    optimistic: True
    restore_mode: RESTORE_DEFAULT_ON
  - platform: template
    id: valve_4_enabled
    name: "Sprenger 4 aktiv"
    icon: "mdi:check-circle-outline"
    optimistic: True
    restore_mode: RESTORE_DEFAULT_ON
  - platform: template
    id: valve_5_enabled
    name: "Sprenger 5 aktiv"
    icon: "mdi:check-circle-outline"
    optimistic: True
    restore_mode: RESTORE_DEFAULT_ON
  - platform: template
    id: valve_6_enabled
    name: "Sprenger 6 aktiv"
    icon: "mdi:check-circle-outline"
    optimistic: True
    restore_mode: RESTORE_DEFAULT_ON
  - platform: template
    id: valve_7_enabled
    name: "Sprenger 7 aktiv"
    icon: "mdi:check-circle-outline"
    optimistic: True
    restore_mode: RESTORE_DEFAULT_ON
  - platform: template
    id: valve_8_enabled
    name: "Sprenger 8 aktiv"
    icon: "mdi:check-circle-outline"
    optimistic: True
    restore_mode: RESTORE_DEFAULT_ON

  # Zones
  - platform: template
    id: zone_1
    name: "Zone 1"
    icon: "mdi:sprinkler-variant"
    lambda: |-
      return id(valve_1).state || id(valve_8).state;      
    turn_on_action: 
      then:
      - if: 
          condition:
            - switch.is_on: valve_1_enabled
          then:
            - number.set:
                id: valve_1_duration
                value: !lambda "return id(zone_1_duration).state;"
            - switch.turn_on: valve_1
      - if:
          condition:
            - switch.is_on: valve_8_enabled
          then:
            - number.set:
                id: valve_8_duration
                value: !lambda "return id(zone_1_duration).state;"
            - switch.turn_on: valve_8
      - delay: !lambda "return id(zone_1_duration).state * 1000 * 60;"
      - switch.turn_off: zone_1
    turn_off_action: 
      then:
        - switch.turn_off: valve_1
        - switch.turn_off: valve_8
  - platform: template
    id: zone_2
    name: "Zone 2"
    icon: "mdi:sprinkler-variant"
    lambda: |-
      return id(valve_2).state;      
    turn_on_action: 
      then:
        - if: 
            condition:
              - switch.is_on: valve_2_enabled
            then:
              - number.set:
                  id: valve_2_duration
                  value: !lambda "return id(zone_2_duration).state;"
              - switch.turn_on: valve_2
        - delay: !lambda 'return id(zone_2_duration).state * 1000 * 60;'
        - switch.turn_off: zone_2
    turn_off_action: 
      then:
        - switch.turn_off: valve_2
  - platform: template
    id: zone_3
    name: "Zone 3"
    icon: "mdi:sprinkler-variant"
    lambda: |-
      return id(valve_3).state;      
    turn_on_action: 
      then:
        - if: 
            condition:
              - switch.is_on: valve_3_enabled
            then:
              - number.set:
                  id: valve_3_duration
                  value: !lambda "return id(zone_3_duration).state;"
              - switch.turn_on: valve_3
        - delay: !lambda 'return id(zone_3_duration).state * 1000 * 60;'
        - switch.turn_off: zone_3
    turn_off_action: 
      then:
        - switch.turn_off: valve_3
  - platform: template
    id: zone_4
    name: "Zone 4"
    icon: "mdi:sprinkler-variant"
    lambda: |-
      return id(valve_4).state || id(valve_5).state;      
    turn_on_action: 
      then:
        - if: 
            condition:
              - switch.is_on: valve_4_enabled
            then:
              - number.set:
                  id: valve_4_duration
                  value: !lambda "return id(zone_4_duration).state;"
              - switch.turn_on: valve_4
        - if:
            condition:
              - switch.is_on: valve_5_enabled
            then:
              - number.set:
                  id: valve_5_duration
                  value: !lambda "return id(zone_4_duration).state;"
              - switch.turn_on: valve_5
        - delay: !lambda 'return id(zone_4_duration).state * 1000 * 60;'
        - switch.turn_off: zone_4
    turn_off_action: 
      then:
        - switch.turn_off: valve_4
        - switch.turn_off: valve_5
  - platform: template
    id: zone_5
    name: "Zone 5"
    icon: "mdi:sprinkler-variant"
    lambda: |-
      return id(valve_6).state || id(valve_7).state;      
    turn_on_action: 
      then:
        - if: 
            condition:
              - switch.is_on: valve_6_enabled
            then:
              - number.set:
                  id: valve_6_duration
                  value: !lambda "return id(zone_5_duration).state;"
              - switch.turn_on: valve_6
        - if:
            condition:
              - switch.is_on: valve_7_enabled
            then:
              - number.set:
                  id: valve_7_duration
                  value: !lambda "return id(zone_5_duration).state;"
              - switch.turn_on: valve_7
        - delay: !lambda 'return id(zone_5_duration).state * 1000 * 60;'
        - switch.turn_off: zone_5
    turn_off_action: 
      then:
        - switch.turn_off: valve_6
        - switch.turn_off: valve_7

  # Zones Enabled
  - platform: template
    id: zone_1_enabled
    name: "Zone 1 aktiv"
    icon: "mdi:check-circle-outline"
    optimistic: True
    restore_mode: RESTORE_DEFAULT_ON
  - platform: template
    id: zone_2_enabled
    name: "Zone 2 aktiv"
    icon: "mdi:check-circle-outline"
    optimistic: True
    restore_mode: RESTORE_DEFAULT_ON
  - platform: template
    id: zone_3_enabled
    name: "Zone 3 aktiv"
    icon: "mdi:check-circle-outline"
    optimistic: True
    restore_mode: RESTORE_DEFAULT_ON
  - platform: template
    id: zone_4_enabled
    name: "Zone 4 aktiv"
    icon: "mdi:check-circle-outline"
    optimistic: True
    restore_mode: RESTORE_DEFAULT_ON
  - platform: template
    id: zone_5_enabled
    name: "Zone 5 aktiv"
    icon: "mdi:check-circle-outline"
    optimistic: True
    restore_mode: RESTORE_DEFAULT_ON

  # Programm
  - platform: template
    id: program
    name: "Programm"
    icon: "mdi:robot"
    #lambda: |-
    #  return id(zone_1).state || id(zone_2).state || id(zone_3).state || id(zone_4).state || id(zone_5).state;
    optimistic: True
    turn_on_action: 
      then:
        - delay: 1s # Initial delay required for potential immediate shutdown
        - if:
            condition:
              - switch.is_on: program_enabled
            then:
              - if:
                  condition:
                    - switch.is_on: zone_1_enabled
                  then:
                    - switch.turn_on: zone_1
                    - delay: 1s
                    #- delay: !lambda "return id(zone_1_duration).state * 1000 * 60;"
                    - wait_until:
                        condition:
                          - switch.is_off: zone_1
                    - delay: $valve_open_delay
              - if:
                  condition:
                    - switch.is_on: zone_2_enabled
                  then:
                    - switch.turn_on: zone_2
                    - delay: 1s
                    #- delay: !lambda "return id(zone_2_duration).state * 1000 * 60;"
                    - wait_until:
                        condition:
                          - switch.is_off: zone_2
                    - delay: $valve_open_delay
              - if:
                  condition:
                    - switch.is_on: zone_3_enabled
                  then:
                    - switch.turn_on: zone_3
                    - delay: 1s
                    #- delay: !lambda "return id(zone_3_duration).state * 1000 * 60;"
                    - wait_until:
                        condition:
                          - switch.is_off: zone_3
                    - delay: $valve_open_delay
              - if:
                  condition:
                    - switch.is_on: zone_4_enabled
                  then:
                    - switch.turn_on: zone_4
                    - delay: 1s
                    #- delay: !lambda "return id(zone_4_duration).state * 1000;"
                    - wait_until:
                        condition:
                          - switch.is_off: zone_4
                    - delay: $valve_open_delay
              - if:
                  condition:
                    - switch.is_on: zone_5_enabled
                  then:
                    - switch.turn_on: zone_5
                    - delay: 1s
                    #- delay: !lambda "return id(zone_5_duration).state * 1000;"
                    - wait_until:
                        condition:
                          - switch.is_off: zone_5
        - switch.turn_off: program
    turn_off_action: 
      then:
        - switch.turn_off: zone_1
        - switch.turn_off: zone_2
        - switch.turn_off: zone_3
        - switch.turn_off: zone_4
        - switch.turn_off: zone_5

  # Programm Enabled
  - platform: template
    id: program_enabled
    name: "Programm aktiv"
    icon: "mdi:check-circle-outline"
    optimistic: True

number:
  # Sprinkler Durations
  - platform: template
    name: "Sprenger 1 Dauer"
    id: valve_1_duration
    icon: "mdi:timer"
    optimistic: true
    min_value: 1
    max_value: $max_runtime
    initial_value: 20
    step: 1
    unit_of_measurement: min
  - platform: template
    name: "Sprenger 2 Dauer"
    id: valve_2_duration
    icon: "mdi:timer"
    optimistic: true
    min_value: 1
    max_value: $max_runtime
    initial_value: 20
    step: 1
    unit_of_measurement: min
  - platform: template
    name: "Sprenger 3 Dauer"
    id: valve_3_duration
    icon: "mdi:timer"
    optimistic: true
    min_value: 1
    max_value: $max_runtime
    initial_value: 20
    step: 1
    unit_of_measurement: min
  - platform: template
    name: "Sprenger 4 Dauer"
    id: valve_4_duration
    icon: "mdi:timer"
    optimistic: true
    min_value: 1
    max_value: $max_runtime
    initial_value: 20
    step: 1
    unit_of_measurement: min
  - platform: template
    name: "Sprenger 5 Dauer"
    id: valve_5_duration
    icon: "mdi:timer"
    optimistic: true
    min_value: 1
    max_value: $max_runtime
    initial_value: 20
    step: 1
    unit_of_measurement: min
  - platform: template
    name: "Sprenger 6 Dauer"
    id: valve_6_duration
    icon: "mdi:timer"
    optimistic: true
    min_value: 1
    max_value: $max_runtime
    initial_value: 20
    step: 1
    unit_of_measurement: min
  - platform: template
    name: "Sprenger 7 Dauer"
    id: valve_7_duration
    icon: "mdi:timer"
    optimistic: true
    min_value: 1
    max_value: $max_runtime
    initial_value: 20
    step: 1
    unit_of_measurement: min
  - platform: template
    name: "Sprenger 8 Dauer"
    id: valve_8_duration
    icon: "mdi:timer"
    optimistic: true
    min_value: 1
    max_value: $max_runtime
    initial_value: 20
    step: 1
    unit_of_measurement: min

  # Zone Durations
  - platform: template
    name: "Zone 1 Dauer"
    id: zone_1_duration
    icon: "mdi:timer"
    optimistic: true
    min_value: 1
    max_value: $max_runtime
    initial_value: 15
    step: 1
    unit_of_measurement: min
  - platform: template
    name: "Zone 2 Dauer"
    id: zone_2_duration
    icon: "mdi:timer"
    optimistic: true
    min_value: 1
    max_value: $max_runtime
    initial_value: 20
    step: 1
    unit_of_measurement: min
  - platform: template
    name: "Zone 3 Dauer"
    id: zone_3_duration
    icon: "mdi:timer"
    optimistic: true
    min_value: 1
    max_value: $max_runtime
    initial_value: 15
    step: 1
    unit_of_measurement: min
  - platform: template
    name: "Zone 4 Dauer"
    id: zone_4_duration
    icon: "mdi:timer"
    optimistic: true
    min_value: 1
    max_value: $max_runtime
    initial_value: 15
    step: 1
    unit_of_measurement: min
  - platform: template
    name: "Zone 5 Dauer"
    id: zone_5_duration
    icon: "mdi:timer"
    optimistic: true
    min_value: 1
    max_value: $max_runtime
    initial_value: 30
    step: 1
    unit_of_measurement: min

The top section remained basically unchanged, however below it nearly everything is different.

Since I no longer had the “magic” behind the sprinkler controller, i.e. all the implicit controls like duration and automation, I had to spell it all out explicitly. While being extremely tedious this provided me with exactly the flexibility I was missing from my previous approaches.

Here are the most notable parts:

The new configuration still has GPIO switches to control the individual valves. But instead of controlling them directly I’m now using a script because without it when you start a valve with a 10 minute runtime, then stop it after 8 minutes and immediately restart it it will only run for another 2 minutes before delay kicks in and stops it. Scripts work differently in that stopping the script will also cancel the delay.

The zones now have conditions to check if a valve is enabled or not, thus allowing a zone to run even if not all valves are involved. Another highlight is that the zone will automatically set the valves’ runtimes to match the zone. There was an intermittent version which set the zone’s runtime according to the maximum runtime of its child valves:

turn_on_action:
  then:
    - number.set:
        id: zone_1_duration
        value: !lambda 'return max(id(valve_1_duration).state, id(valve_8_duration).state)'

This would allow a valve to be shut down before the end of the zone’s runtime:

  • Valve 1: 10min runtime
  • Valve 8: 15min runtime
  • Zone 1: max(10min, 15min) = 15min runtime

The automation now also respects each zone’s enabled status. Together with the same mechanism in each of the zones this setup is super flexible and allows for the automation to only open the valves you want it to.

Edit 2024-07-23

Unfortunately I realized that the current status of the “active” switches is not always properly reflected in Home Assistant. Pretty regularly the controller would lose its connection to HA which caused the values for all “active” switches to be set to their default. This in turn resulted in the automatic program to mostly not run at all - not very desirable. I tried different methods like connecting the switch to a GPIO and setting a restore_mode like this:

  - platform: gpio
    id: program_enabled
    name: "Programm aktiv"
    icon: "mdi:check-circle-outline"
    pin: GPIO16
    restore_mode: RESTORE_DEFAULT_OFF

However, that only resulted in the switch being constantly on with no way of turning it off for some reason.

Solution: Since I wanted to keep it simple, I created an input boolean in Home Assistant which now keeps track of whether the program is active or not.

The Hardware

With the software part figured out it was time to apply it to the real world:

Hardware

Let’s walk through it left to right:

A the very left bottom there’s the 2-wired 230V cable coming into the box. It feeds to both a 24VAC and a 5VDC transformer. The former powers the valves, the latter is for powering the board.

Next up is the board itself. It has a 5VDC as well as a 7-30VDC input, an ESP32 with pinouts for easy flashing and 8 relays.

Last but not least in the right side is the 9 wired cable (8 valves plus ground) connected to the valve box.

To save on cables and connectors I simply bridged all the relais’ common ports.

Conclusion

It took a lot of planning for the hardware and even more trial and error to get the software exactly how I wanted it to behave. However, all this work has paid off massively because not only is the wiring a little simpler than what I had before but having all of the controls on a single board is tremendously beneficial and SO much cleaner when it comes to controlling it with Home Assistant.