Skip to content

Instantly share code, notes, and snippets.

@yougotborked
Last active September 20, 2025 02:37
Show Gist options
  • Select an option

  • Save yougotborked/d38b070bcd26bc790ba91b843543f7df to your computer and use it in GitHub Desktop.

Select an option

Save yougotborked/d38b070bcd26bc790ba91b843543f7df to your computer and use it in GitHub Desktop.
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