Sprinklers that run the morning after a downpour are the classic wasted water, and the classic sign of a system that does not look at the sky. This recipe gives yours a forecast. Each morning, before the usual watering time, it reads the day’s expected rainfall and, if enough is coming, disables the irrigation so the schedule sits the day out. When the forecast is dry it re-enables the schedule, so a skipped day never turns into a skipped week.
The rain threshold is yours to set, in millimetres of expected rainfall for the day. A few millimetres is enough to skip on, since that much rain does the watering for you; raise it if your garden needs more, lower it to skip on the lightest shower. The check runs at a time you set, early enough to land before a normal morning schedule.
Pointing it at the right switch
The recipe disables and re-enables the controller through a switch, so point it at the one that turns the schedule or the whole controller on and off, not a single zone’s run switch. That way a skip stops the day’s program cleanly.
As a second line of defence, the recipe only ever switches back on what it switched off itself. It records that it skipped a day, and a dry morning restores the controller only when that record is set. Nothing gets turned on that the recipe did not turn off, so a switch picked by mistake cannot be started by a dry forecast.
Add speakers as the optional media-player role and the recipe tells you on the mornings it skips, so a dry-looking day that the forecast says will turn wet is not a mystery when the sprinklers stay off.
Files Selora AI's pipeline reads to install this recipe. The manifest declares roles, inputs, and integrations; the package files are Jinja-templated YAML applied to Home Assistant.
# Irrigation Rain Skip — each morning, read the day's forecast rainfall and
# disable irrigation when enough rain is coming, re-enabling it on a dry day.
#
# The forecast comes from a weather entity (Met.no by default) via
# weather.get_forecasts, read into a response variable and the day's
# precipitation pulled from the first daily entry. The irrigation role is a
# switch, and the recipe expects the schedule or master switch: turn_off skips
# the day, turn_on restores it. The weather entity id is an input so a home
# whose entity is named differently, or on OpenWeatherMap, can point at it.
slug: irrigation-rain-skip
version: 1.0.0
title: Irrigation Rain Skip
tagline: A wet forecast skips the day's watering, and a dry one puts the schedule back.
description: >-
Checks the day's forecast rainfall each morning and disables irrigation when
enough rain is expected, re-enabling it when the forecast is dry. Reads the
forecast from the Met.no weather integration with no account or API key.
Optionally announces the days it skips.
author: Selora AI
released: "2026-08-28"
min_integration_version: "0.12.0"
tags: [outdoors, irrigation, weather]
roles:
# Irrigation schedule / master switch — multi-select. kind switch.
# min_count 1. Point at the schedule or controller switch, not a zone run
# switch, so turn_off skips rather than starting a zone. The role id says
# control, not zones, because naming it after zones invited picking exactly
# the entities that must not be picked. The skipped flag in the package is
# the real guard: the recipe only ever switches back on what it switched off.
- id: irrigation_control
title: Irrigation control
kind: switch
min_count: 1
# One master or schedule switch, as the description says. The skip records
# whether it turned the control off; with several picked, a mix of on and
# off controls could not be restored from a single flag.
max_count: 1
selection: required
description: >-
The irrigation schedule or master switch, not an individual zone. The
recipe switches it off to skip a rainy day and back on once the rain has
passed.
# Optional announcement speakers over TTS. media_player. min_count 0.
- id: announce_speakers
title: Announcement speakers
kind: media_player
min_count: 0
selection: required
description: >-
Optional. Speakers that announce a skipped day over TTS. Leave empty to
run silently.
inputs:
# The weather entity read for the forecast. Met.no's default entity is
# weather.forecast_home; change it to match a differently named entity or an
# OpenWeatherMap one.
- id: weather_entity
type: string
label: Weather entity
description: >-
The weather entity the forecast is read from.
default: "weather.forecast_home"
# Expected rainfall for the day, in millimetres, at or above which the day is
# skipped. A few millimetres does the watering for you.
- id: rain_threshold_mm
type: number
label: Rain threshold (mm)
description: >-
Expected rainfall for the day at or above which irrigation is skipped.
default: 3
min: 0
max: 100
# When the morning forecast check runs. Set it before your normal watering
# time.
- id: check_time
type: string
label: Check time
description: >-
The time each morning the forecast is checked. Set it before your watering
schedule.
default: "05:00:00"
# TTS engine entity for the optional spoken announcement. Auto-resolved by the
# tts_engine resolver from the home's configured tts.* entities, so the wizard
# never shows this field. tts.speak works without a paid Home Assistant Cloud
# subscription. The resolver returns an empty string when the home has no TTS
# engine, and the template then skips the announcement. The default below is
# only used by the offline recipe-validation gate.
- id: tts_engine
type: string
label: Text-to-Speech engine
description: >-
Speech engine entity used for the spoken announcement. Resolved
automatically from the Text-to-Speech engines configured in Home
Assistant.
resolver: tts_engine
default: "tts.piper"
integrations:
# Met.no provides the daily forecast from the home's location with no account
# or API key, so no auto_setup fields are needed.
- domain: met
title: Met.no
package_files:
- package/helpers/skip_state.yaml.j2
- package/automations/rain_skip.yaml.j2
{# Irrigation Rain Skip: a single morning automation.
It calls weather.get_forecasts on the weather entity for the daily forecast,
reads the day's precipitation from the first entry into a variable, then a
choose either disables the irrigation (rain at or above the threshold) and
announces it, or re-enables it (a dry day). The weather entity id and the
threshold are rendered into the runtime templates at install time, so the
forecast lookup uses the home's actual entity.
The dry branch also requires a forecast that actually returned a reading, so
a failed or empty lookup leaves an existing skip in place rather than reading
as a dry day and restoring the schedule blind.
The flag is raised only when the control was actually on, so the recipe owns
the skip only when it caused it. A control already off for the winter, or
stopped by the homeowner, is left alone and never claimed, which is what
makes the dry-day branch safe to act on the flag at all.
The dry-day branch re-enables only when the recipe is the one that disabled
it, tracked by a flag helper. The role accepts any switch, and on most
irrigation integrations a zone switch starts watering that zone when it is
turned on, with nothing here to turn it back off. Without the flag a dry
morning would switch on whatever was picked, so a homeowner who chose zone
switches rather than the schedule switch would have every zone running
unattended. With it, the recipe can only ever undo its own skip. #}
{% set su = slug | replace('-', '_') %}
{% set we = inputs.weather_entity %}
{% set skipped = 'input_boolean.' ~ su ~ '_skipped' %}
{% set control = roles.irrigation_control[0] %}
automation:
- id: selora_recipe_{{ su }}_rain_skip
alias: "Irrigation Rain Skip: check forecast"
description: >-
Each morning, check the day's forecast and skip irrigation if enough rain
is expected, otherwise restore a schedule this recipe skipped.
mode: single
trigger:
- platform: time
at: "{{ inputs.check_time }}"
# A check_time that passes while Home Assistant is down is lost, leaving
# irrigation running into a wet day or a recipe-owned skip sitting on
# through a dry one. Re-asking on startup is safe precisely because both
# branches are guarded: the skip only acts on a control that is on, and
# the restore only on a skip this recipe owns and a forecast that actually
# returned a reading.
- platform: homeassistant
event: start
action:
- service: weather.get_forecasts
target:
entity_id: {{ we }}
data:
type: daily
response_variable: forecast
- variables:
# forecast.precipitation is in the weather entity's precipitation_unit,
# which is inches on an imperial Home Assistant. The threshold input is
# millimetres, so convert before comparing: without this a 3 mm
# threshold silently becomes 3 inches and an ordinary wet forecast
# never skips a thing.
precip: "{% raw %}{% set fc = forecast.get('{% endraw %}{{ we }}{% raw %}', {}).get('forecast', []) %}{% set fall = (fc[0].precipitation | float(0)) if fc else 0 %}{{ (fall * 25.4) if state_attr('{% endraw %}{{ we }}{% raw %}', 'precipitation_unit') == 'in' else fall }}{% endraw %}"
# Whether the call actually came back with a usable reading. An empty
# forecast, or an entry carrying no precipitation, must not be read as
# a dry day: precip falls back to 0, and the restore branch would then
# switch the irrigation back on knowing nothing about the weather.
# Leaving a skip in place for a day costs a day of watering; lifting
# it blind can water straight through a storm.
forecast_ok: "{% raw %}{% set fc = forecast.get('{% endraw %}{{ we }}{% raw %}', {}).get('forecast', []) %}{{ fc | count > 0 and 'precipitation' in fc[0] and fc[0].precipitation is not none }}{% endraw %}"
- choose:
- conditions:
- condition: template
value_template: "{% raw %}{{ precip | float(0) >= {% endraw %}{{ inputs.rain_threshold_mm }}{% raw %} }}{% endraw %}"
sequence:
# Only skip, and only claim the skip, when the irrigation was
# actually set to run. A control that is already off is a seasonal
# shutdown or the homeowner's own decision; raising the flag there
# would make the next dry morning switch their irrigation back on
# for them, which is exactly what the flag exists to prevent.
# Announcing is inside the guard too, so the recipe never reports
# skipping a watering that was never going to happen.
- if:
- condition: state
entity_id: {{ control }}
state: "on"
then:
- service: switch.turn_off
target:
entity_id: {{ control }}
- service: input_boolean.turn_on
target:
entity_id: {{ skipped }}
{% if roles.announce_speakers and inputs.tts_engine %}
- service: tts.speak
target:
entity_id: {{ inputs.tts_engine }}
data:
media_player_entity_id:
{% for player in roles.announce_speakers %}
- {{ player }}
{% endfor %}
message: "{% raw %}Rain is expected today, so today's watering is skipped.{% endraw %}"
{% endif %}
default:
- if:
- condition: template
value_template: "{% raw %}{{ forecast_ok | bool(false) }}{% endraw %}"
- condition: state
entity_id: {{ skipped }}
state: "on"
then:
- service: switch.turn_on
target:
entity_id: {{ control }}
- service: input_boolean.turn_off
target:
entity_id: {{ skipped }}
{# Irrigation Rain Skip helper: a flag marking that the recipe is the one that
switched the irrigation off for a rainy day. The dry-day branch requires it
before switching anything back on, so the recipe can only ever undo its own
skip and never starts irrigation the homeowner had stopped themselves. It
survives a restart, so a skip in progress is still cleared the next dry
morning. #}
input_boolean:
{{ slug | replace('-', '_') }}_skipped:
name: "Irrigation Rain Skip: skipped for rain"
icon: mdi:weather-pouring