[Blueprint] Weather Forecast Alerts (TTS + Optional AI & Optional Iphone Alarm Integration ) Your Own In home Weatherman

No Additional Weather Integration Needed & Ai is Optional!
This automation uses the weather entity built into home assistant to read weather forecast alerts with option choice to pass the response through an ai of your choice. Took me a while to figure out the weather entity but hope you enjoy this automation. I notice people were having trouble doing this so here it is!

Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.


Overview

Weather — Forecast Alerts speaks a short, broadcast-style forecast to your speakers on a schedule you choose.
It pulls daily + hourly data, calls out rain/snow timing with daypart and %, and can optionally pass the text through an AI Task for a crisp TV-meteorologist read. A per-speaker TTS pre-roll (ms) prevents clipped openings on slow-to-wake speakers.


Feature highlights
  • :stopwatch: Schedule: Time pattern (e.g., every hour at :03), with active hours and weekdays.
  • :thermometer: Smart summary: Current condition, temp, humidity, today’s high/low.
  • :umbrella: Precip timing: Scans upcoming hours; mentions first hour ≥ threshold with daypart, time, and %.
  • :brain: AI (optional): Rewrites the message with strict “don’t change facts” rules.
  • :loud_sound: Per-speaker pre-roll: Insert 0–300 ms silence before speaking to avoid choppy starts.
  • :speaking_head: TTS-friendly: Works great with tts.piper; voice selectable (e.g., en_US-carlin-high).

Requirements
  • A weather entity that supports weather.get_forecasts (daily + hourly).
  • A TTS platform that exposes tts.speak (e.g., Piper).
  • (Optional) An AI Task entity (e.g., ai_task.google_ai_task_2) if you want the rewrite step.

Setup (quick)
  1. Click the Import badge above and save the blueprint.
  2. Create an automation from this blueprint and fill the inputs (see “Inputs explained”).
  3. Test with Run in the automation editor; tweak Pre-roll (ms) if the first word clips.

Inputs explained
  • Weather entity (with forecast) — Your weather.* entity. Must support daily + hourly forecast services.
  • Trigger — hour pattern — Time pattern hours; /1 = hourly, /2 = every 2 hours, or a fixed hour like 9.
  • Trigger — minute of hour — Minute string 00–59 (e.g., "3" → runs at HH:03).
  • Active from / until — Daily time window (local) when announcements are allowed.
  • Active weekdays — Days the schedule is allowed to run.
  • Precipitation probability threshold (%) — Minimum hourly % within scan window to announce rain/snow.
  • Hours ahead to scan for precip — How many upcoming hours (1–48) to search for the first qualifying hour.
  • Use AI rewrite — On = send to AI Task for a clean, natural rewrite; Off = speak the raw message.
  • AI Task entity — Your ai_task.* entity (only required if AI rewrite is On).
  • AI rewrite instructions — Preset prompt enforcing no fact changes and raw text output.
  • TTS engine entity — Your tts.* entity (e.g., tts.piper).
  • Speaker(s) — One or more media_player.* entities to speak through.
  • Pre-speak volume — Set volume on all selected speakers just before speaking.
  • Cache TTS audio — Allow engine to reuse identical audio (if supported).
  • TTS voice — Voice identifier (e.g., Piper en_US-carlin-high); ignored if engine doesn’t support voices.
  • TTS pre-roll (ms) — Inserted delay (0–300 ms, default 150) before each tts.speak to stop clipped intros.

Preview (example output)

“Good evening! Saturday’s forecast. Right now: Clear night, 58°, humidity 75%. Expect sunny. High 70°. Low 58°. Rain chances this afternoon around 3 PM: 60%.”
(Exact numbers/times depend on your weather provider.)


Advanced tuning
  • If your speaker still clips, raise Pre-roll to ~200–250 ms.
  • Lower Precip threshold (e.g., 30%) to get more frequent rain/snow call-outs.
  • Shorten Hours ahead if you only care about the next work block.
  • Want pure local? Turn AI rewrite Off; the base message is already concise and structured.

Troubleshooting
  • No forecast / missing precip: Your provider might not expose hourly or % data. Try a different weather integration.
  • AI step fails: Ensure your ai_task.* entity exists and is reachable. If your provider expects prompt instead of instructions, rename the key in the blueprint. The automation falls back to the non-AI message if AI returns nothing.
  • Choppy first word: Increase TTS pre-roll; some soundbars or cast devices wake slowly.
  • Wrong voice: Some TTS engines ignore the voice option; that’s expected behavior.

Credits & links
  • Blueprint repo: zodyking/weather-forecast-alert-blueprint
  • Banner art in repo (imageforecast.png).
  • Thanks to the HA community for feedback and to Piper for speedy local TTS.

If you want, I can also drop a mini badge set for quick access (edit/automations/logbook) like in your other thread.

1 Like

Looks very promising! I “installed”, but I think you are missing an error trap…I turned the “AI rewrite” switch off, but entered all the other inputs correctly…and get the error message “Message Malformed: Missing input ai_task_entry”…

While using the ai mode its very fine you can use Google Gemini’s API which is free for the meantime.

Ill diagnose it today and fix it, sorry for inconvenience. Also post a picture of the trace if possible.

As of now i can see one mistake, that forces you to have an AI Task entity, i want to make sure thats the only bug

Weather Forecast TTS Blueprint Update: New Features & Fixes

Hey Home Assistant community!

I’ve updated my Weather Forecast TTS (AI Optional) blueprint based on user feedback and testing.

New Features

  • Optional AI Rewrite for All TTS: If “Use AI rewrite” is enabled, it now reformats all TTS messages (forecasts, current changes, upcoming alerts) via your AI Task entity keeps facts intact, outputs clean text.
  • Time-Based Forecast Toggle: New boolean input to enable/disable scheduled (hourly) forecasts without affecting other triggers.
  • Presence-Triggered Forecasts: Toggle to announce full forecasts when selected presence sensors (binary_sensor) turn ‘on’ (e.g., someone arrives home). Accepts no sensors without error.
  • Current Weather Change Announcements: Toggle to announce any condition change (e.g., “Weather update: conditions have changed to partly cloudy.”) in weatherman style.
  • Upcoming Precipitation Alerts: Toggle for announcements minutes before forecasted rain/snow (configurable 1-30 min), e.g., “Weather alert: rain expected in about 5 minutes.”

Fixes

  • AI Task Optional: No longer requires selection; skips if blank or disabled, no errors.
    ( @area_49 fixed your reported issue.)

  • General Stability: Fixed selectattr typos, ensured all inputs accept no value, improved trigger/condition logic for robustness.

Grab the updated blueprint YAML from the repo and import it let me know if you spot issues so i can fix them asap! This is my fav blueprint and have so much planned for it!


Future Features

  • Iphone Alarm Trigger: When you take your phone out of sleep mode/turn off wake up alarm you can route a weather briefing to a media player. I had this setup via node red but transitioning it to the blueprint soon :slight_smile: (Requires User Setup Via Iphone Which Is Why I Didnt Include It Initially)
  • More Detailed Forecast: Optional multiday forecast
    Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.

Thanks, zodyking

Hey Home Assistant Community! zodyking here with a big update to the Weather Forecast TTS (AI Optional) blueprint. This version brings on-demand wake-up weather reports via webhook, a cleaner collapsible UI, independent volume controls for every announcement type, and full iPhone alarm integration using the Shortcuts app. Utilizing the webhook trigger feature you can this blueprint allows you send a webhook from your iphone that triggers a morning weather briefing when your wake-up alarm goes off. (not sure how to setup on android as i dont use android but share for others to benefit)


New UI Layout – Collapsible Sections


All configuration is now neatly organized into collapsible sections with icons and descriptions:

Section Icon Purpose
Weather Data :comet: Select your weather entity
TTS Settings :speaking_head: Engine, speakers, voice, volume, pre-roll
AI Rephrasing (Optional) :robot: Natural TV-meteorologist rephrasing
Time-Based Announcements :alarm_clock: Scheduled forecasts (hourly, every 3h, etc.)
Sensor Trigger Announcements :walking_man: Play based on presence or any kind of sensor
Alarm Based Announcement :alarm_clock: NEW – Webhook-triggered wake-up report
Change Announcements :cloud::warning: Real-time & upcoming weather shifts
Precipitation Settings :cloud_with_rain: Thresholds & forecast window

All sections collapse by default — expand only what you need!


New Feature: Alarm Based Announcement (Wake-Up Weather)

Now you can trigger a personalized weather report when your iPhone alarm goes offno extra apps, no cloud.

How It Works

  1. Enable “Alarm Based Announcement” in the blueprint.
  2. Set your Webhook ID (e.g., wakeup_alarm).
  3. Enter your name (e.g., Brandon).
  4. Adjust Alarm Volume Level (independent from others).

Webhook URL: http://homeassistant.local:8123/api/webhook/wakeup_alarm


How to Set Up with iPhone (Step-by-Step)

  1. Open Shortcuts app → Automation+
  2. Tap “When my wake-up alarm goes off”Next
  3. Add Action → Search “Get Contents of URL”
  4. Enter:
  1. Tap Done → Name it → Done



That’s it! When your alarm rings, HA speaks: “Good morning Joseph, it’s 7:00 AM. Currently 65 degrees and sunny. High 75, low 55.”

(Precipitation mentioned only if ≥ threshold and in forecast.)


New: Per-Trigger Volume Controls

No more one-size-fits-all volume!

Trigger Type Volume Slider
Time-Based / Sensor Volume Level
Current Change Current Change Volume Level
Upcoming Change Upcoming Change Volume Level
Alarm Webhook Alarm Volume Level

AI Rewrite – Now Smarter & Greeting-Safe

  • Greeting is NEVER removed — “Good morning [Name],” is appended AFTER AI processing.
  • AI only rewrites the forecast content — preserves facts, timing, and style.
  • Example (with AI):

“Good morning Brandon. It’s 7:00 AM. Currently 65 degrees and sunny. High 75, low 55.”


Try it out and let me know how it works with your iPhone alarm! Feedback, feature requests, or issues? Drop a comment below or open an issue on GitHub.

Thanks @zodyking this is great! Any plans to add a sentence trigger with a set conversation response action? That way I could ask for the weather report and get a response from wherever I asked.

Here is a rough example, I just copied the alarm_webhook actions, probably not correct but as a proof of concept it works, lol.
alias: Weather Test
description: ""
triggers:
  - trigger: time_pattern
    hours: /3
    minutes: "3"
    id: time_based
  - trigger: state
    entity_id: []
    to: "on"
    id: sensor_trigger
  - trigger: state
    entity_id: weather.forecast_home
    id: current_change
  - trigger: time_pattern
    minutes: /5
    id: upcoming_change
  - trigger: webhook
    allowed_methods:
      - POST
      - PUT
      - GET
      - HEAD
    local_only: true
    webhook_id: wakeup_alarm
    id: alarm_webhook
  - trigger: conversation
    command:
      - weather report
      - what's the weather like
    id: sentence
conditions:
  - condition: or
    conditions:
      - condition: and
        conditions:
          - condition: trigger
            id: time_based
          - condition: time
            after: "08:00:00"
            before: "21:00:00"
          - condition: template
            value_template: "{{ enable_time_based }}"
          - condition: template
            value_template: >-
              {{ not days_of_week or now().strftime('%a').lower()[:3] in
              days_of_week }}
      - condition: and
        conditions:
          - condition: trigger
            id: sensor_trigger
          - condition: template
            value_template: "{{ enable_sensor_triggered }}"
      - condition: and
        conditions:
          - condition: trigger
            id: current_change
          - condition: template
            value_template: "{{ enable_current_change_announce }}"
      - condition: and
        conditions:
          - condition: trigger
            id: upcoming_change
          - condition: template
            value_template: "{{ enable_upcoming_change_announce }}"
      - condition: and
        conditions:
          - condition: trigger
            id: alarm_webhook
          - condition: template
            value_template: "{{ enable_alarm_announce }}"
      - condition: trigger
        id:
          - sentence
actions:
  - variables:
      key: weather.forecast_home
      spoken: ""
  - choose:
      - conditions:
          - condition: or
            conditions:
              - condition: trigger
                id: time_based
              - condition: trigger
                id: sensor_trigger
        sequence:
          - action: weather.get_forecasts
            target:
              entity_id: weather.forecast_home
            data:
              type: daily
            response_variable: d
          - action: weather.get_forecasts
            target:
              entity_id: weather.forecast_home
            data:
              type: hourly
            response_variable: h
          - variables:
              precip_threshold: 30
              hours_ahead: 24
              now_hour: "{{ now().hour }}"
              day_name: "{{ (now() | as_timestamp | timestamp_custom('%A', true)) }}"
              greeting: >-
                {% if 5 <= now_hour <= 11 %}Good morning{% elif 12 <= now_hour
                <= 17 %} Good afternoon{% else %}Good evening{% endif %}
              now_temp: "{{ state_attr(key,'temperature') }}"
              now_hum: "{{ state_attr(key,'humidity') }}"
              now_cond_h: >-
                {% set s = (states(key) or '') | lower %} {% set s = s |
                replace('_',' ') | replace('-',' ') %} {% set s = s |
                replace('partlycloudy','partly cloudy') |
                replace('clearnight','clear night') %} {{ s | trim }}
              daily_list: "{{ d.get(key,{}).get('forecast',[]) }}"
              today: "{{ daily_list[0] if daily_list|length>0 else dict() }}"
              high: "{{ today.get('temperature') }}"
              low: "{{ today.get('templow', today.get('temperature_low')) }}"
              today_cond_h: >-
                {% set raw = (today.get('condition') or states(key) or '') |
                lower %} {% set s = raw | replace('_',' ') | replace('-',' ') %}
                {% set s = s | replace('partlycloudy','partly cloudy') |
                replace('clearnight','clear night') %} {{ s | trim }}
              now_norm: "{{ now_cond_h | replace(' ','') }}"
              today_norm: "{{ today_cond_h | replace(' ','') }}"
              mention_expect: "{{ today_cond_h != '' and today_norm != now_norm }}"
              hourly_list: "{{ h.get(key,{}).get('forecast',[]) }}"
              next_hours: "{{ hourly_list[:hours_ahead] if hourly_list else [] }}"
              hits: >-
                {{ (next_hours |
                selectattr('precipitation_probability','defined') |
                selectattr('precipitation_probability','ge', precip_threshold) |
                list) }}
              first_hit: "{{ hits[0] if hits|length>0 else dict() }}"
              hit_pp: "{{ first_hit.get('precipitation_probability') }}"
              hit_cond_h: >-
                {% set raw = (first_hit.get('condition','') | lower) %} {% set s
                = raw | replace('_',' ') | replace('-',' ') %} {% set s = s |
                replace('partlycloudy','partly cloudy') |
                replace('clearnight','clear night') %} {{ s | trim }}
              hit_ts: >-
                {% set t = none %} {% if first_hit.get('datetime') %} {% set t =
                as_datetime(first_hit.get('datetime')) | as_timestamp %} {%
                endif %} {{ t }}
              hit_clock: >-
                {% set out = '' %} {% if hit_ts is not none %} {% set out =
                (hit_ts | timestamp_custom('%I %p', true)) | replace(' 0',' ')
                %} {% endif %} {{ out }}
              hit_hour_num: >-
                {% set out = 0 %} {% if hit_ts is not none %} {% set out =
                (hit_ts | timestamp_custom('%H', true)) | int %} {% endif %} {{
                out }}
              hit_daypart: >-
                {% set hnum = hit_hour_num %} {% if 5 <= hnum <= 11 %}this
                morning{% elif 12 <= hnum <= 17 %}this afternoon {% elif 18 <=
                hnum <= 22 %}this evening{% else %}overnight{% endif %}
              precip_kind: >-
                {% if 'snow' in hit_cond_h %}snow{% elif 'sleet' in hit_cond_h
                or 'hail' in hit_cond_h %}mixed precipitation {% elif hit_cond_h
                %}rain{% else %}precipitation{% endif %}
              msg: >-
                {% set p = [] %} {% set p = p + [greeting ~ '! ' ~ day_name ~
                "'s forecast."] %} {% set now_bits = [] %} {% if now_cond_h %}{%
                set now_bits = now_bits + [now_cond_h | capitalize] %}{% endif
                %} {% if now_temp is not none %}{% set now_bits = now_bits + [
                (now_temp | round(0) ~ ' degrees') ] %}{% endif %} {% if now_hum
                is not none %}{% set now_bits = now_bits + [ ('humidity ' ~
                now_hum ~ '%') ] %}{% endif %} {% if now_bits | length > 0 %}{%
                set p = p + ['Right now: ' ~ (now_bits | join(', ')) ~ '.'] %}{%
                endif %} {% set today_bits = [] %} {% if mention_expect %}{% set
                today_bits = today_bits + ['Expect ' ~ today_cond_h ~ '.'] %}{%
                endif %} {% if high is not none %}{% set today_bits = today_bits
                + ['High ' ~ (high | round(0)) ~ ' degrees.'] %}{% endif %} {%
                if low is not none %}{% set today_bits = today_bits + ['Low ' ~
                (low | round(0)) ~ ' degrees.'] %}{% endif %} {% if today_bits |
                length > 0 %}{% set p = p + [ today_bits | join(' ') ] %}{%
                endif %} {% if hits | length > 0 and hit_pp is not none %} {%
                set phr = precip_kind ~ ' chances ' ~ hit_daypart %} {% if
                hit_clock %}{% set phr = phr ~ ' around ' ~ hit_clock %}{% endif
                %} {% set phr = phr ~ ': ' ~ (hit_pp | int) ~ '%.' %} {% set p =
                p + [ phr ] %} {% endif %} {{ p | join(' ') if p|length > 0 else
                'Forecast not available.' }}
              i_ai: false
              i_ai_task: ""
              i_ai_text: >
                Rewrite the message below for clear, natural TTS in a crisp
                TV-meteorologist voice.

                Hard rules:

                - Preserve every fact exactly: all numbers, units (degrees, %,
                mph), times, day names, and weather terms.

                - Do not invent, omit, reorder, round, or restate any facts.
                Keep original sequencing of info.

                - Output RAW TEXT ONLY — no quotes, code fences, YAML/JSON,
                greetings, preambles, or sign-offs.

                - 2–4 short sentences, ≤55 words, plain vocabulary, no emojis,
                no hashtags, no ALL CAPS.

                - If precipitation timing/probability exists, mention it once
                using the same daypart/time and %; otherwise omit.

                - Keep symbols and formatting as given (%, AM/PM). If the input
                is “Forecast not available.” return it unchanged.
          - choose:
              - conditions:
                  - condition: template
                    value_template: "{{ i_ai and (i_ai_task | default('') | string) != '' }}"
                sequence:
                  - action: ai_task.generate_data
                    data:
                      entity_id: "{{ i_ai_task }}"
                      task_name: weather message
                      instructions: |-
                        {{ i_ai_text }}
                        Message:
                        "{{ msg | replace('"','\\"') }}"
                    response_variable: aimsg
                  - variables:
                      spoken: "{{ aimsg.data | default(msg) }}"
            default:
              - variables:
                  spoken: "{{ msg }}"
          - action: media_player.volume_set
            target:
              entity_id:
                - media_player.home_assistant_voice_0a2aa1_media_player
            data:
              volume_level: 0.6
          - repeat:
              for_each:
                - media_player.home_assistant_voice_0a2aa1_media_player
              sequence:
                - delay:
                    milliseconds: 150
                - action: tts.speak
                  target:
                    entity_id: tts.home_assistant_cloud
                  data:
                    cache: true
                    media_player_entity_id: "{{ repeat.item }}"
                    message: "{{ spoken | trim }}"
                    options:
                      voice: en_US-carlin-high
      - conditions:
          - condition: trigger
            id: current_change
        sequence:
          - variables:
              now_cond_raw: "{{ trigger.to_state.state | lower }}"
              now_cond_h: >-
                {% set s = now_cond_raw %} {% set s = s | replace('_',' ') |
                replace('-',' ') %} {% set s = s |
                replace('partlycloudy','partly cloudy') |
                replace('clearnight','clear night') %} {{ s | trim }}
              from_cond_raw: "{{ trigger.from_state.state | default('unknown') | lower }}"
              now_temp: "{{ trigger.to_state.attributes.temperature | round(0) }}"
              is_change: "{{ now_cond_raw != from_cond_raw }}"
              msg: >-
                Weather update: It's now {{ now_temp }} degrees and {{
                now_cond_h }}.
              i_ai: false
              i_ai_task: ""
              i_ai_text: >
                Rewrite the message below for clear, natural TTS in a crisp
                TV-meteorologist voice.

                Hard rules:

                - Preserve every fact exactly: all numbers, units (degrees, %,
                mph), times, day names, and weather terms.

                - Do not invent, omit, reorder, round, or restate any facts.
                Keep original sequencing of info.

                - Output RAW TEXT ONLY — no quotes, code fences, YAML/JSON,
                greetings, preambles, or sign-offs.

                - 2–4 short sentences, ≤55 words, plain vocabulary, no emojis,
                no hashtags, no ALL CAPS.

                - If precipitation timing/probability exists, mention it once
                using the same daypart/time and %; otherwise omit.

                - Keep symbols and formatting as given (%, AM/PM). If the input
                is “Forecast not available.” return it unchanged.
          - if:
              - condition: template
                value_template: "{{ is_change }}"
            then:
              - choose:
                  - conditions:
                      - condition: template
                        value_template: >-
                          {{ i_ai and (i_ai_task | default('') | string) != ''
                          }}
                    sequence:
                      - action: ai_task.generate_data
                        data:
                          entity_id: "{{ i_ai_task }}"
                          task_name: weather message
                          instructions: |-
                            {{ i_ai_text }}
                            Message:
                            "{{ msg | replace('"','\\"') }}"
                        response_variable: aimsg
                      - variables:
                          spoken: "{{ aimsg.data | default(msg) }}"
                default:
                  - variables:
                      spoken: "{{ msg }}"
              - action: media_player.volume_set
                target:
                  entity_id:
                    - media_player.home_assistant_voice_0a2aa1_media_player
                data:
                  volume_level: 0.6
              - repeat:
                  for_each:
                    - media_player.home_assistant_voice_0a2aa1_media_player
                  sequence:
                    - delay:
                        milliseconds: 150
                    - action: tts.speak
                      target:
                        entity_id: tts.home_assistant_cloud
                      data:
                        cache: true
                        media_player_entity_id: "{{ repeat.item }}"
                        message: "{{ spoken | trim }}"
                        options:
                          voice: en_US-carlin-high
      - conditions:
          - condition: trigger
            id: upcoming_change
        sequence:
          - action: weather.get_forecasts
            target:
              entity_id: weather.forecast_home
            data:
              type: hourly
            response_variable: h
          - variables:
              hourly_list: "{{ h.get(key,{}).get('forecast',[]) }}"
              upcoming: >-
                {{ hourly_list | selectattr('datetime','gt', now().isoformat())
                | sort(attribute='datetime') | list }}
              next_hit: "{{ upcoming[0] if upcoming else {} }}"
              next_dt: "{{ next_hit.get('datetime') }}"
              next_ts: "{{ as_timestamp(next_dt) if next_dt else none }}"
              min_to: >-
                {{ ((next_ts - as_timestamp(now())) / 60) | round(0) if next_ts
                else 0 }}
              next_cond_raw: "{{ next_hit.get('condition','') | lower }}"
              next_cond_h: >-
                {% set s = next_cond_raw %} {% set s = s | replace('_',' ') |
                replace('-',' ') %} {% set s = s |
                replace('partlycloudy','partly cloudy') |
                replace('clearnight','clear night') %} {{ s | trim }}
              now_cond_raw: "{{ states(key) | lower }}"
              now_is_precip: >-
                {{ 'rain' in now_cond_raw or 'snow' in now_cond_raw or 'hail' in
                now_cond_raw or 'sleet' in now_cond_raw or 'pour' in
                now_cond_raw or 'lightning' in now_cond_raw }}
              next_is_precip: >-
                {{ 'rain' in next_cond_raw or 'snow' in next_cond_raw or 'hail'
                in next_cond_raw or 'sleet' in next_cond_raw or 'pour' in
                next_cond_raw or 'lightning' in next_cond_raw }}
              should_announce: >-
                {{ next_ts is not none and next_is_precip and not now_is_precip
                and 0 < min_to <= minutes_before_announce }}
              precip_kind: >-
                {% if 'snow' in next_cond_h %}snow{% elif 'sleet' in next_cond_h
                or 'hail' in next_cond_h %}mixed precipitation {% elif
                next_cond_h %}rain{% else %}precipitation{% endif %}
              msg: >-
                Weather alert: {{ precip_kind }} expected in about {{ min_to }}
                minutes.
              i_ai: false
              i_ai_task: ""
              i_ai_text: >
                Rewrite the message below for clear, natural TTS in a crisp
                TV-meteorologist voice.

                Hard rules:

                - Preserve every fact exactly: all numbers, units (degrees, %,
                mph), times, day names, and weather terms.

                - Do not invent, omit, reorder, round, or restate any facts.
                Keep original sequencing of info.

                - Output RAW TEXT ONLY — no quotes, code fences, YAML/JSON,
                greetings, preambles, or sign-offs.

                - 2–4 short sentences, ≤55 words, plain vocabulary, no emojis,
                no hashtags, no ALL CAPS.

                - If precipitation timing/probability exists, mention it once
                using the same daypart/time and %; otherwise omit.

                - Keep symbols and formatting as given (%, AM/PM). If the input
                is “Forecast not available.” return it unchanged.
          - if:
              - condition: template
                value_template: "{{ should_announce }}"
            then:
              - choose:
                  - conditions:
                      - condition: template
                        value_template: >-
                          {{ i_ai and (i_ai_task | default('') | string) != ''
                          }}
                    sequence:
                      - action: ai_task.generate_data
                        data:
                          entity_id: "{{ i_ai_task }}"
                          task_name: weather message
                          instructions: |-
                            {{ i_ai_text }}
                            Message:
                            "{{ msg | replace('"','\\"') }}"
                        response_variable: aimsg
                      - variables:
                          spoken: "{{ aimsg.data | default(msg) }}"
                default:
                  - variables:
                      spoken: "{{ msg }}"
              - action: media_player.volume_set
                target:
                  entity_id:
                    - media_player.home_assistant_voice_0a2aa1_media_player
                data:
                  volume_level: 0.6
              - repeat:
                  for_each:
                    - media_player.home_assistant_voice_0a2aa1_media_player
                  sequence:
                    - delay:
                        milliseconds: 150
                    - action: tts.speak
                      target:
                        entity_id: tts.home_assistant_cloud
                      data:
                        cache: true
                        media_player_entity_id: "{{ repeat.item }}"
                        message: "{{ spoken | trim }}"
                        options:
                          voice: en_US-carlin-high
      - conditions:
          - condition: trigger
            id: alarm_webhook
        sequence:
          - action: weather.get_forecasts
            target:
              entity_id: weather.forecast_home
            data:
              type: daily
            response_variable: d
          - action: weather.get_forecasts
            target:
              entity_id: weather.forecast_home
            data:
              type: hourly
            response_variable: h
          - variables:
              name: ""
              current_time: >-
                {{ as_timestamp(now()) | timestamp_custom('%I:%M %p', true) |
                replace(' 0', ' ') }}
              now_temp: "{{ state_attr(key,'temperature') | round(0) }}"
              now_cond_h: >-
                {% set s = (states(key) or '') | lower %} {% set s = s |
                replace('_',' ') | replace('-',' ') %} {% set s = s |
                replace('partlycloudy','partly cloudy') |
                replace('clearnight','clear night') %} {{ s | trim }}
              daily_list: "{{ d.get(key,{}).get('forecast',[]) }}"
              today: "{{ daily_list[0] if daily_list|length>0 else dict() }}"
              high: "{{ today.get('temperature') | round(0) }}"
              low: >-
                {{ today.get('templow', today.get('temperature_low')) | round(0)
                }}
              hourly_list: "{{ h.get(key,{}).get('forecast',[]) }}"
              hits: >-
                {{ (hourly_list |
                selectattr('precipitation_probability','defined') |
                selectattr('precipitation_probability','ge', precip_threshold) |
                list) }}
              first_hit: "{{ hits[0] if hits|length>0 else dict() }}"
              hit_pp: >-
                {{ first_hit.get('precipitation_probability') | int(default=0)
                }}
              hit_cond_h: >-
                {% set raw = (first_hit.get('condition','') | lower) %} {% set s
                = raw | replace('_',' ') | replace('-',' ') %} {% set s = s |
                replace('partlycloudy','partly cloudy') |
                replace('clearnight','clear night') %} {{ s | trim }}
              hit_ts: >-
                {% set t = none %} {% if first_hit.get('datetime') %} {% set t =
                as_datetime(first_hit.get('datetime')) | as_timestamp %} {%
                endif %} {{ t }}
              hit_clock: >-
                {% set out = '' %} {% if hit_ts is not none %} {% set out =
                (hit_ts | timestamp_custom('%I %p', true)) | replace(' 0',' ')
                %} {% endif %} {{ out }}
              precip_kind: >-
                {% if 'snow' in hit_cond_h %}snow{% elif 'sleet' in hit_cond_h
                or 'hail' in hit_cond_h %}mixed precipitation {% elif hit_cond_h
                %}rain{% else %}precipitation{% endif %}
              precip_msg: >-
                {% if hits | length > 0 and hit_pp > 0 %}expect {{ precip_kind
                }} later in the day around {{ hit_clock }} with a {{ hit_pp }}%
                chance.{% else %}{% endif %}
              msg: >-
                here's your forecast. It's {{ current_time }}. Currently {{
                now_temp }} degrees and {{ now_cond_h }}. {{ precip_msg }} High
                {{ high }} degrees. Low {{ low }} degrees.
              i_ai: false
              i_ai_task: ""
              i_ai_text: >
                Rewrite the message below for clear, natural TTS in a crisp
                TV-meteorologist voice.

                Hard rules:

                - Preserve every fact exactly: all numbers, units (degrees, %,
                mph), times, day names, and weather terms.

                - Do not invent, omit, reorder, round, or restate any facts.
                Keep original sequencing of info.

                - Output RAW TEXT ONLY — no quotes, code fences, YAML/JSON,
                greetings, preambles, or sign-offs.

                - 2–4 short sentences, ≤55 words, plain vocabulary, no emojis,
                no hashtags, no ALL CAPS.

                - If precipitation timing/probability exists, mention it once
                using the same daypart/time and %; otherwise omit.

                - Keep symbols and formatting as given (%, AM/PM). If the input
                is “Forecast not available.” return it unchanged.
          - choose:
              - conditions:
                  - condition: template
                    value_template: "{{ i_ai and (i_ai_task | default('') | string) != '' }}"
                sequence:
                  - action: ai_task.generate_data
                    data:
                      entity_id: "{{ i_ai_task }}"
                      task_name: weather message
                      instructions: |-
                        {{ i_ai_text }}
                        Message:
                        "{{ msg | replace('"','\\"') }}"
                    response_variable: aimsg
                  - variables:
                      spoken: "{{ aimsg.data | default(msg) }}"
            default:
              - variables:
                  spoken: "{{ msg }}"
          - variables:
              spoken_final: Good morning {{ name }}, {{ spoken }}
          - action: media_player.volume_set
            target:
              entity_id:
                - media_player.home_assistant_voice_0a2aa1_media_player
            data:
              volume_level: 0.6
          - repeat:
              for_each:
                - media_player.home_assistant_voice_0a2aa1_media_player
              sequence:
                - delay:
                    milliseconds: 150
                - action: tts.speak
                  target:
                    entity_id: tts.home_assistant_cloud
                  data:
                    cache: true
                    media_player_entity_id: "{{ repeat.item }}"
                    message: "{{ spoken_final | trim }}"
                    options:
                      voice: en_US-carlin-high
      - conditions:
          - condition: trigger
            id:
              - sentence
        sequence:
          - action: weather.get_forecasts
            target:
              entity_id: weather.forecast_home
            data:
              type: daily
            response_variable: d
          - action: weather.get_forecasts
            target:
              entity_id: weather.forecast_home
            data:
              type: hourly
            response_variable: h
          - variables:
              name: ""
              current_time: >-
                {{ as_timestamp(now()) | timestamp_custom('%I:%M %p', true) |
                replace(' 0', ' ') }}
              now_temp: "{{ state_attr(key,'temperature') | round(0) }}"
              now_cond_h: >-
                {% set s = (states(key) or '') | lower %} {% set s = s |
                replace('_',' ') | replace('-',' ') %} {% set s = s |
                replace('partlycloudy','partly cloudy') |
                replace('clearnight','clear night') %} {{ s | trim }}
              daily_list: "{{ d.get(key,{}).get('forecast',[]) }}"
              today: "{{ daily_list[0] if daily_list|length>0 else dict() }}"
              high: "{{ today.get('temperature') | round(0) }}"
              low: >-
                {{ today.get('templow', today.get('temperature_low')) | round(0)
                }}
              hourly_list: "{{ h.get(key,{}).get('forecast',[]) }}"
              hits: >-
                {{ (hourly_list |
                selectattr('precipitation_probability','defined') |
                selectattr('precipitation_probability','ge', precip_threshold) |
                list) }}
              first_hit: "{{ hits[0] if hits|length>0 else dict() }}"
              hit_pp: >-
                {{ first_hit.get('precipitation_probability') | int(default=0)
                }}
              hit_cond_h: >-
                {% set raw = (first_hit.get('condition','') | lower) %} {% set s
                = raw | replace('_',' ') | replace('-',' ') %} {% set s = s |
                replace('partlycloudy','partly cloudy') |
                replace('clearnight','clear night') %} {{ s | trim }}
              hit_ts: >-
                {% set t = none %} {% if first_hit.get('datetime') %} {% set t =
                as_datetime(first_hit.get('datetime')) | as_timestamp %} {%
                endif %} {{ t }}
              hit_clock: >-
                {% set out = '' %} {% if hit_ts is not none %} {% set out =
                (hit_ts | timestamp_custom('%I %p', true)) | replace(' 0',' ')
                %} {% endif %} {{ out }}
              precip_kind: >-
                {% if 'snow' in hit_cond_h %}snow{% elif 'sleet' in hit_cond_h
                or 'hail' in hit_cond_h %}mixed precipitation {% elif hit_cond_h
                %}rain{% else %}precipitation{% endif %}
              precip_msg: >-
                {% if hits | length > 0 and hit_pp > 0 %}expect {{ precip_kind
                }} later in the day around {{ hit_clock }} with a {{ hit_pp }}%
                chance.{% else %}{% endif %}
              msg: >-
                here's your forecast. It's {{ current_time }}. Currently {{
                now_temp }} degrees and {{ now_cond_h }}. {{ precip_msg }} High
                {{ high }} degrees. Low {{ low }} degrees.
              i_ai: false
              i_ai_task: ""
              i_ai_text: >
                Rewrite the message below for clear, natural TTS in a crisp
                TV-meteorologist voice.

                Hard rules:

                - Preserve every fact exactly: all numbers, units (degrees, %,
                mph), times, day names, and weather terms.

                - Do not invent, omit, reorder, round, or restate any facts.
                Keep original sequencing of info.

                - Output RAW TEXT ONLY — no quotes, code fences, YAML/JSON,
                greetings, preambles, or sign-offs.

                - 2–4 short sentences, ≤55 words, plain vocabulary, no emojis,
                no hashtags, no ALL CAPS.

                - If precipitation timing/probability exists, mention it once
                using the same daypart/time and %; otherwise omit.

                - Keep symbols and formatting as given (%, AM/PM). If the input
                is “Forecast not available.” return it unchanged.
          - choose:
              - conditions:
                  - condition: template
                    value_template: "{{ i_ai and (i_ai_task | default('') | string) != '' }}"
                sequence:
                  - action: ai_task.generate_data
                    data:
                      entity_id: "{{ i_ai_task }}"
                      task_name: weather message
                      instructions: |-
                        {{ i_ai_text }}
                        Message:
                        "{{ msg | replace('"','\\"') }}"
                    response_variable: aimsg
                  - variables:
                      spoken: "{{ aimsg.data | default(msg) }}"
            default:
              - variables:
                  spoken: "{{ msg }}"
          - variables:
              spoken_final: Good morning {{ name }}, {{ spoken }}
          - set_conversation_response: "{{ spoken_final | trim }}"
trigger_variables:
  enable_time_based: false
  enable_sensor_triggered: false
  enable_current_change_announce: true
  enable_upcoming_change_announce: true
  minutes_before_announce: 30
  enable_alarm_announce: false
mode: single

Oh, and one more thing. I noticed the run mode was set to single. This could be an issue. For example, if the blueprint is being triggered by the every 5 minute trigger, then it might prevent a run from another trigger. Then again, that may be by design.

Sure I’ll add this feature I was not gonna do voice satellite related things until I finished designing my own ceiling mounted satellite but I’ll add the feature I can’t say no lol

1 Like

@MakingNate

I created the new feature, i did it fairly quick before i went to work and haven’t tested it yet,

Please try it and let me know!

Thanks for the quick update @zodyking. Unfortunately it did not work, looks like the enable_voice_satellite variable never gets set in trigger_variables. That said, looking at the current configuration, if I configured the Speakers to say, Kitchen and Living Room, the automation based triggers would play to those preconfigured speakers which makes sense. But if I ask “What’s the weather” in my bedroom, it would respond in the kitchen and living room.

Instead, if you use the Set conversation response action when triggered by voice_satellite then it will dynamically respond from wherever the conversation was initiated.

Thanks again!

When I use webhook as trigger i get this error:
UndefinedError: 'precip_threshold' is undefined When triggered manually or from sensor it’s running fine. What’s wrong?

webhook works without a doubt, ill need a trace file from the errored automation run to properly diagnose it. Are you familiar with downloading trade file. Once you do download it, open it and paste the content here


Then wrap it in preformatted text like this. :)  

On the other hand…


Open the precipitation settings and change value then save. If that does not fix it then ill scavenge your trace file once uploaded.

No problem, also im gonna need time to re-setup my voice satellite and test this once i fix it.

1 Like

@zodyking, your blueprint got me thinking. In order to setup optional triggers you use conditions to check if that trigger should be allowed to continue. But what if instead you use the enable_ variables directly with the enable option of the trigger, like this:

trigger:
  - trigger: time_pattern
    hours: !input hour_pattern
    minutes: !input minute_offset
    id: time_based
    enabled: !input enable_time_based

Now the trigger is actually disabled if the slider is off and the conditions section is no longer necessary. Just a thought as I was playing around. I am new to Home Assistant and Blueprints so if there is a reason not to do it that way I would love to know.