Last active
September 20, 2025 02:37
-
-
Save yougotborked/d38b070bcd26bc790ba91b843543f7df to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| blueprint: | |
| name: Google Calendar TTS Reminder (fixed) | |
| description: > | |
| Announce Google Calendar events using TTS on selected speakers. Supports | |
| YAML config embedded in the event description (between `---` and `...`). | |
| Example YAML in event description: | |
| --- | |
| announce: true | |
| messages: | |
| - "Time for soccer practice!" | |
| - "Don't forget your gear!" | |
| travel_time: true | |
| reminder_offset: "00:15:00" # overrides default | |
| owner_only: false | |
| ... | |
| Notes / changes vs your original: | |
| - Uses a minute-based time_pattern trigger so per-event reminder_offset | |
| overrides work. No more template-trigger race with parsing. | |
| - Reads the calendar's `message`, `description`, `location`, `start_time`. | |
| (Google Calendar entities expose these names.) | |
| - Avoids trying to write to `sensor.*` (read-only). If you want travel time | |
| announced, pass in an existing travel time sensor entity (from Waze/Google/ | |
| HERE integrations, etc.). | |
| - Prevents duplicate announcements by checking `this.attributes.last_triggered`. | |
| domain: automation | |
| input: | |
| calendar_entity: | |
| name: Calendar Entity | |
| selector: | |
| entity: | |
| domain: calendar | |
| tts_entity: | |
| name: TTS Entity | |
| description: The TTS entity (e.g. tts.cloud_say) used with tts.speak. | |
| selector: | |
| entity: | |
| domain: tts | |
| media_player: | |
| name: Media Player | |
| description: Media player to output the TTS. | |
| selector: | |
| entity: | |
| domain: media_player | |
| default_reminder_offset: | |
| name: Default Reminder Offset (HH:MM:SS) | |
| description: Time before event start to announce, if not overridden in YAML. | |
| default: 00:15:00 | |
| selector: | |
| text: | |
| multiline: false | |
| type: text | |
| travel_time_sensor: | |
| name: (Optional) Travel Time Sensor | |
| description: > | |
| A sensor whose state is a human-friendly duration or minutes (e.g. | |
| Waze or Google Distance Matrix). If provided and event YAML sets | |
| travel_time: true, its value will be appended to the announcement. | |
| default: "" | |
| selector: | |
| entity: | |
| domain: sensor | |
| multiple: false | |
| include_entities: [] | |
| mode: single | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # Triggers every minute so we can compute dynamic per-event reminder windows. | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| trigger: | |
| - platform: time_pattern | |
| minutes: "/1" | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # Variables derived from the selected calendar entity. | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| variables: | |
| cal: !input calendar_entity | |
| tts_entity: !input tts_entity | |
| media_player: !input media_player | |
| default_reminder_offset: !input default_reminder_offset | |
| travel_time_sensor: !input travel_time_sensor | |
| # Calendar attributes (names used by HA Google Calendar) | |
| ev_start_ts: > | |
| {{ as_timestamp(state_attr(cal, 'start_time')) }} | |
| ev_title: > | |
| {{ state_attr(cal, 'message') or state_attr(cal, 'summary') or 'Calendar event' }} | |
| ev_desc: > | |
| {{ state_attr(cal, 'description') | default('', true) }} | |
| ev_location: > | |
| {{ state_attr(cal, 'location') | default('', true) }} | |
| # Extract YAML block from description (between --- and ...). If none, {}. | |
| yaml_start: > | |
| {{ (ev_desc.find('---')) if ev_desc is string else -1 }} | |
| yaml_end: > | |
| {{ (ev_desc.rfind('...') + 3) if ev_desc is string else -1 }} | |
| yaml_block: > | |
| {{ ev_desc[yaml_start:yaml_end] if yaml_start != -1 and yaml_end >= yaml_start else '' }} | |
| cfg: > | |
| {{ (yaml_block | from_yaml) if yaml_block else {} }} | |
| # Announce flag (defaults to true) | |
| cfg_announce: > | |
| {% if cfg is mapping %} | |
| {{ (cfg.get('announce', true) | string | lower) in ['true','1','yes','on','y'] or (cfg.get('announce', true) is sameas true) }} | |
| {% else %} | |
| true | |
| {% endif %} | |
| # Reminder offset: prefer per-event YAML string, else default input string. | |
| # Convert to seconds so we can do numeric math reliably. | |
| cfg_offset_str: > | |
| {{ (cfg.get('reminder_offset') if cfg is mapping else None) or default_reminder_offset }} | |
| cfg_offset_seconds: > | |
| {# Robustly convert offset to seconds whether it's a string like "00:15:00" or already numeric #} | |
| {% set v = cfg_offset_str %} | |
| {% if v is number %} | |
| {{ v | float(0) }} | |
| {% else %} | |
| {% set td = as_timedelta(v) %} | |
| {{ td.total_seconds() if td else 0 }} | |
| {% endif %} | |
| # Window check: fire only when now is within the 60s window after (start - offset). | |
| reminder_epoch: > | |
| {{ (ev_start_ts - cfg_offset_seconds) if ev_start_ts else None }} | |
| due_now: > | |
| {% set now_ts = now().timestamp() %} | |
| {{ reminder_epoch is number and (0 <= (now_ts - reminder_epoch) < 59) }} | |
| # Build message list | |
| messages: > | |
| {% if cfg is mapping and cfg.get('messages') %} | |
| {{ cfg.get('messages') }} | |
| {% else %} | |
| {{ [ev_title] }} | |
| {% endif %} | |
| # Travel time toggle | |
| travel_time_enabled: > | |
| {{ cfg is mapping and cfg.get('travel_time', false) in [true, 'true', 'True'] }} | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # Conditions: only when an event exists and we're inside the 1‑minute window, | |
| # and the event has announce enabled. Also block repeats using last_triggered. | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| condition: | |
| - condition: template | |
| alias: Event has a start time | |
| value_template: "{{ ev_start_ts is number }}" | |
| - condition: template | |
| alias: Announcement enabled via YAML or default | |
| value_template: "{{ cfg_announce }}" | |
| - condition: template | |
| alias: We are in the 60s window for this event's reminder time | |
| value_template: "{{ due_now }}" | |
| - condition: template | |
| alias: Prevent duplicates (don't fire again after we've announced) | |
| value_template: > | |
| {% set last = this.attributes.last_triggered %} | |
| {% if last %} | |
| {{ as_timestamp(last) < reminder_epoch }} | |
| {% else %} | |
| true | |
| {% endif %} | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # Action: speak a random message; optionally include travel time sensor state. | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| action: | |
| - variables: | |
| base_msg: "{{ (messages | random) }}" | |
| travel_suffix: > | |
| {% set tt = states(travel_time_sensor) if travel_time_sensor else '' %} | |
| {% if travel_time_enabled and travel_time_sensor and tt not in ['', 'unknown', 'unavailable'] %} | |
| {{ " It will take approximately " ~ tt ~ "." }} | |
| {% else %} | |
| {{ '' }} | |
| {% endif %} | |
| final_msg: "{{ base_msg ~ travel_suffix }}" | |
| - service: tts.speak | |
| target: | |
| entity_id: "{{ tts_entity }}" | |
| data: | |
| media_player_entity_id: "{{ media_player }}" | |
| message: "{{ final_msg }}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment