RainMachine Pro — Local integration with zones, programs, restrictions and real-time state

RainMachine Pro for Home Assistant

A custom integration for RainMachine smart irrigation controllers that works entirely on your local network — no cloud, no external dependencies.

This integration is designed as a drop-in replacement for the official Home Assistant RainMachine integration, with additional entities, real-time zone state, and full control over zones and programs directly from Home Assistant.


Why this instead of the official integration?

  • More entities: switches, binary sensors, select, button, firmware update, run completion time sensors
  • Zone and program switches reflect state changes from the app or scheduled runs within seconds (configurable fast polling, default 10 s)
  • Planned irrigation durations per zone and per program, with weather-adaptive vs fixed detection
  • Per-program duration adjustment directly from Home Assistant (+/− buttons)
  • Full multilanguage support: English, Italian, French, German, Spanish

What you get

Switches

  • Start/stop each zone manually (10 min default)
  • Enable/disable each zone
  • Start/stop each program — with per-zone planned durations, total duration, next/last run, frequency, and enabled state as attributes (updated every 10 s)
  • Enable/disable each program
  • Freeze protection on/off
  • Extra water on hot days on/off

Binary sensors

  • Freeze restriction active
  • Hourly / month / weekday / rain delay restriction active
  • Rain sensor and flow sensor triggered

Sensors

  • Today's total watering duration (with long-term statistics)
  • Per-zone watering details (scheduled vs actual, skip reason, planned duration per program)
  • Run completion time per zone and per program (live countdown)
  • Rain delay status
  • Weather parser last run timestamps
  • 7-day forecast (temperature, rain, ET0, condition)

Program duration adjustment

  • Per-program + and buttons to scale all zone durations proportionally
  • Configurable step: 5%, 10%, 15%, or 20% (slider entity per program, default 10%)
  • Works for both weather-adaptive zones (scales userPercentage) and fixed-duration zones (scales duration directly)
  • Example: press + with step 10% → zone A 1200 s → 1320 s, zone B 600 s → 660 s

Other

  • Select: freeze protection temperature threshold (−7 °C to +4 °C)
  • Number: rain delay (0–14 days), adjustment step per program (5–20%)
  • Button: reboot controller, increase/decrease duration per program
  • Update: firmware update status and trigger

Planned duration attributes

Each program switch exposes the planned irrigation duration for every active zone as attributes — updated every 10 seconds via the fast coordinator:

Fronte parco: 1615 # seconds Fronte parco_type: adattiva Laterale: 1325 Laterale_type: adattiva total_duration: 5655 enabled: on next_run: 2026-04-21 04:10 frequency: Ogni giorno 

Each zone sensor exposes the planned duration per program:

Estate: 1615 Estate_type: adattiva Concime: 900 Concime_type: fissa 

Duration type is adaptive (weather-driven, computed from WaterSense referenceTime × userPercentage) or fixed (explicit duration set in the app), translated in all supported languages. Values are integers (seconds), compatible with Home Assistant statistics and template sensors.


How it works

The integration uses two polling coordinators:

  • Fast coordinator (default 10 s, configurable 5–60 s) — polls zones, programs and the watering queue for real-time state; also drives program switch attributes
  • Slow coordinator (default 5 min, configurable 1–60 min) — polls weather parsers, forecast, restrictions, provision, firmware info, and static zone properties

Zone and program names are taken directly from your RainMachine configuration, so they appear in Home Assistant exactly as you named them in the app.


Installation

Available on HACS (pending default catalog approval) or manual install.

https://github.com/yashijoe/ha-rainmachine-pro

After installation, go to Settings → Devices & Services → Add Integration and search for RainMachine Pro. You only need your device's local IP address and password.


Entities:

Lovelace:


Feedback welcome

This is an actively maintained project. Bug reports, feature requests and pull requests are welcome on GitHub. If you're already using the official RainMachine integration and want to migrate, feel free to ask here.

It does not. The official integration lists local polling as it’s IOT Class. I personally never would have bought RainMachine units if they required cloud connectivity.

Why make a new integration instead of adding any of these improvements to the core integration?

Hi Rohan Kapoor, you are right about the cloud API, it was a misinterpretation of the official integration documentation, which I have now corrected.

Why not add these new features to the core integration?
RainMachine products have been discontinued for a couple of years now, and for the same amount of time they have been asking you to subscribe to access your RainMachine data via cloud (unless you set up a port forward).

I don’t think it is a product that appeals to a wide audience, and I haven’t seen any updates to the official integration for a long time.
I preferred to work based on my own specific needs rather than trying to convince the official integration developer to add them.

Some may seem trivial, such as the time elapsed since the last weather update, but if you own a RainMachine you will know that the system sometimes gets stuck and you need to reboot it or disconnect and reconnect the Wi-Fi.

Either way, the code is on GitHub and if the official integration developer wanted to incorporate the new entities, I would be more than happy.

Added button to increase/decrease duration per program

I've added plenty of new features. Below is a list of what I've done so far:

  • Local polling — communicates directly with your RainMachine on your LAN
  • Today's watering summary — total irrigation duration (including manual/forced runs) with statistics support for long-term tracking
  • Per-zone details — scheduled vs actual duration, start time, and skip reason for each zone
  • Planned zone durations — each program switch exposes expected watering duration per active zone (weather-adaptive or fixed); each zone sensor exposes expected duration per program
  • Program duration adjustment — per-program +/− buttons scale all active zone durations by a configurable step (5–20%); works for both adaptive and fixed zones
  • Editable program start time — set each program's scheduled start time directly from Home Assistant
  • Editable program frequency — set each program's irrigation schedule (Daily, Every N days, Odd/Even days, or specific weekdays) directly from Home Assistant
  • Zone and program control — start/stop irrigation zones and programs, enable/disable them
  • Rain delay control — view current delay status and set new delays directly from Home Assistant
  • Freeze protection — enable/disable and set the freeze protection temperature threshold
  • Restriction monitoring — binary sensors for all active watering restrictions
  • Weather parser status — last run timestamp for each configured weather source
  • 7-day forecast — daily weather condition, temperature, rain, and evapotranspiration
  • Firmware update — trigger firmware updates from the Home Assistant update panel
  • Reboot button — reboot the controller directly from Home Assistant
  • Fully configurable from UI — no YAML needed
  • Multi-language — English, Italian, French, German, and Spanish translations included

Much better than the official integration, thanks for the work!

Would you mind sharing your Lovelace YAML? I tried to reproduce it but couldn't get it right.

Of course I am sharing the code!

Note: you need to install "HTML Jinja2 Template card" from HACS first.

Let's start with the Forecast card
My intention was to replicate the table that RainMachine displays when accessed from the web interface.

Card code. Only change the language string

type: custom:html-template-card
ignore_line_breaks: true
content: >
  {% set lang = 'it' %}{# Available languages: it, en, de, fr, es #}
  {% if lang == 'it' %}
    {% set days = {0:'Lun',1:'Mar',2:'Mer',3:'Gio',4:'Ven',5:'Sab',6:'Dom'} %}
    {% set months = {1:'Gennaio',2:'Febbraio',3:'Marzo',4:'Aprile',5:'Maggio',6:'Giugno',7:'Luglio',8:'Agosto',9:'Settembre',10:'Ottobre',11:'Novembre',12:'Dicembre'} %}
    {% set max_temp_label = 'Max temp' %}{% set min_temp_label = 'Min temp' %}{% set rain_label = 'Pioggia (mm)' %}{% set et_label = 'ET (mm)' %}
  {% elif lang == 'de' %}
    {% set days = {0:'Mo',1:'Di',2:'Mi',3:'Do',4:'Fr',5:'Sa',6:'So'} %}
    {% set months = {1:'Januar',2:'Februar',3:'März',4:'April',5:'Mai',6:'Juni',7:'Juli',8:'August',9:'September',10:'Oktober',11:'November',12:'Dezember'} %}
    {% set max_temp_label = 'Max Temp' %}{% set min_temp_label = 'Min Temp' %}{% set rain_label = 'Regen (mm)' %}{% set et_label = 'ET (mm)' %}
  {% elif lang == 'fr' %}
    {% set days = {0:'Lun',1:'Mar',2:'Mer',3:'Jeu',4:'Ven',5:'Sam',6:'Dim'} %}
    {% set months = {1:'Janvier',2:'Février',3:'Mars',4:'Avril',5:'Mai',6:'Juin',7:'Juillet',8:'Août',9:'Septembre',10:'Octobre',11:'Novembre',12:'Décembre'} %}
    {% set max_temp_label = 'Temp max' %}{% set min_temp_label = 'Temp min' %}{% set rain_label = 'Pluie (mm)' %}{% set et_label = 'ET (mm)' %}
  {% elif lang == 'es' %}
    {% set days = {0:'Lun',1:'Mar',2:'Mié',3:'Jue',4:'Vie',5:'Sáb',6:'Dom'} %}
    {% set months = {1:'Enero',2:'Febrero',3:'Marzo',4:'Abril',5:'Mayo',6:'Junio',7:'Julio',8:'Agosto',9:'Septiembre',10:'Octubre',11:'Noviembre',12:'Diciembre'} %}
    {% set max_temp_label = 'Temp máx' %}{% set min_temp_label = 'Temp mín' %}{% set rain_label = 'Lluvia (mm)' %}{% set et_label = 'ET (mm)' %}
  {% else %}
    {% set days = {0:'Mon',1:'Tue',2:'Wed',3:'Thu',4:'Fri',5:'Sat',6:'Sun'} %}
    {% set months = {1:'January',2:'February',3:'March',4:'April',5:'May',6:'June',7:'July',8:'August',9:'September',10:'October',11:'November',12:'December'} %}
    {% set max_temp_label = 'Max temp' %}{% set min_temp_label = 'Min temp' %}{% set rain_label = 'Rain (mm)' %}{% set et_label = 'ET (mm)' %}
  {% endif %}
  {% set ss = ['sensor.rainmachine_forecast_condition_0','sensor.rainmachine_forecast_condition_1','sensor.rainmachine_forecast_condition_2','sensor.rainmachine_forecast_condition_3','sensor.rainmachine_forecast_condition_4','sensor.rainmachine_forecast_condition_5','sensor.rainmachine_forecast_condition_6'] %}
  {% set today = now().strftime('%Y-%m-%d') %}
  {% set lbl = 'width:1%;white-space:nowrap;text-align:right;color:grey;padding-right:6px' %}
  <table style="width:100%;text-align:center;border-collapse:collapse;font-size:0.85em">
    <tr>
      <td style="{{ lbl }}">{{ months[now().month] }}</td>
      {% for s in ss %}
        {% set d = state_attr(s,'day')|default('') %}
        {% set dt = strptime(d,'%Y-%m-%d') if d else now() %}
        <td style="padding:4px 2px;{% if d==today %}background:rgba(33,150,243,0.15);border-radius:8px;{% endif %}">
          <b>{{ days[dt.weekday()] }}<br>{{ dt.day }}</b>
        </td>
      {% endfor %}
    </tr>
    <tr>
      <td></td>
      {% for s in ss %}
        <td style="padding:4px 0">
          <ha-icon icon="{{ state_attr(s,'icon') }}" style="--mdc-icon-size:2em;color:steelblue"></ha-icon>
        </td>
      {% endfor %}
    </tr>
    <tr>
      <td style="{{ lbl }}">{{ max_temp_label }}</td>
      {% for s in ss %}
        <td>{{ state_attr(s,'max_temperature')|int(0) }}°</td>
      {% endfor %}
    </tr>
    <tr>
      <td style="{{ lbl }}">{{ min_temp_label }}</td>
      {% for s in ss %}
        <td>{{ state_attr(s,'min_temperature')|int(0) }}°</td>
      {% endfor %}
    </tr>
    <tr>
      <td style="{{ lbl }}">{{ rain_label }}</td>
      {% for s in ss %}
        {% set d = state_attr(s,'day')|default('') %}
        {% set v = state_attr(s,'rain')|float(0)|round(2) if d<today else state_attr(s,'precipitation_forecast')|float(0)|round(2) %}
        <td>{{ v }}</td>
      {% endfor %}
    </tr>
    <tr>
      <td style="{{ lbl }}">{{ et_label }}</td>
      {% for s in ss %}
        <td>{{ state_attr(s,'EvapoTranspiration')|float(0)|round(2) }}</td>
      {% endfor %}
    </tr>
  </table>

The entities involved in this card are the following, along with their respective attributes:

  • sensor.rainmachine_forecast_condition_0
  • sensor.rainmachine_forecast_condition_1
  • sensor.rainmachine_forecast_condition_2
  • sensor.rainmachine_forecast_condition_3
  • sensor.rainmachine_forecast_condition_4
  • sensor.rainmachine_forecast_condition_5
  • sensor.rainmachine_forecast_condition_6

Let's move on to the program card. In my case the program is called "estate"

Card code. Only change the program name and your language string

type: custom:html-template-card
ignore_line_breaks: true
content: >
  {% set program = 'estate' %}{# Change to your program name #}
  {% set lang = 'it' %}{# Available languages: it, en, de, fr, es #}
  {% if lang == 'it' %}
    {% set day_names = ['Lun','Mar','Mer','Gio','Ven','Sab','Dom'] %}
    {% set active_label = 'Attivo' %}{% set inactive_label = 'Inattivo' %}{% set start_label = 'Inizio' %}{% set every_label = 'ogni' %}{% set days_label = 'giorni' %}{% set next_run_label = 'Prossimo avvio' %}{% set today_label = 'oggi' %}{% set tomorrow_label = 'domani' %}
    {% set zone_label = 'Zona' %}{% set duration_label = 'Durata' %}{% set type_label = 'Tipo' %}{% set total_label = 'Totale' %}
    {% set freq_daily = 'Ogni giorno' %}{% set freq_every_n = 'Ogni N giorni' %}{% set freq_odd = 'Giorni dispari' %}{% set freq_even = 'Giorni pari' %}{% set freq_selected = 'Giorni selezionati' %}
    {% set type_map = {'suggested':'Suggerita','custom':'Personalizzata','not_set':'Non impostata'} %}
  {% elif lang == 'de' %}
    {% set day_names = ['Mo','Di','Mi','Do','Fr','Sa','So'] %}
    {% set active_label = 'Aktiv' %}{% set inactive_label = 'Inaktiv' %}{% set start_label = 'Start' %}{% set every_label = 'alle' %}{% set days_label = 'Tage' %}{% set next_run_label = 'Nächster Start' %}{% set today_label = 'heute' %}{% set tomorrow_label = 'morgen' %}
    {% set zone_label = 'Zone' %}{% set duration_label = 'Dauer' %}{% set type_label = 'Typ' %}{% set total_label = 'Gesamt' %}
    {% set freq_daily = 'Täglich' %}{% set freq_every_n = 'Alle N Tage' %}{% set freq_odd = 'Ungerade Tage' %}{% set freq_even = 'Gerade Tage' %}{% set freq_selected = 'Ausgewählte Tage' %}
    {% set type_map = {'suggested':'Vorgeschlagen','custom':'Benutzerdefiniert','not_set':'Nicht gesetzt'} %}
  {% elif lang == 'fr' %}
    {% set day_names = ['Lun','Mar','Mer','Jeu','Ven','Sam','Dim'] %}
    {% set active_label = 'Actif' %}{% set inactive_label = 'Inactif' %}{% set start_label = 'Début' %}{% set every_label = 'tous les' %}{% set days_label = 'jours' %}{% set next_run_label = 'Prochain démarrage' %}{% set today_label = "aujourd'hui" %}{% set tomorrow_label = 'demain' %}
    {% set zone_label = 'Zone' %}{% set duration_label = 'Durée' %}{% set type_label = 'Type' %}{% set total_label = 'Total' %}
    {% set freq_daily = 'Quotidien' %}{% set freq_every_n = 'Tous les N jours' %}{% set freq_odd = 'Jours impairs' %}{% set freq_even = 'Jours pairs' %}{% set freq_selected = 'Jours sélectionnés' %}
    {% set type_map = {'suggested':'Suggérée','custom':'Personnalisée','not_set':'Non définie'} %}
  {% elif lang == 'es' %}
    {% set day_names = ['Lun','Mar','Mié','Jue','Vie','Sáb','Dom'] %}
    {% set active_label = 'Activo' %}{% set inactive_label = 'Inactivo' %}{% set start_label = 'Inicio' %}{% set every_label = 'cada' %}{% set days_label = 'días' %}{% set next_run_label = 'Próximo inicio' %}{% set today_label = 'hoy' %}{% set tomorrow_label = 'mañana' %}
    {% set zone_label = 'Zona' %}{% set duration_label = 'Duración' %}{% set type_label = 'Tipo' %}{% set total_label = 'Total' %}
    {% set freq_daily = 'Diario' %}{% set freq_every_n = 'Cada N días' %}{% set freq_odd = 'Días impares' %}{% set freq_even = 'Días pares' %}{% set freq_selected = 'Días seleccionados' %}
    {% set type_map = {'suggested':'Sugerida','custom':'Personalizada','not_set':'No establecida'} %}
  {% else %}
    {% set day_names = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'] %}
    {% set active_label = 'Active' %}{% set inactive_label = 'Inactive' %}{% set start_label = 'Start' %}{% set every_label = 'every' %}{% set days_label = 'days' %}{% set next_run_label = 'Next run' %}{% set today_label = 'today' %}{% set tomorrow_label = 'tomorrow' %}
    {% set zone_label = 'Zone' %}{% set duration_label = 'Duration' %}{% set type_label = 'Type' %}{% set total_label = 'Total' %}
    {% set freq_daily = 'Daily' %}{% set freq_every_n = 'Every N days' %}{% set freq_odd = 'Odd days' %}{% set freq_even = 'Even days' %}{% set freq_selected = 'Selected days' %}
    {% set type_map = {'suggested':'Suggested','custom':'Custom','not_set':'Not set'} %}
  {% endif %}
  {% set freq_map = {'daily':freq_daily,'every_n_days':freq_every_n,'odd_days':freq_odd,'even_days':freq_even,'selected_days':freq_selected} %}
  {% set switch_entity = 'switch.rainmachine_' ~ program %}
  {% set frequency_select = 'select.rainmachine_' ~ program ~ '_frequency' %}
  {% set start_time_entity = 'time.rainmachine_' ~ program ~ '_start_time' %}
  {% set frequency_interval = 'number.rainmachine_' ~ program ~ '_frequency_interval' %}
  {% set decrease_button = 'button.rainmachine_' ~ program ~ '_decrease_duration' %}
  {% set increase_button = 'button.rainmachine_' ~ program ~ '_increase_duration' %}
  {% set frequency_mon = 'switch.rainmachine_' ~ program ~ '_frequency_mon' %}
  {% set frequency_tue = 'switch.rainmachine_' ~ program ~ '_frequency_tue' %}
  {% set frequency_wed = 'switch.rainmachine_' ~ program ~ '_frequency_wed' %}
  {% set frequency_thu = 'switch.rainmachine_' ~ program ~ '_frequency_thu' %}
  {% set frequency_fri = 'switch.rainmachine_' ~ program ~ '_frequency_fri' %}
  {% set frequency_sat = 'switch.rainmachine_' ~ program ~ '_frequency_sat' %}
  {% set frequency_sun = 'switch.rainmachine_' ~ program ~ '_frequency_sun' %}
  {% set weather_adaptive = 'switch.rainmachine_' ~ program ~ '_weather_adaptive_watering' %}
  {% set adaptive_freq = 'switch.rainmachine_' ~ program ~ '_use_adaptive_frequency' %}
  {% macro fmt_dur(sec) %}{% set s = sec | int %}{% if s >= 3600 %}{{ s // 3600 }}h {{ (s % 3600) // 60 }}min {{ s % 60 }}s{% else %}{{ s // 60 }}min {{ s % 60 }}s{% endif %}{% endmacro %}
  {% set skip = ['enabled','start_time','frequency','total_duration','device_class','icon','friendly_name','last_run','next_run','last_run_start','last_run_end'] %}
  {% set attrs = states[switch_entity].attributes %}
  {% set enabled = attrs.get('enabled') %}
  {% set freq_raw = states(frequency_select) %}
  {% set freq_label = freq_map.get(freq_raw, freq_raw) %}
  {% set days = [(day_names[0],frequency_mon),(day_names[1],frequency_tue),(day_names[2],frequency_wed),(day_names[3],frequency_thu),(day_names[4],frequency_fri),(day_names[5],frequency_sat),(day_names[6],frequency_sun)] %}
  {% set next_run_str = attrs.get('next_run') %}
  <div>
    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
      <div style="display:flex;align-items:center;gap:6px">
        <span style="font-weight:bold;font-size:1em">{{ attrs.friendly_name | replace('RainMachine ', '') }}</span>
        <button onclick="document.querySelector('home-assistant').hass.callService('button','press',{entity_id:'{{ decrease_button }}'})"
          style="width:22px;height:22px;background:var(--secondary-background-color);border:none;border-radius:6px;font-size:0.95em;cursor:pointer;color:var(--primary-text-color);display:flex;align-items:center;justify-content:center;padding:0">−</button>
        <button onclick="document.querySelector('home-assistant').hass.callService('button','press',{entity_id:'{{ increase_button }}'})"
          style="width:22px;height:22px;background:var(--secondary-background-color);border:none;border-radius:6px;font-size:0.95em;cursor:pointer;color:var(--primary-text-color);display:flex;align-items:center;justify-content:center;padding:0">+</button>
      </div>
      {% if enabled == true or enabled == 'on' %}
        <span style="color:#4CAF50;font-size:0.85em">● {{ active_label }}</span>
      {% else %}
        <span style="color:grey;font-size:0.85em">● {{ inactive_label }}</span>
      {% endif %}
    </div>
    <div style="color:grey;font-size:0.85em;margin-bottom:8px">
      <div style="display:flex;align-items:center;gap:4px;flex-wrap:wrap">
        {{ start_label }}:
        <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ start_time_entity }}'};this.dispatchEvent(e)" style="cursor:pointer;text-decoration:underline dotted">{{ states(start_time_entity)[:-3] }}</span>
        &nbsp;·&nbsp;
        <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ frequency_select }}'};this.dispatchEvent(e)" style="cursor:pointer;text-decoration:underline dotted">{{ freq_label }}</span>
        {% if freq_raw == 'every_n_days' %}
          &nbsp;·&nbsp; {{ every_label }}
          <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ frequency_interval }}'};this.dispatchEvent(e)" style="cursor:pointer;text-decoration:underline dotted">{{ states(frequency_interval) | int }}</span>
          {{ days_label }}
        {% endif %}
        {% if next_run_str %}
          {% set next_dt = as_datetime(next_run_str) %}
          {% set delta = (next_dt.date() - now().date()).days %}
          {% set t = next_dt.strftime('%H:%M') %}
          &nbsp;·&nbsp; {{ next_run_label }}:
          {% if delta == 0 %}{{ today_label }} {{ t }}
          {% elif delta == 1 %}{{ tomorrow_label }} {{ t }}
          {% elif delta <= 6 %}{{ day_names[next_dt.weekday()] }} {{ t }}
          {% else %}{{ next_dt.strftime('%d/%m') }} {{ t }}
          {% endif %}
        {% endif %}
      </div>
      {% if freq_raw == 'selected_days' %}
      <div style="display:flex;gap:4px;margin-top:6px">
        {% for label, entity in days %}
          {% set active = states(entity) == 'on' %}
          <button onclick="document.querySelector('home-assistant').hass.callService('switch','toggle',{entity_id:'{{ entity }}'})" style="flex:1;padding:3px 0;border:none;border-radius:4px;cursor:pointer;font-size:0.78em;background:{{ '#4CAF50' if active else 'var(--secondary-background-color)' }};color:{{ 'white' if active else 'grey' }}">{{ label }}</button>
        {% endfor %}
      </div>
      {% endif %}
    </div>
    <table style="width:100%;border-collapse:collapse;font-size:0.85em;table-layout:fixed">
      <colgroup>
        <col style="width:38%">
        <col style="width:22%">
        <col style="width:15%">
        <col style="width:25%">
      </colgroup>
      <tr style="color:grey;border-bottom:1px solid var(--divider-color)">
        <th style="text-align:left;padding:3px 6px">{{ zone_label }}</th>
        <th style="text-align:right;padding:3px 6px">{{ duration_label }}</th>
        <th style="text-align:right;padding:3px 6px">%</th>
        <th style="text-align:right;padding:3px 6px">{{ type_label }}</th>
      </tr>
      {% for key, val in attrs.items() %}
        {% if key not in skip and not key.endswith('_type') and (val is number or val is none) %}
        {% set zone_slug = key | lower | replace(' ', '_') %}
        {% set type_select = 'select.rainmachine_' ~ program ~ '_' ~ zone_slug ~ '_duration_type' %}
        {% set type_raw = states(type_select) %}
        {% set custom_dur_entity = 'number.rainmachine_' ~ program ~ '_' ~ zone_slug ~ '_custom_duration' %}
        {% set pct_entity = 'number.rainmachine_' ~ program ~ '_' ~ zone_slug ~ '_watering_percentage' %}
        <tr style="border-bottom:1px solid var(--divider-color)">
          <td style="padding:4px 6px">{{ key }}</td>
          <td style="padding:4px 6px;text-align:right">
            {% if type_raw == 'custom' %}
              <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ custom_dur_entity }}'};this.dispatchEvent(e)"
                style="cursor:pointer;text-decoration:underline dotted">{{ fmt_dur(val) if val is number else '—' }}</span>
            {% else %}
              {{ fmt_dur(val) if val is number else '—' }}
            {% endif %}
          </td>
          <td style="padding:4px 6px;text-align:right">
            {% if type_raw == 'suggested' %}
              <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ pct_entity }}'};this.dispatchEvent(e)"
                style="cursor:pointer;text-decoration:underline dotted">{{ states(pct_entity) | int }}%</span>
            {% endif %}
          </td>
          <td style="padding:4px 6px;text-align:right;color:grey;font-style:italic">
            <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ type_select }}'};this.dispatchEvent(e)"
              style="cursor:pointer;text-decoration:underline dotted">{{ type_map.get(type_raw, type_raw) }}</span>
          </td>
        </tr>
        {% endif %}
      {% endfor %}
      <tr>
        <td style="text-align:right;padding:4px 6px;color:grey;font-size:0.85em">{{ total_label }}</td>
        <td style="text-align:right;padding:4px 6px;font-weight:bold">{{ fmt_dur(attrs.total_duration) }}</td>
        <td colspan="2"></td>
      </tr>
    </table>
    <div style="display:flex;gap:8px;margin-top:12px">
      {% set wa_on = states(weather_adaptive) == 'on' %}
      {% set af_on = states(adaptive_freq) == 'on' %}
      <button onclick="document.querySelector('home-assistant').hass.callService('switch','toggle',{entity_id:'{{ weather_adaptive }}'})"
        style="flex:1;height:32px;background:{{ '#4CAF50' if wa_on else 'var(--secondary-background-color)' }};border:none;border-radius:8px;font-size:0.75em;cursor:pointer;color:{{ 'white' if wa_on else 'grey' }};padding:0 4px">
        {{ state_attr(weather_adaptive, 'friendly_name') | replace('RainMachine ', '') }}
      </button>
      <button onclick="document.querySelector('home-assistant').hass.callService('switch','toggle',{entity_id:'{{ adaptive_freq }}'})"
        style="flex:1;height:32px;background:{{ '#4CAF50' if af_on else 'var(--secondary-background-color)' }};border:none;border-radius:8px;font-size:0.75em;cursor:pointer;color:{{ 'white' if af_on else 'grey' }};padding:0 4px">
        {{ state_attr(adaptive_freq, 'friendly_name') | replace('RainMachine ', '') }}
      </button>
    </div>
  </div>

The entities involved in this card are the following, along with their respective attributes:

switch.rainmachine_estate
switch.rainmachine_estate_frequency_mon
switch.rainmachine_estate_frequency_tue
switch.rainmachine_estate_frequency_wed
switch.rainmachine_estate_frequency_thu
switch.rainmachine_estate_frequency_fri
switch.rainmachine_estate_frequency_sat
switch.rainmachine_estate_frequency_sun
select.rainmachine_estate_frequency
time.rainmachine_estate_start_time
number.rainmachine_estate_frequency_interval
number.rainmachine_estate_adjustment_step
button.rainmachine_estate_decrease_duration
button.rainmachine_estate_increase_duration

The list of zones should be retrieved dynamically

Next, meteo services updates:

Card code. Only change the language string

type: custom:html-template-card
ignore_line_breaks: true
content: >
  {% set lang = 'it' %}{# Available languages: it, en, de, fr, es #}
  {% if lang == 'it' %}
    {% set lbl_parser = 'Parser' %}{% set lbl_last_update = 'Ultimo aggiornamento' %}
    {% set now_label = 'Adesso' %}{% set ago_prefix = '' %}{% set ago_suffix = ' fa' %}
    {% set min_singular = 'minuto' %}{% set min_plural = 'minuti' %}
    {% set h_singular = 'ora' %}{% set h_plural = 'ore' %}
    {% set d_singular = 'giorno' %}{% set d_plural = 'giorni' %}
    {% set and_label = ' e ' %}
  {% elif lang == 'de' %}
    {% set lbl_parser = 'Parser' %}{% set lbl_last_update = 'Letztes Update' %}
    {% set now_label = 'Jetzt' %}{% set ago_prefix = 'vor ' %}{% set ago_suffix = '' %}
    {% set min_singular = 'Minute' %}{% set min_plural = 'Minuten' %}
    {% set h_singular = 'Stunde' %}{% set h_plural = 'Stunden' %}
    {% set d_singular = 'Tag' %}{% set d_plural = 'Tagen' %}
    {% set and_label = ' und ' %}
  {% elif lang == 'fr' %}
    {% set lbl_parser = 'Parser' %}{% set lbl_last_update = 'Dernière mise à jour' %}
    {% set now_label = 'Maintenant' %}{% set ago_prefix = 'il y a ' %}{% set ago_suffix = '' %}
    {% set min_singular = 'minute' %}{% set min_plural = 'minutes' %}
    {% set h_singular = 'heure' %}{% set h_plural = 'heures' %}
    {% set d_singular = 'jour' %}{% set d_plural = 'jours' %}
    {% set and_label = ' et ' %}
  {% elif lang == 'es' %}
    {% set lbl_parser = 'Parser' %}{% set lbl_last_update = 'Última actualización' %}
    {% set now_label = 'Ahora' %}{% set ago_prefix = 'hace ' %}{% set ago_suffix = '' %}
    {% set min_singular = 'minuto' %}{% set min_plural = 'minutos' %}
    {% set h_singular = 'hora' %}{% set h_plural = 'horas' %}
    {% set d_singular = 'día' %}{% set d_plural = 'días' %}
    {% set and_label = ' y ' %}
  {% else %}
    {% set lbl_parser = 'Parser' %}{% set lbl_last_update = 'Last update' %}
    {% set now_label = 'Now' %}{% set ago_prefix = '' %}{% set ago_suffix = ' ago' %}
    {% set min_singular = 'minute' %}{% set min_plural = 'minutes' %}
    {% set h_singular = 'hour' %}{% set h_plural = 'hours' %}
    {% set d_singular = 'day' %}{% set d_plural = 'days' %}
    {% set and_label = ' and ' %}
  {% endif %}
  {% set ss = states | selectattr('entity_id', 'match', 'sensor.rainmachine_.*_last_run') | map(attribute='entity_id') | list | sort %}
  <table style="width:100%;border-collapse:collapse;font-size:0.9em">
    <thead>
      <tr style="border-bottom:1px solid var(--divider-color);color:grey">
        <th style="text-align:left;padding:4px 6px">{{ lbl_parser }}</th>
        <th style="text-align:left;padding:4px 6px">{{ lbl_last_update }}</th>
      </tr>
    </thead>
    <tbody>
      {% for s in ss %}
      {% set val = states(s) %}
      {% if val not in ('unknown','unavailable','') %}
        {% set diff = (now() - as_datetime(val)).total_seconds() | int %}
        {% if diff < 60 %}
          {% set rel = now_label %}
        {% elif diff < 3600 %}
          {% set m = (diff / 60) | int %}
          {% set min_label = min_singular if m == 1 else min_plural %}
          {% set rel = ago_prefix ~ m ~ ' ' ~ min_label ~ ago_suffix %}
        {% elif diff < 86400 %}
          {% set h = (diff / 3600) | int %}
          {% set m = ((diff % 3600) / 60) | int %}
          {% set h_label = h_singular if h == 1 else h_plural %}
          {% set min_label = min_singular if m == 1 else min_plural %}
          {% set rel = ago_prefix ~ h ~ ' ' ~ h_label ~ ((and_label ~ m ~ ' ' ~ min_label) if m > 0 else '') ~ ago_suffix %}
        {% else %}
          {% set g = (diff / 86400) | int %}
          {% set d_label = d_singular if g == 1 else d_plural %}
          {% set rel = ago_prefix ~ g ~ ' ' ~ d_label ~ ago_suffix %}
        {% endif %}
      {% else %}
        {% set rel = '—' %}
      {% endif %}
      <tr style="border-bottom:1px solid var(--divider-color)">
        <td style="text-align:left;padding:6px">{{ state_attr(s,'friendly_name') | replace('RainMachine ', '') }}</td>
        <td style="text-align:left;padding:6px;color:grey">{{ rel }}</td>
      </tr>
      {% endfor %}
    </tbody>
  </table>

The entities involved in this card are the following, along with their respective attributes:
sensor.rainmachine_*_last_run

The list of sensors should be retrieved dynamically

1 Like

Hi, I've been investigating the forecast sensors with the help of Claude AI and found two things worth sharing.

1. Forecast sensors _1 to _6 remain unavailable

I queried the /api/4/mixer endpoint directly on two different RainMachine controllers and the forecast data is definitely there. The API returns a mixerData array where mixerData[0] is the most recent entry and contains multiple dailyValues — index 0 being yesterday, and indices 1 to 5 (or more) being the following days.

Here is what mixerData[0].dailyValues looks like:

  • dailyValues[0] → yesterday :white_check_mark:sensor.rainmachine_forecast_condition_0 works
  • dailyValues[1] → today :cross_mark:sensor.rainmachine_forecast_condition_1 unavailable
  • dailyValues[2] → tomorrow :cross_mark: → unavailable
  • dailyValues[3] to [5+] :cross_mark: → all unavailable

It looks like the integration is only reading dailyValues[0] from mixerData[0] and ignoring the rest. The fix should iterate over all dailyValues entries in mixerData[0].

2. WUnderground vs METNO — incomplete data

I also compared the /api/4/mixer response between two controllers, one running METNO only and one running WUnderground only:

WUnderground only:

  • minTemp, maxTemp, qpf, condition :white_check_mark:
  • temperature, rh, dewPoint, pressure → all null :cross_mark:
  • 6 days of forecast

METNO only:

  • All fields populated :white_check_mark:
  • 11 days of forecast

The RainMachine web UI shows complete data on both controllers, so RainMachine must be merging data internally before displaying it. But the raw /api/4/mixer endpoint exposes each parser's data separately, meaning users running WUnderground only will get incomplete forecast sensor attributes in HA. Adding METNO as a second parser fixes this.

Might be worth mentioning in the docs, or having the integration fall back gracefully when fields are null.

Hope this helps!

Perfect. Thanks.

v1.4.0

New switches per program

  • Weather adaptive watering - ON uses internet weather data for adaptive watering, OFF ignores it
  • Use adaptive frequency - ON sets adaptive frequency to 50%, OFF disables it

v1.4.3

Added

  • Duration type select per zone per program: select.<program>_<zone>_duration_type
    • Options: Suggested / Custom / Not set (translated in all 5 languages via HA translation system)
    • Category: CONFIG (hidden from main dashboard by default)
    • Switching to suggested sets duration=0, active=true
    • Switching to custom restores last saved duration (fallback: WaterSense referenceTime or 600s)
    • Switching to not_set sets active=false
    • Duration backup stored via homeassistant.helpers.storage.Store — persists across HA restarts

Example:

Card code. Only change the program name and your language string

type: custom:html-template-card
ignore_line_breaks: true
content: >
  {% set program = 'estate' %}{# Change to your program name #}
  {% set lang = 'it' %}{# Available languages: it, en, de, fr, es #}
  {% if lang == 'it' %}
    {% set day_names = ['Lun','Mar','Mer','Gio','Ven','Sab','Dom'] %}
    {% set active_label = 'Attivo' %}{% set inactive_label = 'Inattivo' %}{% set start_label = 'Inizio' %}{% set every_label = 'ogni' %}{% set days_label = 'giorni' %}{% set next_run_label = 'Prossimo avvio' %}{% set today_label = 'oggi' %}{% set tomorrow_label = 'domani' %}
    {% set zone_label = 'Zona' %}{% set duration_label = 'Durata' %}{% set type_label = 'Tipo' %}{% set total_label = 'Totale' %}
    {% set freq_daily = 'Ogni giorno' %}{% set freq_every_n = 'Ogni N giorni' %}{% set freq_odd = 'Giorni dispari' %}{% set freq_even = 'Giorni pari' %}{% set freq_selected = 'Giorni selezionati' %}
    {% set type_map = {'suggested':'Suggerita','custom':'Personalizzata','not_set':'Non impostata'} %}
  {% elif lang == 'de' %}
    {% set day_names = ['Mo','Di','Mi','Do','Fr','Sa','So'] %}
    {% set active_label = 'Aktiv' %}{% set inactive_label = 'Inaktiv' %}{% set start_label = 'Start' %}{% set every_label = 'alle' %}{% set days_label = 'Tage' %}{% set next_run_label = 'Nächster Start' %}{% set today_label = 'heute' %}{% set tomorrow_label = 'morgen' %}
    {% set zone_label = 'Zone' %}{% set duration_label = 'Dauer' %}{% set type_label = 'Typ' %}{% set total_label = 'Gesamt' %}
    {% set freq_daily = 'Täglich' %}{% set freq_every_n = 'Alle N Tage' %}{% set freq_odd = 'Ungerade Tage' %}{% set freq_even = 'Gerade Tage' %}{% set freq_selected = 'Ausgewählte Tage' %}
    {% set type_map = {'suggested':'Vorgeschlagen','custom':'Benutzerdefiniert','not_set':'Nicht gesetzt'} %}
  {% elif lang == 'fr' %}
    {% set day_names = ['Lun','Mar','Mer','Jeu','Ven','Sam','Dim'] %}
    {% set active_label = 'Actif' %}{% set inactive_label = 'Inactif' %}{% set start_label = 'Début' %}{% set every_label = 'tous les' %}{% set days_label = 'jours' %}{% set next_run_label = 'Prochain démarrage' %}{% set today_label = "aujourd'hui" %}{% set tomorrow_label = 'demain' %}
    {% set zone_label = 'Zone' %}{% set duration_label = 'Durée' %}{% set type_label = 'Type' %}{% set total_label = 'Total' %}
    {% set freq_daily = 'Quotidien' %}{% set freq_every_n = 'Tous les N jours' %}{% set freq_odd = 'Jours impairs' %}{% set freq_even = 'Jours pairs' %}{% set freq_selected = 'Jours sélectionnés' %}
    {% set type_map = {'suggested':'Suggérée','custom':'Personnalisée','not_set':'Non définie'} %}
  {% elif lang == 'es' %}
    {% set day_names = ['Lun','Mar','Mié','Jue','Vie','Sáb','Dom'] %}
    {% set active_label = 'Activo' %}{% set inactive_label = 'Inactivo' %}{% set start_label = 'Inicio' %}{% set every_label = 'cada' %}{% set days_label = 'días' %}{% set next_run_label = 'Próximo inicio' %}{% set today_label = 'hoy' %}{% set tomorrow_label = 'mañana' %}
    {% set zone_label = 'Zona' %}{% set duration_label = 'Duración' %}{% set type_label = 'Tipo' %}{% set total_label = 'Total' %}
    {% set freq_daily = 'Diario' %}{% set freq_every_n = 'Cada N días' %}{% set freq_odd = 'Días impares' %}{% set freq_even = 'Días pares' %}{% set freq_selected = 'Días seleccionados' %}
    {% set type_map = {'suggested':'Sugerida','custom':'Personalizada','not_set':'No establecida'} %}
  {% else %}
    {% set day_names = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'] %}
    {% set active_label = 'Active' %}{% set inactive_label = 'Inactive' %}{% set start_label = 'Start' %}{% set every_label = 'every' %}{% set days_label = 'days' %}{% set next_run_label = 'Next run' %}{% set today_label = 'today' %}{% set tomorrow_label = 'tomorrow' %}
    {% set zone_label = 'Zone' %}{% set duration_label = 'Duration' %}{% set type_label = 'Type' %}{% set total_label = 'Total' %}
    {% set freq_daily = 'Daily' %}{% set freq_every_n = 'Every N days' %}{% set freq_odd = 'Odd days' %}{% set freq_even = 'Even days' %}{% set freq_selected = 'Selected days' %}
    {% set type_map = {'suggested':'Suggested','custom':'Custom','not_set':'Not set'} %}
  {% endif %}
  {% set freq_map = {'daily':freq_daily,'every_n_days':freq_every_n,'odd_days':freq_odd,'even_days':freq_even,'selected_days':freq_selected} %}
  {% set switch_entity = 'switch.rainmachine_' ~ program %}
  {% set frequency_select = 'select.rainmachine_' ~ program ~ '_frequency' %}
  {% set start_time_entity = 'time.rainmachine_' ~ program ~ '_start_time' %}
  {% set frequency_interval = 'number.rainmachine_' ~ program ~ '_frequency_interval' %}
  {% set decrease_button = 'button.rainmachine_' ~ program ~ '_decrease_duration' %}
  {% set increase_button = 'button.rainmachine_' ~ program ~ '_increase_duration' %}
  {% set frequency_mon = 'switch.rainmachine_' ~ program ~ '_frequency_mon' %}
  {% set frequency_tue = 'switch.rainmachine_' ~ program ~ '_frequency_tue' %}
  {% set frequency_wed = 'switch.rainmachine_' ~ program ~ '_frequency_wed' %}
  {% set frequency_thu = 'switch.rainmachine_' ~ program ~ '_frequency_thu' %}
  {% set frequency_fri = 'switch.rainmachine_' ~ program ~ '_frequency_fri' %}
  {% set frequency_sat = 'switch.rainmachine_' ~ program ~ '_frequency_sat' %}
  {% set frequency_sun = 'switch.rainmachine_' ~ program ~ '_frequency_sun' %}
  {% set weather_adaptive = 'switch.rainmachine_' ~ program ~ '_weather_adaptive_watering' %}
  {% set adaptive_freq = 'switch.rainmachine_' ~ program ~ '_use_adaptive_frequency' %}
  {% macro fmt_dur(sec) %}{% set s = sec | int %}{% if s >= 3600 %}{{ s // 3600 }}h {{ (s % 3600) // 60 }}min {{ s % 60 }}s{% else %}{{ s // 60 }}min {{ s % 60 }}s{% endif %}{% endmacro %}
  {% set skip = ['enabled','start_time','frequency','total_duration','device_class','icon','friendly_name','last_run','next_run','last_run_start','last_run_end'] %}
  {% set attrs = states[switch_entity].attributes %}
  {% set enabled = attrs.get('enabled') %}
  {% set freq_raw = states(frequency_select) %}
  {% set freq_label = freq_map.get(freq_raw, freq_raw) %}
  {% set days = [(day_names[0],frequency_mon),(day_names[1],frequency_tue),(day_names[2],frequency_wed),(day_names[3],frequency_thu),(day_names[4],frequency_fri),(day_names[5],frequency_sat),(day_names[6],frequency_sun)] %}
  {% set next_run_str = attrs.get('next_run') %}
  <div>
    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
      <div style="display:flex;align-items:center;gap:6px">
        <span style="font-weight:bold;font-size:1em">{{ attrs.friendly_name | replace('RainMachine ', '') }}</span>
        <button onclick="document.querySelector('home-assistant').hass.callService('button','press',{entity_id:'{{ decrease_button }}'})"
          style="width:22px;height:22px;background:var(--secondary-background-color);border:none;border-radius:6px;font-size:0.95em;cursor:pointer;color:var(--primary-text-color);display:flex;align-items:center;justify-content:center;padding:0">−</button>
        <button onclick="document.querySelector('home-assistant').hass.callService('button','press',{entity_id:'{{ increase_button }}'})"
          style="width:22px;height:22px;background:var(--secondary-background-color);border:none;border-radius:6px;font-size:0.95em;cursor:pointer;color:var(--primary-text-color);display:flex;align-items:center;justify-content:center;padding:0">+</button>
      </div>
      {% if enabled == true or enabled == 'on' %}
        <span style="color:#4CAF50;font-size:0.85em">● {{ active_label }}</span>
      {% else %}
        <span style="color:grey;font-size:0.85em">● {{ inactive_label }}</span>
      {% endif %}
    </div>
    <div style="color:grey;font-size:0.85em;margin-bottom:8px">
      <div style="display:flex;align-items:center;gap:4px;flex-wrap:wrap">
        {{ start_label }}:
        <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ start_time_entity }}'};this.dispatchEvent(e)" style="cursor:pointer;text-decoration:underline dotted">{{ states(start_time_entity)[:-3] }}</span>
        &nbsp;·&nbsp;
        <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ frequency_select }}'};this.dispatchEvent(e)" style="cursor:pointer;text-decoration:underline dotted">{{ freq_label }}</span>
        {% if freq_raw == 'every_n_days' %}
          &nbsp;·&nbsp; {{ every_label }}
          <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ frequency_interval }}'};this.dispatchEvent(e)" style="cursor:pointer;text-decoration:underline dotted">{{ states(frequency_interval) | int }}</span>
          {{ days_label }}
        {% endif %}
        {% if next_run_str %}
          {% set next_dt = as_datetime(next_run_str) %}
          {% set delta = (next_dt.date() - now().date()).days %}
          {% set t = next_dt.strftime('%H:%M') %}
          &nbsp;·&nbsp; {{ next_run_label }}:
          {% if delta == 0 %}{{ today_label }} {{ t }}
          {% elif delta == 1 %}{{ tomorrow_label }} {{ t }}
          {% elif delta <= 6 %}{{ day_names[next_dt.weekday()] }} {{ t }}
          {% else %}{{ next_dt.strftime('%d/%m') }} {{ t }}
          {% endif %}
        {% endif %}
      </div>
      {% if freq_raw == 'selected_days' %}
      <div style="display:flex;gap:4px;margin-top:6px">
        {% for label, entity in days %}
          {% set active = states(entity) == 'on' %}
          <button onclick="document.querySelector('home-assistant').hass.callService('switch','toggle',{entity_id:'{{ entity }}'})" style="flex:1;padding:3px 0;border:none;border-radius:4px;cursor:pointer;font-size:0.78em;background:{{ '#4CAF50' if active else 'var(--secondary-background-color)' }};color:{{ 'white' if active else 'grey' }}">{{ label }}</button>
        {% endfor %}
      </div>
      {% endif %}
    </div>
    <table style="width:100%;border-collapse:collapse;font-size:0.85em;table-layout:fixed">
      <colgroup>
        <col style="width:38%">
        <col style="width:22%">
        <col style="width:15%">
        <col style="width:25%">
      </colgroup>
      <tr style="color:grey;border-bottom:1px solid var(--divider-color)">
        <th style="text-align:left;padding:3px 6px">{{ zone_label }}</th>
        <th style="text-align:right;padding:3px 6px">{{ duration_label }}</th>
        <th style="text-align:right;padding:3px 6px">%</th>
        <th style="text-align:right;padding:3px 6px">{{ type_label }}</th>
      </tr>
      {% for key, val in attrs.items() %}
        {% if key not in skip and not key.endswith('_type') and (val is number or val is none) %}
        {% set zone_slug = key | lower | replace(' ', '_') %}
        {% set type_select = 'select.rainmachine_' ~ program ~ '_' ~ zone_slug ~ '_duration_type' %}
        {% set type_raw = states(type_select) %}
        {% set custom_dur_entity = 'number.rainmachine_' ~ program ~ '_' ~ zone_slug ~ '_custom_duration' %}
        {% set pct_entity = 'number.rainmachine_' ~ program ~ '_' ~ zone_slug ~ '_watering_percentage' %}
        <tr style="border-bottom:1px solid var(--divider-color)">
          <td style="padding:4px 6px">{{ key }}</td>
          <td style="padding:4px 6px;text-align:right">
            {% if type_raw == 'custom' %}
              <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ custom_dur_entity }}'};this.dispatchEvent(e)"
                style="cursor:pointer;text-decoration:underline dotted">{{ fmt_dur(val) if val is number else '—' }}</span>
            {% else %}
              {{ fmt_dur(val) if val is number else '—' }}
            {% endif %}
          </td>
          <td style="padding:4px 6px;text-align:right">
            {% if type_raw == 'suggested' %}
              <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ pct_entity }}'};this.dispatchEvent(e)"
                style="cursor:pointer;text-decoration:underline dotted">{{ states(pct_entity) | int }}%</span>
            {% endif %}
          </td>
          <td style="padding:4px 6px;text-align:right;color:grey;font-style:italic">
            <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ type_select }}'};this.dispatchEvent(e)"
              style="cursor:pointer;text-decoration:underline dotted">{{ type_map.get(type_raw, type_raw) }}</span>
          </td>
        </tr>
        {% endif %}
      {% endfor %}
      <tr>
        <td style="text-align:right;padding:4px 6px;color:grey;font-size:0.85em">{{ total_label }}</td>
        <td style="text-align:right;padding:4px 6px;font-weight:bold">{{ fmt_dur(attrs.total_duration) }}</td>
        <td colspan="2"></td>
      </tr>
    </table>
    <div style="display:flex;gap:8px;margin-top:12px">
      {% set wa_on = states(weather_adaptive) == 'on' %}
      {% set af_on = states(adaptive_freq) == 'on' %}
      <button onclick="document.querySelector('home-assistant').hass.callService('switch','toggle',{entity_id:'{{ weather_adaptive }}'})"
        style="flex:1;height:32px;background:{{ '#4CAF50' if wa_on else 'var(--secondary-background-color)' }};border:none;border-radius:8px;font-size:0.75em;cursor:pointer;color:{{ 'white' if wa_on else 'grey' }};padding:0 4px">
        {{ state_attr(weather_adaptive, 'friendly_name') | replace('RainMachine ', '') }}
      </button>
      <button onclick="document.querySelector('home-assistant').hass.callService('switch','toggle',{entity_id:'{{ adaptive_freq }}'})"
        style="flex:1;height:32px;background:{{ '#4CAF50' if af_on else 'var(--secondary-background-color)' }};border:none;border-radius:8px;font-size:0.75em;cursor:pointer;color:{{ 'white' if af_on else 'grey' }};padding:0 4px">
        {{ state_attr(adaptive_freq, 'friendly_name') | replace('RainMachine ', '') }}
      </button>
    </div>
  </div>

1 Like

v1.5.1

For each HA-enabled zone in each enabled program, three new CONFIG-category entities have been added:

select.__duration_type — choose between suggested, custom, and not set. Options are translated into 5 languages via the native HA translation system. Selecting an option sets the active and duration fields on the device: not set disables the zone in the program, suggested zeros the duration letting WaterSense calculate it, custom restores the duration configured in the number entity.

number.__custom_duration — custom duration in minutes (0.5–299.5, step 0.5, box input). The value is independent of the current mode: it is not overwritten when the device is in suggested or not set, thanks to RestoreEntity which persists it across HA restarts.

number.__watering_percentage — WaterSense percentage (10–200%, step 5%). Directly writes userPercentage to the device.

Example (you can now click for editing):

Card code, only change language string

type: custom:html-template-card
ignore_line_breaks: true
content: >
  {% set program = 'estate' %}{# Change to your program name #}
  {% set lang = 'it' %}{# Available languages: it, en, de, fr, es #}
  {% if lang == 'it' %}
    {% set day_names = ['Lun','Mar','Mer','Gio','Ven','Sab','Dom'] %}
    {% set active_label = 'Attivo' %}{% set inactive_label = 'Inattivo' %}{% set start_label = 'Inizio' %}{% set every_label = 'ogni' %}{% set days_label = 'giorni' %}{% set next_run_label = 'Prossimo avvio' %}{% set today_label = 'oggi' %}{% set tomorrow_label = 'domani' %}
    {% set zone_label = 'Zona' %}{% set duration_label = 'Durata' %}{% set type_label = 'Tipo' %}{% set total_label = 'Totale' %}
    {% set freq_daily = 'Ogni giorno' %}{% set freq_every_n = 'Ogni N giorni' %}{% set freq_odd = 'Giorni dispari' %}{% set freq_even = 'Giorni pari' %}{% set freq_selected = 'Giorni selezionati' %}
    {% set type_map = {'suggested':'Suggerita','custom':'Personalizzata','not_set':'Non impostata'} %}
  {% elif lang == 'de' %}
    {% set day_names = ['Mo','Di','Mi','Do','Fr','Sa','So'] %}
    {% set active_label = 'Aktiv' %}{% set inactive_label = 'Inaktiv' %}{% set start_label = 'Start' %}{% set every_label = 'alle' %}{% set days_label = 'Tage' %}{% set next_run_label = 'Nächster Start' %}{% set today_label = 'heute' %}{% set tomorrow_label = 'morgen' %}
    {% set zone_label = 'Zone' %}{% set duration_label = 'Dauer' %}{% set type_label = 'Typ' %}{% set total_label = 'Gesamt' %}
    {% set freq_daily = 'Täglich' %}{% set freq_every_n = 'Alle N Tage' %}{% set freq_odd = 'Ungerade Tage' %}{% set freq_even = 'Gerade Tage' %}{% set freq_selected = 'Ausgewählte Tage' %}
    {% set type_map = {'suggested':'Vorgeschlagen','custom':'Benutzerdefiniert','not_set':'Nicht gesetzt'} %}
  {% elif lang == 'fr' %}
    {% set day_names = ['Lun','Mar','Mer','Jeu','Ven','Sam','Dim'] %}
    {% set active_label = 'Actif' %}{% set inactive_label = 'Inactif' %}{% set start_label = 'Début' %}{% set every_label = 'tous les' %}{% set days_label = 'jours' %}{% set next_run_label = 'Prochain démarrage' %}{% set today_label = "aujourd'hui" %}{% set tomorrow_label = 'demain' %}
    {% set zone_label = 'Zone' %}{% set duration_label = 'Durée' %}{% set type_label = 'Type' %}{% set total_label = 'Total' %}
    {% set freq_daily = 'Quotidien' %}{% set freq_every_n = 'Tous les N jours' %}{% set freq_odd = 'Jours impairs' %}{% set freq_even = 'Jours pairs' %}{% set freq_selected = 'Jours sélectionnés' %}
    {% set type_map = {'suggested':'Suggérée','custom':'Personnalisée','not_set':'Non définie'} %}
  {% elif lang == 'es' %}
    {% set day_names = ['Lun','Mar','Mié','Jue','Vie','Sáb','Dom'] %}
    {% set active_label = 'Activo' %}{% set inactive_label = 'Inactivo' %}{% set start_label = 'Inicio' %}{% set every_label = 'cada' %}{% set days_label = 'días' %}{% set next_run_label = 'Próximo inicio' %}{% set today_label = 'hoy' %}{% set tomorrow_label = 'mañana' %}
    {% set zone_label = 'Zona' %}{% set duration_label = 'Duración' %}{% set type_label = 'Tipo' %}{% set total_label = 'Total' %}
    {% set freq_daily = 'Diario' %}{% set freq_every_n = 'Cada N días' %}{% set freq_odd = 'Días impares' %}{% set freq_even = 'Días pares' %}{% set freq_selected = 'Días seleccionados' %}
    {% set type_map = {'suggested':'Sugerida','custom':'Personalizada','not_set':'No establecida'} %}
  {% else %}
    {% set day_names = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'] %}
    {% set active_label = 'Active' %}{% set inactive_label = 'Inactive' %}{% set start_label = 'Start' %}{% set every_label = 'every' %}{% set days_label = 'days' %}{% set next_run_label = 'Next run' %}{% set today_label = 'today' %}{% set tomorrow_label = 'tomorrow' %}
    {% set zone_label = 'Zone' %}{% set duration_label = 'Duration' %}{% set type_label = 'Type' %}{% set total_label = 'Total' %}
    {% set freq_daily = 'Daily' %}{% set freq_every_n = 'Every N days' %}{% set freq_odd = 'Odd days' %}{% set freq_even = 'Even days' %}{% set freq_selected = 'Selected days' %}
    {% set type_map = {'suggested':'Suggested','custom':'Custom','not_set':'Not set'} %}
  {% endif %}
  {% set freq_map = {'daily':freq_daily,'every_n_days':freq_every_n,'odd_days':freq_odd,'even_days':freq_even,'selected_days':freq_selected} %}
  {% set switch_entity = 'switch.rainmachine_' ~ program %}
  {% set frequency_select = 'select.rainmachine_' ~ program ~ '_frequency' %}
  {% set start_time_entity = 'time.rainmachine_' ~ program ~ '_start_time' %}
  {% set frequency_interval = 'number.rainmachine_' ~ program ~ '_frequency_interval' %}
  {% set decrease_button = 'button.rainmachine_' ~ program ~ '_decrease_duration' %}
  {% set increase_button = 'button.rainmachine_' ~ program ~ '_increase_duration' %}
  {% set frequency_mon = 'switch.rainmachine_' ~ program ~ '_frequency_mon' %}
  {% set frequency_tue = 'switch.rainmachine_' ~ program ~ '_frequency_tue' %}
  {% set frequency_wed = 'switch.rainmachine_' ~ program ~ '_frequency_wed' %}
  {% set frequency_thu = 'switch.rainmachine_' ~ program ~ '_frequency_thu' %}
  {% set frequency_fri = 'switch.rainmachine_' ~ program ~ '_frequency_fri' %}
  {% set frequency_sat = 'switch.rainmachine_' ~ program ~ '_frequency_sat' %}
  {% set frequency_sun = 'switch.rainmachine_' ~ program ~ '_frequency_sun' %}
  {% set weather_adaptive = 'switch.rainmachine_' ~ program ~ '_weather_adaptive_watering' %}
  {% set adaptive_freq = 'switch.rainmachine_' ~ program ~ '_use_adaptive_frequency' %}
  {% macro fmt_dur(sec) %}{% set s = sec | int %}{% if s >= 3600 %}{{ s // 3600 }}h {{ (s % 3600) // 60 }}min {{ s % 60 }}s{% else %}{{ s // 60 }}min {{ s % 60 }}s{% endif %}{% endmacro %}
  {% set skip = ['enabled','start_time','frequency','total_duration','device_class','icon','friendly_name','last_run','next_run','last_run_start','last_run_end'] %}
  {% set attrs = states[switch_entity].attributes %}
  {% set enabled = attrs.get('enabled') %}
  {% set freq_raw = states(frequency_select) %}
  {% set freq_label = freq_map.get(freq_raw, freq_raw) %}
  {% set days = [(day_names[0],frequency_mon),(day_names[1],frequency_tue),(day_names[2],frequency_wed),(day_names[3],frequency_thu),(day_names[4],frequency_fri),(day_names[5],frequency_sat),(day_names[6],frequency_sun)] %}
  {% set next_run_str = attrs.get('next_run') %}
  <div>
    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
      <div style="display:flex;align-items:center;gap:6px">
        <span style="font-weight:bold;font-size:1em">{{ attrs.friendly_name | replace('RainMachine ', '') }}</span>
        <button onclick="document.querySelector('home-assistant').hass.callService('button','press',{entity_id:'{{ decrease_button }}'})"
          style="width:22px;height:22px;background:var(--secondary-background-color);border:none;border-radius:6px;font-size:0.95em;cursor:pointer;color:var(--primary-text-color);display:flex;align-items:center;justify-content:center;padding:0">−</button>
        <button onclick="document.querySelector('home-assistant').hass.callService('button','press',{entity_id:'{{ increase_button }}'})"
          style="width:22px;height:22px;background:var(--secondary-background-color);border:none;border-radius:6px;font-size:0.95em;cursor:pointer;color:var(--primary-text-color);display:flex;align-items:center;justify-content:center;padding:0">+</button>
      </div>
      {% if enabled == true or enabled == 'on' %}
        <span style="color:#4CAF50;font-size:0.85em">● {{ active_label }}</span>
      {% else %}
        <span style="color:grey;font-size:0.85em">● {{ inactive_label }}</span>
      {% endif %}
    </div>
    <div style="color:grey;font-size:0.85em;margin-bottom:8px">
      <div style="display:flex;align-items:center;gap:4px;flex-wrap:wrap">
        {{ start_label }}:
        <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ start_time_entity }}'};this.dispatchEvent(e)" style="cursor:pointer;text-decoration:underline dotted">{{ states(start_time_entity)[:-3] }}</span>
        &nbsp;·&nbsp;
        <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ frequency_select }}'};this.dispatchEvent(e)" style="cursor:pointer;text-decoration:underline dotted">{{ freq_label }}</span>
        {% if freq_raw == 'every_n_days' %}
          &nbsp;·&nbsp; {{ every_label }}
          <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ frequency_interval }}'};this.dispatchEvent(e)" style="cursor:pointer;text-decoration:underline dotted">{{ states(frequency_interval) | int }}</span>
          {{ days_label }}
        {% endif %}
        {% if next_run_str %}
          {% set next_dt = as_datetime(next_run_str) %}
          {% set delta = (next_dt.date() - now().date()).days %}
          {% set t = next_dt.strftime('%H:%M') %}
          &nbsp;·&nbsp; {{ next_run_label }}:
          {% if delta == 0 %}{{ today_label }} {{ t }}
          {% elif delta == 1 %}{{ tomorrow_label }} {{ t }}
          {% elif delta <= 6 %}{{ day_names[next_dt.weekday()] }} {{ t }}
          {% else %}{{ next_dt.strftime('%d/%m') }} {{ t }}
          {% endif %}
        {% endif %}
      </div>
      {% if freq_raw == 'selected_days' %}
      <div style="display:flex;gap:4px;margin-top:6px">
        {% for label, entity in days %}
          {% set active = states(entity) == 'on' %}
          <button onclick="document.querySelector('home-assistant').hass.callService('switch','toggle',{entity_id:'{{ entity }}'})" style="flex:1;padding:3px 0;border:none;border-radius:4px;cursor:pointer;font-size:0.78em;background:{{ '#4CAF50' if active else 'var(--secondary-background-color)' }};color:{{ 'white' if active else 'grey' }}">{{ label }}</button>
        {% endfor %}
      </div>
      {% endif %}
    </div>
    <table style="width:100%;border-collapse:collapse;font-size:0.85em;table-layout:fixed">
      <colgroup>
        <col style="width:38%">
        <col style="width:22%">
        <col style="width:15%">
        <col style="width:25%">
      </colgroup>
      <tr style="color:grey;border-bottom:1px solid var(--divider-color)">
        <th style="text-align:left;padding:3px 6px">{{ zone_label }}</th>
        <th style="text-align:right;padding:3px 6px">{{ duration_label }}</th>
        <th style="text-align:right;padding:3px 6px">%</th>
        <th style="text-align:right;padding:3px 6px">{{ type_label }}</th>
      </tr>
      {% for key, val in attrs.items() %}
        {% if key not in skip and not key.endswith('_type') and (val is number or val is none) %}
        {% set zone_slug = key | lower | replace(' ', '_') %}
        {% set type_select = 'select.rainmachine_' ~ program ~ '_' ~ zone_slug ~ '_duration_type' %}
        {% set type_raw = states(type_select) %}
        {% set custom_dur_entity = 'number.rainmachine_' ~ program ~ '_' ~ zone_slug ~ '_custom_duration' %}
        {% set pct_entity = 'number.rainmachine_' ~ program ~ '_' ~ zone_slug ~ '_watering_percentage' %}
        <tr style="border-bottom:1px solid var(--divider-color)">
          <td style="padding:4px 6px">{{ key }}</td>
          <td style="padding:4px 6px;text-align:right">
            {% if type_raw == 'custom' %}
              <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ custom_dur_entity }}'};this.dispatchEvent(e)"
                style="cursor:pointer;text-decoration:underline dotted">{{ fmt_dur(val) if val is number else '—' }}</span>
            {% else %}
              {{ fmt_dur(val) if val is number else '—' }}
            {% endif %}
          </td>
          <td style="padding:4px 6px;text-align:right">
            {% if type_raw == 'suggested' %}
              <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ pct_entity }}'};this.dispatchEvent(e)"
                style="cursor:pointer;text-decoration:underline dotted">{{ states(pct_entity) | int }}%</span>
            {% endif %}
          </td>
          <td style="padding:4px 6px;text-align:right;color:grey;font-style:italic">
            <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ type_select }}'};this.dispatchEvent(e)"
              style="cursor:pointer;text-decoration:underline dotted">{{ type_map.get(type_raw, type_raw) }}</span>
          </td>
        </tr>
        {% endif %}
      {% endfor %}
      <tr>
        <td style="text-align:right;padding:4px 6px;color:grey;font-size:0.85em">{{ total_label }}</td>
        <td style="text-align:right;padding:4px 6px;font-weight:bold">{{ fmt_dur(attrs.total_duration) }}</td>
        <td colspan="2"></td>
      </tr>
    </table>
    <div style="display:flex;gap:8px;margin-top:12px">
      {% set wa_on = states(weather_adaptive) == 'on' %}
      {% set af_on = states(adaptive_freq) == 'on' %}
      <button onclick="document.querySelector('home-assistant').hass.callService('switch','toggle',{entity_id:'{{ weather_adaptive }}'})"
        style="flex:1;height:32px;background:{{ '#4CAF50' if wa_on else 'var(--secondary-background-color)' }};border:none;border-radius:8px;font-size:0.75em;cursor:pointer;color:{{ 'white' if wa_on else 'grey' }};padding:0 4px">
        {{ state_attr(weather_adaptive, 'friendly_name') | replace('RainMachine ', '') }}
      </button>
      <button onclick="document.querySelector('home-assistant').hass.callService('switch','toggle',{entity_id:'{{ adaptive_freq }}'})"
        style="flex:1;height:32px;background:{{ '#4CAF50' if af_on else 'var(--secondary-background-color)' }};border:none;border-radius:8px;font-size:0.75em;cursor:pointer;color:{{ 'white' if af_on else 'grey' }};padding:0 4px">
        {{ state_attr(adaptive_freq, 'friendly_name') | replace('RainMachine ', '') }}
      </button>
    </div>
  </div>

v1.5.4
Increase/decrease of total time/zone time with the same logic as the RainMachine app

Card code, only change language string

type: custom:html-template-card
ignore_line_breaks: true
content: >
  {% set program = 'estate' %}{# Change to your program name #}
  {% set lang = 'it' %}{# Available languages: it, en, de, fr, es #}
  {% if lang == 'it' %}
    {% set day_names = ['Lun','Mar','Mer','Gio','Ven','Sab','Dom'] %}
    {% set active_label = 'Attivo' %}{% set inactive_label = 'Inattivo' %}{% set start_label = 'Inizio' %}{% set every_label = 'ogni' %}{% set days_label = 'giorni' %}{% set next_run_label = 'Prossimo avvio' %}{% set today_label = 'oggi' %}{% set tomorrow_label = 'domani' %}
    {% set zone_label = 'Zona' %}{% set duration_label = 'Durata' %}{% set type_label = 'Tipo' %}{% set total_label = 'Totale' %}
    {% set freq_daily = 'Ogni giorno' %}{% set freq_every_n = 'Ogni N giorni' %}{% set freq_odd = 'Giorni dispari' %}{% set freq_even = 'Giorni pari' %}{% set freq_selected = 'Giorni selezionati' %}
    {% set type_map = {'suggested':'Suggerita','custom':'Personalizzata','not_set':'Non impostata'} %}
  {% elif lang == 'de' %}
    {% set day_names = ['Mo','Di','Mi','Do','Fr','Sa','So'] %}
    {% set active_label = 'Aktiv' %}{% set inactive_label = 'Inaktiv' %}{% set start_label = 'Start' %}{% set every_label = 'alle' %}{% set days_label = 'Tage' %}{% set next_run_label = 'Nächster Start' %}{% set today_label = 'heute' %}{% set tomorrow_label = 'morgen' %}
    {% set zone_label = 'Zone' %}{% set duration_label = 'Dauer' %}{% set type_label = 'Typ' %}{% set total_label = 'Gesamt' %}
    {% set freq_daily = 'Täglich' %}{% set freq_every_n = 'Alle N Tage' %}{% set freq_odd = 'Ungerade Tage' %}{% set freq_even = 'Gerade Tage' %}{% set freq_selected = 'Ausgewählte Tage' %}
    {% set type_map = {'suggested':'Vorgeschlagen','custom':'Benutzerdefiniert','not_set':'Nicht gesetzt'} %}
  {% elif lang == 'fr' %}
    {% set day_names = ['Lun','Mar','Mer','Jeu','Ven','Sam','Dim'] %}
    {% set active_label = 'Actif' %}{% set inactive_label = 'Inactif' %}{% set start_label = 'Début' %}{% set every_label = 'tous les' %}{% set days_label = 'jours' %}{% set next_run_label = 'Prochain démarrage' %}{% set today_label = "aujourd'hui" %}{% set tomorrow_label = 'demain' %}
    {% set zone_label = 'Zone' %}{% set duration_label = 'Durée' %}{% set type_label = 'Type' %}{% set total_label = 'Total' %}
    {% set freq_daily = 'Quotidien' %}{% set freq_every_n = 'Tous les N jours' %}{% set freq_odd = 'Jours impairs' %}{% set freq_even = 'Jours pairs' %}{% set freq_selected = 'Jours sélectionnés' %}
    {% set type_map = {'suggested':'Suggérée','custom':'Personnalisée','not_set':'Non définie'} %}
  {% elif lang == 'es' %}
    {% set day_names = ['Lun','Mar','Mié','Jue','Vie','Sáb','Dom'] %}
    {% set active_label = 'Activo' %}{% set inactive_label = 'Inactivo' %}{% set start_label = 'Inicio' %}{% set every_label = 'cada' %}{% set days_label = 'días' %}{% set next_run_label = 'Próximo inicio' %}{% set today_label = 'hoy' %}{% set tomorrow_label = 'mañana' %}
    {% set zone_label = 'Zona' %}{% set duration_label = 'Duración' %}{% set type_label = 'Tipo' %}{% set total_label = 'Total' %}
    {% set freq_daily = 'Diario' %}{% set freq_every_n = 'Cada N días' %}{% set freq_odd = 'Días impares' %}{% set freq_even = 'Días pares' %}{% set freq_selected = 'Días seleccionados' %}
    {% set type_map = {'suggested':'Sugerida','custom':'Personalizada','not_set':'No establecida'} %}
  {% else %}
    {% set day_names = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'] %}
    {% set active_label = 'Active' %}{% set inactive_label = 'Inactive' %}{% set start_label = 'Start' %}{% set every_label = 'every' %}{% set days_label = 'days' %}{% set next_run_label = 'Next run' %}{% set today_label = 'today' %}{% set tomorrow_label = 'tomorrow' %}
    {% set zone_label = 'Zone' %}{% set duration_label = 'Duration' %}{% set type_label = 'Type' %}{% set total_label = 'Total' %}
    {% set freq_daily = 'Daily' %}{% set freq_every_n = 'Every N days' %}{% set freq_odd = 'Odd days' %}{% set freq_even = 'Even days' %}{% set freq_selected = 'Selected days' %}
    {% set type_map = {'suggested':'Suggested','custom':'Custom','not_set':'Not set'} %}
  {% endif %}
  {% set freq_map = {'daily':freq_daily,'every_n_days':freq_every_n,'odd_days':freq_odd,'even_days':freq_even,'selected_days':freq_selected} %}
  {% set switch_entity = 'switch.rainmachine_' ~ program %}
  {% set frequency_select = 'select.rainmachine_' ~ program ~ '_frequency' %}
  {% set start_time_entity = 'time.rainmachine_' ~ program ~ '_start_time' %}
  {% set frequency_interval = 'number.rainmachine_' ~ program ~ '_frequency_interval' %}
  {% set decrease_button = 'button.rainmachine_' ~ program ~ '_decrease_duration' %}
  {% set increase_button = 'button.rainmachine_' ~ program ~ '_increase_duration' %}
  {% set frequency_mon = 'switch.rainmachine_' ~ program ~ '_frequency_mon' %}
  {% set frequency_tue = 'switch.rainmachine_' ~ program ~ '_frequency_tue' %}
  {% set frequency_wed = 'switch.rainmachine_' ~ program ~ '_frequency_wed' %}
  {% set frequency_thu = 'switch.rainmachine_' ~ program ~ '_frequency_thu' %}
  {% set frequency_fri = 'switch.rainmachine_' ~ program ~ '_frequency_fri' %}
  {% set frequency_sat = 'switch.rainmachine_' ~ program ~ '_frequency_sat' %}
  {% set frequency_sun = 'switch.rainmachine_' ~ program ~ '_frequency_sun' %}
  {% set weather_adaptive = 'switch.rainmachine_' ~ program ~ '_weather_adaptive_watering' %}
  {% set adaptive_freq = 'switch.rainmachine_' ~ program ~ '_use_adaptive_frequency' %}
  {% macro fmt_dur(sec) %}{% set s = sec | int %}{% if s >= 3600 %}{{ s // 3600 }}h {{ (s % 3600) // 60 }}min {{ s % 60 }}s{% else %}{{ s // 60 }}min {{ s % 60 }}s{% endif %}{% endmacro %}
  {% set skip = ['enabled','start_time','frequency','total_duration','device_class','icon','friendly_name','last_run','next_run','last_run_start','last_run_end'] %}
  {% set attrs = states[switch_entity].attributes %}
  {% set enabled = attrs.get('enabled') %}
  {% set freq_raw = states(frequency_select) %}
  {% set freq_label = freq_map.get(freq_raw, freq_raw) %}
  {% set days = [(day_names[0],frequency_mon),(day_names[1],frequency_tue),(day_names[2],frequency_wed),(day_names[3],frequency_thu),(day_names[4],frequency_fri),(day_names[5],frequency_sat),(day_names[6],frequency_sun)] %}
  {% set next_run_str = attrs.get('next_run') %}
  <div>
    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
      <div style="display:flex;align-items:center;gap:6px">
        <span style="font-weight:bold;font-size:1em">{{ attrs.friendly_name | replace('RainMachine ', '') }}</span>
        <button onclick="document.querySelector('home-assistant').hass.callService('button','press',{entity_id:'{{ decrease_button }}'})"
          style="width:22px;height:22px;background:var(--secondary-background-color);border:none;border-radius:6px;font-size:0.95em;cursor:pointer;color:var(--primary-text-color);display:flex;align-items:center;justify-content:center;padding:0">−</button>
        <button onclick="document.querySelector('home-assistant').hass.callService('button','press',{entity_id:'{{ increase_button }}'})"
          style="width:22px;height:22px;background:var(--secondary-background-color);border:none;border-radius:6px;font-size:0.95em;cursor:pointer;color:var(--primary-text-color);display:flex;align-items:center;justify-content:center;padding:0">+</button>
      </div>
      {% if enabled == true or enabled == 'on' %}
        <span style="color:#4CAF50;font-size:0.85em">● {{ active_label }}</span>
      {% else %}
        <span style="color:grey;font-size:0.85em">● {{ inactive_label }}</span>
      {% endif %}
    </div>
    <div style="color:grey;font-size:0.85em;margin-bottom:8px">
      <div style="display:flex;align-items:center;gap:4px;flex-wrap:wrap">
        {{ start_label }}:
        <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ start_time_entity }}'};this.dispatchEvent(e)" style="cursor:pointer;text-decoration:underline dotted">{{ states(start_time_entity)[:-3] }}</span>
        &nbsp;·&nbsp;
        <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ frequency_select }}'};this.dispatchEvent(e)" style="cursor:pointer;text-decoration:underline dotted">{{ freq_label }}</span>
        {% if freq_raw == 'every_n_days' %}
          &nbsp;·&nbsp; {{ every_label }}
          <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ frequency_interval }}'};this.dispatchEvent(e)" style="cursor:pointer;text-decoration:underline dotted">{{ states(frequency_interval) | int }}</span>
          {{ days_label }}
        {% endif %}
        {% if next_run_str %}
          {% set next_dt = as_datetime(next_run_str) %}
          {% set delta = (next_dt.date() - now().date()).days %}
          {% set t = next_dt.strftime('%H:%M') %}
          &nbsp;·&nbsp; {{ next_run_label }}:
          {% if delta == 0 %}{{ today_label }} {{ t }}
          {% elif delta == 1 %}{{ tomorrow_label }} {{ t }}
          {% elif delta <= 6 %}{{ day_names[next_dt.weekday()] }} {{ t }}
          {% else %}{{ next_dt.strftime('%d/%m') }} {{ t }}
          {% endif %}
        {% endif %}
      </div>
      {% if freq_raw == 'selected_days' %}
      <div style="display:flex;gap:4px;margin-top:6px">
        {% for label, entity in days %}
          {% set active = states(entity) == 'on' %}
          <button onclick="document.querySelector('home-assistant').hass.callService('switch','toggle',{entity_id:'{{ entity }}'})" style="flex:1;padding:3px 0;border:none;border-radius:4px;cursor:pointer;font-size:0.78em;background:{{ '#4CAF50' if active else 'var(--secondary-background-color)' }};color:{{ 'white' if active else 'grey' }}">{{ label }}</button>
        {% endfor %}
      </div>
      {% endif %}
    </div>
    <table style="width:100%;border-collapse:collapse;font-size:0.85em;table-layout:fixed">
      <colgroup>
        <col style="width:38%">
        <col style="width:22%">
        <col style="width:15%">
        <col style="width:25%">
      </colgroup>
      <tr style="color:grey;border-bottom:1px solid var(--divider-color)">
        <th style="text-align:left;padding:3px 6px">{{ zone_label }}</th>
        <th style="text-align:right;padding:3px 6px">{{ duration_label }}</th>
        <th style="text-align:right;padding:3px 6px">%</th>
        <th style="text-align:right;padding:3px 6px">{{ type_label }}</th>
      </tr>
      {% for key, val in attrs.items() %}
        {% if key not in skip and not key.endswith('_type') and (val is number or val is none) %}
        {% set zone_slug = key | lower | replace(' ', '_') %}
        {% set type_select = 'select.rainmachine_' ~ program ~ '_' ~ zone_slug ~ '_duration_type' %}
        {% set type_raw = states(type_select) %}
        {% set custom_dur_entity = 'number.rainmachine_' ~ program ~ '_' ~ zone_slug ~ '_custom_duration' %}
        {% set pct_entity = 'number.rainmachine_' ~ program ~ '_' ~ zone_slug ~ '_watering_percentage' %}
        <tr style="border-bottom:1px solid var(--divider-color)">
          <td style="padding:4px 6px">{{ key }}</td>
          <td style="padding:4px 6px;text-align:right">
            {% if type_raw == 'custom' %}
              <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ custom_dur_entity }}'};this.dispatchEvent(e)"
                style="cursor:pointer;text-decoration:underline dotted">{{ fmt_dur(val) if val is number else '—' }}</span>
            {% else %}
              {{ fmt_dur(val) if val is number else '—' }}
            {% endif %}
          </td>
          <td style="padding:4px 6px;text-align:right">
            {% if type_raw == 'suggested' %}
              <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ pct_entity }}'};this.dispatchEvent(e)"
                style="cursor:pointer;text-decoration:underline dotted">{{ states(pct_entity) | int }}%</span>
            {% endif %}
          </td>
          <td style="padding:4px 6px;text-align:right;color:grey;font-style:italic">
            <span onclick="var e=new Event('hass-more-info',{bubbles:true,composed:true});e.detail={entityId:'{{ type_select }}'};this.dispatchEvent(e)"
              style="cursor:pointer;text-decoration:underline dotted">{{ type_map.get(type_raw, type_raw) }}</span>
          </td>
        </tr>
        {% endif %}
      {% endfor %}
      <tr>
        <td style="text-align:right;padding:4px 6px;color:grey;font-size:0.85em">{{ total_label }}</td>
        <td style="text-align:right;padding:4px 6px;font-weight:bold">{{ fmt_dur(attrs.total_duration) }}</td>
        <td colspan="2"></td>
      </tr>
    </table>
    <div style="display:flex;gap:8px;margin-top:12px">
      {% set wa_on = states(weather_adaptive) == 'on' %}
      {% set af_on = states(adaptive_freq) == 'on' %}
      <button onclick="document.querySelector('home-assistant').hass.callService('switch','toggle',{entity_id:'{{ weather_adaptive }}'})"
        style="flex:1;height:32px;background:{{ '#4CAF50' if wa_on else 'var(--secondary-background-color)' }};border:none;border-radius:8px;font-size:0.75em;cursor:pointer;color:{{ 'white' if wa_on else 'grey' }};padding:0 4px">
        {{ state_attr(weather_adaptive, 'friendly_name') | replace('RainMachine ', '') }}
      </button>
      <button onclick="document.querySelector('home-assistant').hass.callService('switch','toggle',{entity_id:'{{ adaptive_freq }}'})"
        style="flex:1;height:32px;background:{{ '#4CAF50' if af_on else 'var(--secondary-background-color)' }};border:none;border-radius:8px;font-size:0.75em;cursor:pointer;color:{{ 'white' if af_on else 'grey' }};padding:0 4px">
        {{ state_attr(adaptive_freq, 'friendly_name') | replace('RainMachine ', '') }}
      </button>
    </div>
  </div>

v1.5.9

Added sensors countdown per program/zone

[v1.6.9]

  • Irrigation forecast sensors: 7 sensors per enabled program from GET api/4/dailystats/details

Card code. Only change the program name and your language string

type: custom:html-template-card
ignore_line_breaks: true
content: >
  {% set program = 'estate' %}{# Change to your program name #} {% set lang =
  'it' %}{# Available languages: it, en, de, fr, es #} {% if lang == 'it' %}
    {% set day_names = ['Lun','Mar','Mer','Gio','Ven','Sab','Dom'] %}
    {% set lbl_day = 'Giorno' %}{% set lbl_sched = 'Schedulato' %}{% set lbl_comp = 'Previsto' %}{% set lbl_flag = 'Note' %}{% set lbl_today = 'Oggi' %}{% set lbl_tomorrow = 'Domani' %}{% set lbl_title = 'Previsione irrigazione' %}
  {% elif lang == 'de' %}
    {% set day_names = ['Mo','Di','Mi','Do','Fr','Sa','So'] %}
    {% set lbl_day = 'Tag' %}{% set lbl_sched = 'Geplant' %}{% set lbl_comp = 'Prognose' %}{% set lbl_flag = 'Hinweis' %}{% set lbl_today = 'Heute' %}{% set lbl_tomorrow = 'Morgen' %}{% set lbl_title = 'Bewässerungsvorhersage' %}
  {% elif lang == 'fr' %}
    {% set day_names = ['Lun','Mar','Mer','Jeu','Ven','Sam','Dim'] %}
    {% set lbl_day = 'Jour' %}{% set lbl_sched = 'Planifié' %}{% set lbl_comp = 'Prévu' %}{% set lbl_flag = 'Notes' %}{% set lbl_today = "Aujourd'hui" %}{% set lbl_tomorrow = 'Demain' %}{% set lbl_title = "Prévision d'irrigation" %}
  {% elif lang == 'es' %}
    {% set day_names = ['Lun','Mar','Mié','Jue','Vie','Sáb','Dom'] %}
    {% set lbl_day = 'Día' %}{% set lbl_sched = 'Programado' %}{% set lbl_comp = 'Previsto' %}{% set lbl_flag = 'Notas' %}{% set lbl_today = 'Hoy' %}{% set lbl_tomorrow = 'Mañana' %}{% set lbl_title = 'Previsión de riego' %}
  {% else %}
    {% set day_names = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'] %}
    {% set lbl_day = 'Day' %}{% set lbl_sched = 'Scheduled' %}{% set lbl_comp = 'Forecast' %}{% set lbl_flag = 'Notes' %}{% set lbl_today = 'Today' %}{% set lbl_tomorrow = 'Tomorrow' %}{% set lbl_title = 'Irrigation forecast' %}
  {% endif %} {% macro fmt_sec(sec) %}{% set s = sec | int %}{% set m = s // 60
  %}{% set ss = s % 60 %}{{ m }}:{{ '0' if ss < 10 else '' }}{{ ss }}{% endmacro
  %} {% set first = 'sensor.rainmachine_' ~ program ~ '_irrigation_forecast_0'
  %} {% set ns = namespace(zones=[]) %} {% for key in states[first].attributes
  %}
    {% if key.endswith('_computed_sec') and key != 'computed_sec' %}
      {% set ns.zones = ns.zones + [key | replace('_computed_sec', '')] %}
    {% endif %}
  {% endfor %} <style>
    .fc-wrap { font-size: 13px; }
    .fc-title { font-size: 14px; font-weight: bold; margin-bottom: 8px; }
    .fc-table { border-collapse: collapse; width: 100%; }
    .fc-table th { padding: 5px 8px; text-align: center; font-weight: bold; border-bottom: 1px solid #555; }
    .fc-table th .sub { font-size: 10px; font-weight: normal; color: #888; }
    .fc-table td { padding: 4px 8px; text-align: center; border-bottom: 1px solid #2a2a2a; }
    .fc-day { text-align: left !important; }
    .fc-today { background: rgba(33, 150, 243, 0.15); }
    .fc-warn { color: #FF9800; }
  </style> <div class="fc-wrap">
    <div class="fc-title">{{ program | capitalize }} - {{ lbl_title }}</div>
    <table class="fc-table">
      <thead>
        <tr>
          <th class="fc-day">{{ lbl_day }}</th>
          <th>{{ lbl_sched }}<br><span class="sub">min:sec</span></th>
          <th>{{ lbl_comp }}<br><span class="sub">min:sec</span></th>
          <th>%</th>
          <th class="fc-day">{{ lbl_flag }}</th>
        </tr>
      </thead>
      <tbody>
        {% set today = now().date() %}
        {% for i in range(7) %}
          {% set sensor = 'sensor.rainmachine_' ~ program ~ '_irrigation_forecast_' ~ i %}
          {% set attrs = states[sensor].attributes %}
          {% set day_date = as_datetime(attrs.day).date() %}
          {% set delta = (day_date - today).days %}
          {% if delta == 0 %}{% set day_lbl = lbl_today %}
          {% elif delta == 1 %}{% set day_lbl = lbl_tomorrow %}
          {% else %}{% set day_lbl = day_names[day_date.weekday()] ~ ' ' ~ day_date.strftime('%d/%m') %}{% endif %}
          {% set scheduled = attrs.get('scheduled_sec', 0) | int %}
          {% set computed = attrs.get('computed_sec', 0) | int %}
          {% set pct = ((computed / scheduled * 100) | round(0) | int) if scheduled > 0 else 0 %}
          {% set ns2 = namespace(flags=[]) %}
          {% for zone in ns.zones %}
            {% set flag = attrs.get(zone ~ '_watering_flag', '') %}
            {% if flag and flag not in ns2.flags %}
              {% set ns2.flags = ns2.flags + [flag] %}
            {% endif %}
          {% endfor %}
          <tr class="{{ 'fc-today' if delta == 0 else '' }}">
            <td class="fc-day">{{ day_lbl }}</td>
            <td>{{ fmt_sec(scheduled) }}</td>
            <td class="{{ 'fc-warn' if pct < 100 else '' }}">{{ fmt_sec(computed) }}</td>
            <td class="{{ 'fc-warn' if pct < 100 else '' }}">{{ pct }}%</td>
            <td class="fc-day fc-warn">{{ ns2.flags | join(', ') }}</td>
          </tr>
        {% endfor %}
      </tbody>
    </table>
  </div>

v1.6.16 - Cycle & Soak control per program

What's new

Cycle & Soak control (3 entities per enabled program)

Three entities added per enabled program:

Entity Description Range
select.<program>_cycle_soak_mode Mode: off / auto / custom
number.<program>_cycle_soak_cycles Number of cycles (custom mode) 2–50, step 1
number.<program>_cycle_soak_min Soak duration between cycles (custom mode) 0–300 min, step 1

Behaviour:

  • off → disables cycle & soak (cs_on = false)
  • auto → device computes cycles automatically (cs_on = true, cycles = -1)
  • custom → applies user-defined cycles and soak time immediately

Number entities apply to the API immediately if mode is already custom; otherwise values are stored as pending and sent when switching to custom.

This card code generated some errors in my HAOS config that resulted in no display of information. I’m not experienced in scripting etc so I asked chatgpt to update it for my use. Posting the code here in case it’s helpful to someone else

type: custom:html-template-card
ignore_line_breaks: true
content: |
  {# ============================================================
     RAINMACHINE WEEKLY FORECAST CARD
     ============================================================ #}

  {% set program = 'primary_schedule' %}
  {% set display_name = 'Primary Schedule' %}
  {% set lang = 'en' %}
  {% set days_to_show = 7 %}

  {# ---------- Labels ---------- #}

  {% if lang == 'it' %}
    {% set day_names = ['Lun','Mar','Mer','Gio','Ven','Sab','Dom'] %}
    {% set lbl_day = 'Giorno' %}
    {% set lbl_sched = 'Programmato' %}
    {% set lbl_comp = 'Previsto' %}
    {% set lbl_notes = 'Note' %}
    {% set lbl_today = 'Oggi' %}
    {% set lbl_tomorrow = 'Domani' %}
    {% set lbl_title = 'Previsione irrigazione' %}
    {% set lbl_week_sched = 'Programmato' %}
    {% set lbl_week_forecast = 'Previsto' %}
    {% set lbl_savings = 'Risparmio' %}
    {% set lbl_next = 'Prossima irrigazione' %}
    {% set lbl_no_watering = 'Nessuna irrigazione prevista' %}
    {% set lbl_rain = 'Pioggia' %}
    {% set lbl_temp = 'Temperatura' %}
    {% set lbl_humidity = 'Umidità' %}
    {% set lbl_wind = 'Vento' %}
    {% set lbl_et = 'ET' %}
  {% elif lang == 'de' %}
    {% set day_names = ['Mo','Di','Mi','Do','Fr','Sa','So'] %}
    {% set lbl_day = 'Tag' %}
    {% set lbl_sched = 'Geplant' %}
    {% set lbl_comp = 'Prognose' %}
    {% set lbl_notes = 'Hinweise' %}
    {% set lbl_today = 'Heute' %}
    {% set lbl_tomorrow = 'Morgen' %}
    {% set lbl_title = 'Bewässerungsvorhersage' %}
    {% set lbl_week_sched = 'Geplant' %}
    {% set lbl_week_forecast = 'Prognose' %}
    {% set lbl_savings = 'Einsparung' %}
    {% set lbl_next = 'Nächste Bewässerung' %}
    {% set lbl_no_watering = 'Keine Bewässerung geplant' %}
    {% set lbl_rain = 'Regen' %}
    {% set lbl_temp = 'Temperatur' %}
    {% set lbl_humidity = 'Feuchtigkeit' %}
    {% set lbl_wind = 'Wind' %}
    {% set lbl_et = 'ET' %}
  {% elif lang == 'fr' %}
    {% set day_names = ['Lun','Mar','Mer','Jeu','Ven','Sam','Dim'] %}
    {% set lbl_day = 'Jour' %}
    {% set lbl_sched = 'Planifié' %}
    {% set lbl_comp = 'Prévu' %}
    {% set lbl_notes = 'Notes' %}
    {% set lbl_today = "Aujourd'hui" %}
    {% set lbl_tomorrow = 'Demain' %}
    {% set lbl_title = "Prévision d'irrigation" %}
    {% set lbl_week_sched = 'Planifié' %}
    {% set lbl_week_forecast = 'Prévu' %}
    {% set lbl_savings = 'Économie' %}
    {% set lbl_next = 'Prochain arrosage' %}
    {% set lbl_no_watering = 'Aucun arrosage prévu' %}
    {% set lbl_rain = 'Pluie' %}
    {% set lbl_temp = 'Température' %}
    {% set lbl_humidity = 'Humidité' %}
    {% set lbl_wind = 'Vent' %}
    {% set lbl_et = 'ET' %}
  {% elif lang == 'es' %}
    {% set day_names = ['Lun','Mar','Mié','Jue','Vie','Sáb','Dom'] %}
    {% set lbl_day = 'Día' %}
    {% set lbl_sched = 'Programado' %}
    {% set lbl_comp = 'Previsto' %}
    {% set lbl_notes = 'Notas' %}
    {% set lbl_today = 'Hoy' %}
    {% set lbl_tomorrow = 'Mañana' %}
    {% set lbl_title = 'Previsión de riego' %}
    {% set lbl_week_sched = 'Programado' %}
    {% set lbl_week_forecast = 'Previsto' %}
    {% set lbl_savings = 'Ahorro' %}
    {% set lbl_next = 'Próximo riego' %}
    {% set lbl_no_watering = 'No hay riego previsto' %}
    {% set lbl_rain = 'Lluvia' %}
    {% set lbl_temp = 'Temperatura' %}
    {% set lbl_humidity = 'Humedad' %}
    {% set lbl_wind = 'Viento' %}
    {% set lbl_et = 'ET' %}
  {% else %}
    {% set day_names = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'] %}
    {% set lbl_day = 'Day' %}
    {% set lbl_sched = 'Scheduled' %}
    {% set lbl_comp = 'Forecast' %}
    {% set lbl_notes = 'Notes' %}
    {% set lbl_today = 'Today' %}
    {% set lbl_tomorrow = 'Tomorrow' %}
    {% set lbl_title = 'Irrigation forecast' %}
    {% set lbl_week_sched = 'Scheduled' %}
    {% set lbl_week_forecast = 'Forecast' %}
    {% set lbl_savings = 'Water savings' %}
    {% set lbl_next = 'Next watering' %}
    {% set lbl_no_watering = 'No watering forecast' %}
    {% set lbl_rain = 'Rain' %}
    {% set lbl_temp = 'Temperature' %}
    {% set lbl_humidity = 'Humidity' %}
    {% set lbl_wind = 'Wind' %}
    {% set lbl_et = 'ET' %}
  {% endif %}

  {# ---------- Formatting macros ---------- #}

  {% macro fmt_duration(value) %}
    {% set total = value | int(0) %}
    {% set hours = total // 3600 %}
    {% set minutes = (total % 3600) // 60 %}
    {% set seconds = total % 60 %}

    {% if hours > 0 %}
      {{ hours }}h {{ '%02d' | format(minutes) }}m
    {% else %}
      {{ minutes }}:{{ '%02d' | format(seconds) }}
    {% endif %}
  {% endmacro %}

  {% macro fmt_metric(value, suffix='') %}
    {% if value is not none %}
      {{ value }}{{ suffix }}
    {% endif %}
  {% endmacro %}

  {# ---------- Entity and zone discovery ---------- #}

  {% set first_entity =
    'sensor.rainmachine_' ~ program ~ '_irrigation_forecast_0'
  %}

  {% set first_results = expand(first_entity) | list %}
  {% set zones = namespace(items=[]) %}

  {% if first_results | count > 0 %}
    {% for attribute_name in first_results[0].attributes.keys() %}
      {% if attribute_name.endswith('_computed_sec')
            and attribute_name != 'computed_sec' %}
        {% set zone_name =
          attribute_name | replace('_computed_sec', '')
        %}
        {% set zones.items = zones.items + [zone_name] %}
      {% endif %}
    {% endfor %}
  {% endif %}

  {# ---------- Weekly summary calculation ---------- #}

  {% set summary = namespace(
    scheduled=0,
    computed=0,
    next_day='',
    next_found=false,
    active_days=0
  ) %}

  {% set today = now().date() %}

  {% for i in range(days_to_show) %}
    {% set entity =
      'sensor.rainmachine_' ~
      program ~
      '_irrigation_forecast_' ~
      i
    %}

    {% set scheduled =
      state_attr(entity, 'scheduled_sec') | int(0)
    %}

    {% set computed =
      state_attr(entity, 'computed_sec') | int(0)
    %}

    {% set day_raw = state_attr(entity, 'day') %}

    {% set summary.scheduled = summary.scheduled + scheduled %}
    {% set summary.computed = summary.computed + computed %}

    {% if computed > 0 %}
      {% set summary.active_days = summary.active_days + 1 %}

      {% if not summary.next_found and day_raw %}
        {% set next_date =
          strptime(day_raw | string, '%Y-%m-%d').date()
        %}

        {% set next_delta = (next_date - today).days %}

        {% if next_delta == 0 %}
          {% set summary.next_day = lbl_today %}
        {% elif next_delta == 1 %}
          {% set summary.next_day = lbl_tomorrow %}
        {% else %}
          {% set summary.next_day =
            day_names[next_date.weekday()] ~
            ' ' ~
            next_date.strftime('%b %-d')
          %}
        {% endif %}

        {% set summary.next_found = true %}
      {% endif %}
    {% endif %}
  {% endfor %}

  {% if summary.scheduled > 0 %}
    {% set weekly_percent =
      (summary.computed / summary.scheduled * 100)
      | round(0)
      | int
    %}

    {% set savings_percent =
      (100 - weekly_percent)
    %}
  {% else %}
    {% set weekly_percent = 0 %}
    {% set savings_percent = 0 %}
  {% endif %}

  {% if savings_percent < 0 %}
    {% set savings_percent = 0 %}
  {% endif %}

  {# ---------- Styles ---------- #}

  <style>
    .rm-card {
      color: var(--primary-text-color);
      font-family: var(--paper-font-body1_-_font-family, inherit);
    }

    .rm-header {
      display: flex;
      align-items: center;
      gap: 12px;
      margin-bottom: 14px;
    }

    .rm-header-icon {
      display: flex;
      align-items: center;
      justify-content: center;
      width: 42px;
      height: 42px;
      flex: 0 0 42px;
      border-radius: 50%;
      color: var(--primary-color);
      background: color-mix(
        in srgb,
        var(--primary-color) 16%,
        transparent
      );
    }

    .rm-header-icon ha-icon {
      --mdc-icon-size: 25px;
    }

    .rm-heading {
      min-width: 0;
    }

    .rm-title {
      font-size: 16px;
      font-weight: 600;
      line-height: 1.25;
    }

    .rm-subtitle {
      margin-top: 2px;
      color: var(--secondary-text-color);
      font-size: 12px;
    }

    .rm-summary {
      display: grid;
      grid-template-columns: repeat(4, minmax(0, 1fr));
      gap: 8px;
      margin-bottom: 15px;
    }

    .rm-summary-item {
      min-width: 0;
      padding: 10px;
      border-radius: 13px;
      background: var(
        --ha-card-background,
        var(--card-background-color)
      );
      border: 1px solid var(--divider-color);
    }

    .rm-summary-label {
      overflow: hidden;
      color: var(--secondary-text-color);
      font-size: 10px;
      line-height: 1.2;
      text-overflow: ellipsis;
      text-transform: uppercase;
      white-space: nowrap;
    }

    .rm-summary-value {
      overflow: hidden;
      margin-top: 4px;
      font-size: 15px;
      font-weight: 600;
      line-height: 1.2;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .rm-good-text {
      color: var(--success-color, #43a047);
    }

    .rm-warning-text {
      color: var(--warning-color, #fb8c00);
    }

    .rm-danger-text {
      color: var(--error-color, #db4437);
    }

    .rm-table-wrap {
      width: 100%;
      overflow-x: auto;
      border-radius: 12px;
      border: 1px solid var(--divider-color);
    }

    .rm-table {
      width: 100%;
      min-width: 720px;
      border-collapse: collapse;
    }

    .rm-table th {
      padding: 9px 8px;
      color: var(--secondary-text-color);
      background: color-mix(
        in srgb,
        var(--primary-text-color) 4%,
        transparent
      );
      border-bottom: 1px solid var(--divider-color);
      font-size: 10px;
      font-weight: 600;
      letter-spacing: 0.03em;
      text-align: center;
      text-transform: uppercase;
    }

    .rm-table td {
      padding: 9px 8px;
      border-bottom: 1px solid var(--divider-color);
      font-size: 12px;
      text-align: center;
      vertical-align: middle;
    }

    .rm-table tbody tr:last-child td {
      border-bottom: 0;
    }

    .rm-table tbody tr:hover {
      background: color-mix(
        in srgb,
        var(--primary-color) 5%,
        transparent
      );
    }

    .rm-left {
      text-align: left !important;
    }

    .rm-day-cell {
      display: flex;
      align-items: center;
      gap: 9px;
      min-width: 125px;
    }

    .rm-day-icon {
      display: flex;
      align-items: center;
      justify-content: center;
      width: 30px;
      height: 30px;
      flex: 0 0 30px;
      border-radius: 50%;
      background: color-mix(
        in srgb,
        var(--primary-color) 12%,
        transparent
      );
      color: var(--primary-color);
    }

    .rm-day-icon ha-icon {
      --mdc-icon-size: 19px;
    }

    .rm-day-name {
      font-weight: 600;
      line-height: 1.2;
    }

    .rm-day-date {
      margin-top: 2px;
      color: var(--secondary-text-color);
      font-size: 10px;
      line-height: 1.2;
    }

    .rm-today {
      background: color-mix(
        in srgb,
        var(--primary-color) 10%,
        transparent
      );
    }

    .rm-skipped {
      background: color-mix(
        in srgb,
        var(--error-color, #db4437) 6%,
        transparent
      );
    }

    .rm-runtime {
      font-weight: 600;
      white-space: nowrap;
    }

    .rm-progress-wrap {
      min-width: 105px;
    }

    .rm-percent {
      margin-bottom: 4px;
      font-size: 11px;
      font-weight: 600;
    }

    .rm-progress-track {
      width: 100%;
      height: 7px;
      overflow: hidden;
      border-radius: 999px;
      background: color-mix(
        in srgb,
        var(--primary-text-color) 12%,
        transparent
      );
    }

    .rm-progress-fill {
      height: 100%;
      border-radius: inherit;
      background: var(--success-color, #43a047);
    }

    .rm-progress-fill.rm-progress-medium {
      background: var(--warning-color, #fb8c00);
    }

    .rm-progress-fill.rm-progress-low {
      background: var(--error-color, #db4437);
    }

    .rm-metrics {
      display: flex;
      flex-wrap: wrap;
      justify-content: center;
      gap: 4px;
      min-width: 130px;
    }

    .rm-metric {
      display: inline-flex;
      align-items: center;
      gap: 3px;
      padding: 3px 6px;
      border-radius: 999px;
      color: var(--secondary-text-color);
      background: color-mix(
        in srgb,
        var(--primary-text-color) 6%,
        transparent
      );
      font-size: 10px;
      white-space: nowrap;
    }

    .rm-metric ha-icon {
      --mdc-icon-size: 13px;
    }

    .rm-note {
      max-width: 190px;
      color: var(--secondary-text-color);
      font-size: 11px;
      line-height: 1.3;
    }

    .rm-note-active {
      color: var(--warning-color, #fb8c00);
      font-weight: 500;
    }

    .rm-empty {
      padding: 18px;
      color: var(--secondary-text-color);
      text-align: center;
    }

    .rm-error {
      padding: 14px;
      border-radius: 12px;
      color: var(--error-color, #db4437);
      background: color-mix(
        in srgb,
        var(--error-color, #db4437) 10%,
        transparent
      );
    }

    @media (max-width: 700px) {
      .rm-summary {
        grid-template-columns: repeat(2, minmax(0, 1fr));
      }

      .rm-table-wrap {
        overflow: visible;
        border: 0;
      }

      .rm-table {
        display: block;
        min-width: 0;
      }

      .rm-table thead {
        display: none;
      }

      .rm-table tbody {
        display: grid;
        gap: 10px;
      }

      .rm-table tr {
        display: block;
        overflow: hidden;
        border: 1px solid var(--divider-color);
        border-radius: 13px;
        background: var(
          --ha-card-background,
          var(--card-background-color)
        );
      }

      .rm-table td {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 12px;
        padding: 8px 11px;
        border-bottom: 1px solid var(--divider-color);
        text-align: right !important;
      }

      .rm-table td::before {
        content: attr(data-label);
        flex: 0 0 90px;
        color: var(--secondary-text-color);
        font-size: 10px;
        font-weight: 600;
        text-align: left;
        text-transform: uppercase;
      }

      .rm-table td:first-child {
        padding-top: 11px;
        padding-bottom: 11px;
      }

      .rm-table td:last-child {
        border-bottom: 0;
      }

      .rm-day-cell {
        width: 100%;
        justify-content: flex-end;
      }

      .rm-progress-wrap {
        width: min(170px, 55vw);
      }

      .rm-metrics {
        justify-content: flex-end;
      }

      .rm-note {
        max-width: min(220px, 55vw);
      }
    }
  </style>

  {# ---------- Card ---------- #}

  <div class="rm-card">

    <div class="rm-header">
      <div class="rm-header-icon">
        <ha-icon icon="mdi:sprinkler-variant"></ha-icon>
      </div>

      <div class="rm-heading">
        <div class="rm-title">
          {{ display_name }} — {{ lbl_title }}
        </div>

        <div class="rm-subtitle">
          {{ summary.active_days }} active
          watering day{{ '' if summary.active_days == 1 else 's' }}
          in the next {{ days_to_show }} days
        </div>
      </div>
    </div>

    {% if first_results | count == 0 %}

      <div class="rm-error">
        Entity not found: {{ first_entity }}
      </div>

    {% else %}

      <div class="rm-summary">

        <div class="rm-summary-item">
          <div class="rm-summary-label">
            {{ lbl_week_sched }}
          </div>
          <div class="rm-summary-value">
            {{ fmt_duration(summary.scheduled) }}
          </div>
        </div>

        <div class="rm-summary-item">
          <div class="rm-summary-label">
            {{ lbl_week_forecast }}
          </div>
          <div class="rm-summary-value">
            {{ fmt_duration(summary.computed) }}
          </div>
        </div>

        <div class="rm-summary-item">
          <div class="rm-summary-label">
            {{ lbl_savings }}
          </div>
          <div class="rm-summary-value
            {{ 'rm-good-text' if savings_percent > 0 else '' }}">
            {{ savings_percent }}%
          </div>
        </div>

        <div class="rm-summary-item">
          <div class="rm-summary-label">
            {{ lbl_next }}
          </div>
          <div class="rm-summary-value">
            {{ summary.next_day
               if summary.next_found
               else 'None' }}
          </div>
        </div>

      </div>

      <div class="rm-table-wrap">
        <table class="rm-table">

          <thead>
            <tr>
              <th class="rm-left">{{ lbl_day }}</th>
              <th>{{ lbl_sched }}</th>
              <th>{{ lbl_comp }}</th>
              <th>Runtime</th>
              <th>Weather</th>
              <th class="rm-left">{{ lbl_notes }}</th>
            </tr>
          </thead>

          <tbody>

            {% for i in range(days_to_show) %}

              {% set entity =
                'sensor.rainmachine_' ~
                program ~
                '_irrigation_forecast_' ~
                i
              %}

              {% set day_raw = state_attr(entity, 'day') %}

              {% set scheduled =
                state_attr(entity, 'scheduled_sec') | int(0)
              %}

              {% set computed =
                state_attr(entity, 'computed_sec') | int(0)
              %}

              {% if day_raw %}

                {% set day_date =
                  strptime(day_raw | string, '%Y-%m-%d').date()
                %}

                {% set delta = (day_date - today).days %}

                {% if delta == 0 %}
                  {% set day_label = lbl_today %}
                {% elif delta == 1 %}
                  {% set day_label = lbl_tomorrow %}
                {% else %}
                  {% set day_label =
                    day_names[day_date.weekday()]
                  %}
                {% endif %}

                {% if scheduled > 0 %}
                  {% set percent =
                    (computed / scheduled * 100)
                    | round(0)
                    | int
                  %}
                {% else %}
                  {% set percent = 0 %}
                {% endif %}

                {% if percent > 100 %}
                  {% set progress_width = 100 %}
                {% elif percent < 0 %}
                  {% set progress_width = 0 %}
                {% else %}
                  {% set progress_width = percent %}
                {% endif %}

                {# Collect unique watering flags #}

                {% set flags = namespace(items=[]) %}

                {% for zone in zones.items %}
                  {% set flag =
                    state_attr(
                      entity,
                      zone ~ '_watering_flag'
                    )
                  %}

                  {% if flag
                        and flag not in [
                          'unknown',
                          'unavailable',
                          'None'
                        ]
                        and flag not in flags.items %}
                    {% set flags.items =
                      flags.items + [flag]
                    %}
                  {% endif %}
                {% endfor %}

                {% set flag_text =
                  flags.items | join(', ')
                %}

                {% set flag_lower =
                  flag_text | lower
                %}

                {# Search for optional weather attributes #}

                {% set rain = namespace(value=none) %}
                {% for key in [
                  'rain_mm',
                  'precipitation_mm',
                  'precipitation',
                  'rain'
                ] %}
                  {% set candidate = state_attr(entity, key) %}
                  {% if rain.value is none
                        and candidate is not none %}
                    {% set rain.value = candidate %}
                  {% endif %}
                {% endfor %}

                {% set temperature = namespace(value=none) %}
                {% for key in [
                  'temperature',
                  'temp',
                  'forecast_temperature'
                ] %}
                  {% set candidate = state_attr(entity, key) %}
                  {% if temperature.value is none
                        and candidate is not none %}
                    {% set temperature.value = candidate %}
                  {% endif %}
                {% endfor %}

                {% set humidity = namespace(value=none) %}
                {% for key in [
                  'humidity',
                  'relative_humidity'
                ] %}
                  {% set candidate = state_attr(entity, key) %}
                  {% if humidity.value is none
                        and candidate is not none %}
                    {% set humidity.value = candidate %}
                  {% endif %}
                {% endfor %}

                {% set wind = namespace(value=none) %}
                {% for key in [
                  'wind_speed',
                  'wind',
                  'forecast_wind_speed'
                ] %}
                  {% set candidate = state_attr(entity, key) %}
                  {% if wind.value is none
                        and candidate is not none %}
                    {% set wind.value = candidate %}
                  {% endif %}
                {% endfor %}

                {% set et = namespace(value=none) %}
                {% for key in [
                  'et_mm',
                  'et',
                  'evapotranspiration',
                  'evapotranspiration_mm'
                ] %}
                  {% set candidate = state_attr(entity, key) %}
                  {% if et.value is none
                        and candidate is not none %}
                    {% set et.value = candidate %}
                  {% endif %}
                {% endfor %}

                {# Select status icon #}

                {% if 'rain' in flag_lower
                      or 'piogg' in flag_lower
                      or 'pluie' in flag_lower %}
                  {% set status_icon = 'mdi:weather-pouring' %}
                {% elif 'freeze' in flag_lower
                      or 'frost' in flag_lower
                      or 'gel' in flag_lower %}
                  {% set status_icon = 'mdi:snowflake-alert' %}
                {% elif 'wind' in flag_lower %}
                  {% set status_icon = 'mdi:weather-windy' %}
                {% elif scheduled > 0 and computed == 0 %}
                  {% set status_icon = 'mdi:water-off' %}
                {% elif percent < 50 and scheduled > 0 %}
                  {% set status_icon =
                    'mdi:weather-partly-rainy'
                  %}
                {% elif computed > 0 %}
                  {% set status_icon =
                    'mdi:sprinkler-variant'
                  %}
                {% else %}
                  {% set status_icon =
                    'mdi:weather-sunny'
                  %}
                {% endif %}

                {% set is_skipped =
                  scheduled > 0 and computed == 0
                %}

                <tr class="
                  {{ 'rm-today' if delta == 0 else '' }}
                  {{ 'rm-skipped' if is_skipped else '' }}
                ">

                  <td
                    class="rm-left"
                    data-label="{{ lbl_day }}"
                  >
                    <div class="rm-day-cell">
                      <div class="rm-day-icon">
                        <ha-icon
                          icon="{{ status_icon }}"
                        ></ha-icon>
                      </div>

                      <div>
                        <div class="rm-day-name">
                          {{ day_label }}
                        </div>

                        <div class="rm-day-date">
                          {{ day_date.strftime('%b %-d') }}
                        </div>
                      </div>
                    </div>
                  </td>

                  <td data-label="{{ lbl_sched }}">
                    <span class="rm-runtime">
                      {{ fmt_duration(scheduled) }}
                    </span>
                  </td>

                  <td data-label="{{ lbl_comp }}">
                    <span class="
                      rm-runtime
                      {% if scheduled > 0 and percent == 0 %}
                        rm-danger-text
                      {% elif scheduled > 0 and percent < 75 %}
                        rm-warning-text
                      {% elif computed > 0 %}
                        rm-good-text
                      {% endif %}
                    ">
                      {{ fmt_duration(computed) }}
                    </span>
                  </td>

                  <td data-label="Runtime">
                    <div class="rm-progress-wrap">

                      <div class="
                        rm-percent
                        {% if scheduled > 0 and percent == 0 %}
                          rm-danger-text
                        {% elif scheduled > 0 and percent < 75 %}
                          rm-warning-text
                        {% elif computed > 0 %}
                          rm-good-text
                        {% endif %}
                      ">
                        {{ percent }}%
                      </div>

                      <div class="rm-progress-track">
                        <div
                          class="
                            rm-progress-fill
                            {% if percent < 35 %}
                              rm-progress-low
                            {% elif percent < 75 %}
                              rm-progress-medium
                            {% endif %}
                          "
                          style="width: {{ progress_width }}%;"
                        ></div>
                      </div>

                    </div>
                  </td>

                  <td data-label="Weather">
                    <div class="rm-metrics">

                      {% if rain.value is not none %}
                        <span
                          class="rm-metric"
                          title="{{ lbl_rain }}"
                        >
                          <ha-icon
                            icon="mdi:weather-rainy"
                          ></ha-icon>
                          {{ rain.value }} mm
                        </span>
                      {% endif %}

                      {% if temperature.value is not none %}
                        <span
                          class="rm-metric"
                          title="{{ lbl_temp }}"
                        >
                          <ha-icon
                            icon="mdi:thermometer"
                          ></ha-icon>
                          {{ temperature.value }}°
                        </span>
                      {% endif %}

                      {% if humidity.value is not none %}
                        <span
                          class="rm-metric"
                          title="{{ lbl_humidity }}"
                        >
                          <ha-icon
                            icon="mdi:water-percent"
                          ></ha-icon>
                          {{ humidity.value }}%
                        </span>
                      {% endif %}

                      {% if wind.value is not none %}
                        <span
                          class="rm-metric"
                          title="{{ lbl_wind }}"
                        >
                          <ha-icon
                            icon="mdi:weather-windy"
                          ></ha-icon>
                          {{ wind.value }}
                        </span>
                      {% endif %}

                      {% if et.value is not none %}
                        <span
                          class="rm-metric"
                          title="{{ lbl_et }}"
                        >
                          <ha-icon
                            icon="mdi:leaf"
                          ></ha-icon>
                          {{ et.value }}
                        </span>
                      {% endif %}

                      {% if rain.value is none
                            and temperature.value is none
                            and humidity.value is none
                            and wind.value is none
                            and et.value is none %}
                        <span class="rm-metric">
                          <ha-icon
                            icon="{{ status_icon }}"
                          ></ha-icon>
                          {{ percent }}%
                        </span>
                      {% endif %}

                    </div>
                  </td>

                  <td
                    class="rm-left"
                    data-label="{{ lbl_notes }}"
                  >
                    {% if flag_text %}
                      <div class="rm-note rm-note-active">
                        {{ flag_text }}
                      </div>
                    {% elif is_skipped %}
                      <div class="rm-note rm-note-active">
                        Watering skipped
                      </div>
                    {% elif scheduled == 0 %}
                      <div class="rm-note">
                        Not scheduled
                      </div>
                    {% elif computed < scheduled %}
                      <div class="rm-note rm-note-active">
                        Runtime reduced by
                        {{ 100 - percent }}%
                      </div>
                    {% else %}
                      <div class="rm-note">
                        Full scheduled runtime
                      </div>
                    {% endif %}
                  </td>

                </tr>

              {% endif %}
            {% endfor %}

          </tbody>
        </table>
      </div>

      {% if summary.active_days == 0 %}
        <div class="rm-empty">
          <ha-icon icon="mdi:water-off"></ha-icon>
          {{ lbl_no_watering }}
        </div>
      {% endif %}

    {% endif %}
  </div>