cd ../blog
#homeassistant#smarthome#irrigation#automation#garden

Rain-Aware Seasonal Watering in Home Assistant

How my garden irrigation went from a dumb timer to a weather check that never worked, and finally to a seasonal, rain-aware Home Assistant automation.

Jaba Macharashvili 5 min read

In this post
  1. Version one: a very confident timer
  2. Version two: the weather check that was not a weather check
  3. Teaching it what “it rained today” means
  4. Teaching it what month it is
  5. Skips that tell you they happened
  6. The dashboard
  7. What changed in numbers
  8. Three things I would tell past me

The best thing about home automation is that it does exactly what you told it to. That is also the worst thing.

Last week I watched my sprinklers run a full cycle in the rain. The automation controlling them was called Watering (morning & evening, weather-dependent). It had weather conditions in it. It had been running all summer. And it was soaking a garden that was already wet.

This is the story of how that automation got there, and how I rebuilt it.

Version one: a very confident timer

The first sketch was the one everybody writes. Two triggers, three zones, a pile of delays:

triggers:
  - trigger: sun
    event: sunrise
    offset: "00:30:00"
  - trigger: sun
    event: sunset
    offset: "01:00:00"
actions:
  - action: switch.turn_on
    target:
      entity_id: switch.backyard_24v_4ch_switch_2
  - delay: "00:30:00"
  - action: switch.turn_off
    target:
      entity_id: switch.backyard_24v_4ch_switch_2
  - delay: "00:00:15"
  - action: valve.open_valve
    target:
      entity_id: valve.backyard_24v_4ch_switch_1
  - delay: "00:35:00"
  - action: valve.close_valve
    target:
      entity_id: valve.backyard_24v_4ch_switch_1
  - delay: "00:00:15"
  - action: switch.turn_on
    target:
      entity_id: switch.front_garden_irrigation_switch_1
  - delay: "00:20:00"
  - action: switch.turn_off
    target:
      entity_id: switch.front_garden_irrigation_switch_1

Backyard sprinklers on a 4 channel 24 V board, a drip valve on the same board, and the front garden on its own relay. Thirty minutes, thirty five minutes, twenty minutes, twice a day. That is 85 minutes per cycle and 171 minutes of watering every single day, in July and in November alike.

It worked in the sense that the grass was green. It also ran during a thunderstorm, which is the moment you realise your irrigation system has no idea what a sky is.

Version two: the weather check that was not a weather check

So I added conditions. This is where it gets embarrassing, because on paper this looks fine:

conditions:
  - condition: not
    conditions:
      - condition: state
        entity_id: sensor.openweathermap_condition
        state: rainy
  - condition: not
    conditions:
      - condition: state
        entity_id: sensor.openweathermap_precipitation_kind
        state: Rain
  - condition: not
    conditions:
      - condition: or
        conditions:
          - condition: state
            entity_id: sensor.openweathermap_weather
            state: overcast clouds
          - condition: state
            entity_id: sensor.openweathermap_weather
            state: light rain

Three problems, and I lived with all of them for months.

It matched exact strings. OpenWeatherMap does not only say light rain. It says moderate rain, heavy intensity rain, shower rain, drizzle, thunderstorm with rain. None of those equal any of my three values, so every one of them watered.

It had no memory. This is the real bug. Conditions are evaluated at the instant the automation fires. If it poured all afternoon and the sky cleared by seven in the evening, the evening cycle saw clear skies and dumped another 85 minutes on saturated soil. A rain check that only looks at the present moment is barely a rain check.

It was still the same fixed 85 minutes. A September garden was getting the July schedule.

The result was a system that looked clever in the automation editor and was dumb in the garden.

Teaching it what “it rained today” means

The fix that mattered was not smarter conditions. It was giving Home Assistant a number it never had: how much rain actually fell today.

The weather integration gives a rain rate in mm/h. On its own that is useless, because it is only true for the moment you read it. But a rate integrated over time is an amount, and both pieces ship with Home Assistant:

  1. An Integration (Riemann sum) helper on sensor.openweathermap_rain, method left, time unit hours. Millimetres per hour integrated over hours gives millimetres. That becomes sensor.openweathermap_rain_accumulated, a running total.
  2. A Utility Meter helper on top of it with a daily cycle. That becomes sensor.openweathermap_rain_today, which resets at midnight.

Two helpers created in the UI, no custom component, no external service. Now the automation can ask the only question that matters: has enough rain fallen today that I should stay off?

wet: >
  {{ (states('sensor.openweathermap_rain_today') | float(0)) >= 2
     or (states('sensor.openweathermap_rain') | float(0)) >= 0.2
     or states('sensor.openweathermap_condition') in
        ['rainy','pouring','lightning-rainy','snowy-rainy','hail','snowy']
     or 'rain' in states('sensor.openweathermap_weather') | lower
     or 'drizzle' in states('sensor.openweathermap_weather') | lower
     or 'thunder' in states('sensor.openweathermap_weather') | lower }}

Three layers: rain that already fell today, rain falling right now, and a substring match that catches every wording the API throws at me. 'rain' in ... beats listing fifteen exact states, and it never misses moderate rain again.

Teaching it what month it is

Fixing rain still left a garden watered in September like it was August. Plants do not want a constant amount of water, they want a seasonal amount. Long days at 35 °C need roughly double what a mild October day needs, and in a Tbilisi winter watering is at best wasted and at worst freezes a pipe.

So the durations became a function of the month, with the zone times cut by about a third from the old values:

variables:
  factor: >
    {{ {6:1.0, 7:1.0, 8:1.0, 5:0.75, 9:0.75, 4:0.5, 10:0.5}.get(now().month, 0) }}
  backyard_s: "{{ (1200 * factor) | int }}"
  drip_s: "{{ (1320 * factor) | int }}"
  front_s: "{{ (780 * factor) | int }}"
MonthsLevelPer runCycles
Jun to Aug100%55 minmorning and evening
May, Sep75%41 minevening only
Apr, Oct50%27 minevening only
Nov to Maroffnonenone

The delays then use those variables instead of hard coded times:

- action: switch.turn_on
  target:
    entity_id: switch.backyard_24v_4ch_switch_2
- delay:
    seconds: "{{ backyard_s }}"
- action: switch.turn_off
  target:
    entity_id: switch.backyard_24v_4ch_switch_2

Outside summer only the evening cycle runs. The morning trigger still exists, it just stops itself:

- if:
    - condition: trigger
      id: morning
    - condition: template
      value_template: "{{ now().month not in [6,7,8] }}"
  then:
    - stop: "Morning cycle only runs in June to August"

Morning watering in cool weather leaves leaves wet for hours and invites mildew. In July it is the opposite, the morning run is what carries the plants through the afternoon.

Skips that tell you they happened

A silent skip is indistinguishable from a broken automation. So every skip sends a notification with the numbers behind it:

- if:
    - condition: template
      value_template: "{{ wet }}"
  then:
    - action: notify.mobile_app_jabas_iphone_17_pro
      data:
        title: "Home Automation"
        message: >
          Watering skipped: rain today {{ rain_today | round(1) }} mm,
          now {{ rain_now }} mm/h, {{ states('sensor.openweathermap_weather') }}
    - stop: "Rain"

There is also an input_boolean.watering_hold_skip_next toggle for the evenings when I know I do not want water, for example before laying turf. The automation clears it after using it, so a hold is always exactly one cycle.

The dashboard

The Watering view: status chips, season plan, zone tiles, rain gauges and the run log

My sprinklers ran in the rain for months because nothing ever showed me what they were doing. So the rebuild came with a view that answers the obvious questions without opening the automation editor:

  • Status chips: next run time, planned minutes, season level, rain today, outside temperature.
  • A status card driven by a template sensor that says Ready, 41 min planned, or Would skip, rain 3.2 mm today, or On hold, or Off-season.
  • Zone tiles for the backyard, the drip line and the front garden, which light up while they are running.
  • Gauges for rain today, where the needle crosses into red exactly at the 2 mm skip threshold, and for the season level.
  • A log of every run and every skip for the last two weeks.

The status sensor is a small template helper, which keeps the logic in one place:

{% set f = {6:1.0,7:1.0,8:1.0,5:0.75,9:0.75,4:0.5,10:0.5}.get(now().month,0) %}
{% if f == 0 %}Off-season (Nov-Mar)
{% elif is_state('input_boolean.watering_hold_skip_next','on') %}On hold
{% elif wet %}Would skip - rain {{ states('sensor.openweathermap_rain_today') }} mm today
{% else %}Ready - {{ ((1200+1320+780) * f / 60) | round(0) | int }} min planned{% endif %}

What changed in numbers

Before: 171 minutes a day, every day, rain included, all year.

Now, in late September: 41 minutes in the evening, skipped entirely when it rains, and nothing at all between November and March. That is roughly a 75% cut in water, and a happier garden, because the old schedule was quietly drowning it in the shoulder seasons.

Three things I would tell past me

  1. A condition that reads the present moment is not a rain sensor. Integrate the rate, store the total, decide on the total.
  2. Match loosely on weather text. A substring test catches a dozen variants you would never enumerate by hand.
  3. Make the automation tell you when it does nothing. The skips are the interesting events. A notification with millimetres in it is how I would have found this bug in week one instead of month three.

The next step is soil moisture probes in the beds, so the garden itself gets a vote instead of a weather station three kilometres away.

If you are building something similar, feel free to reach out on LinkedIn.


Follow along. New posts show up on the RSS feed. I also share my projects on GitHub, MakerWorld and LinkedIn.

← All posts

Got it working, or stuck? [email protected]