Skip to content

Instantly share code, notes, and snippets.

@yougotborked
Forked from rokam/tts-door-opening.yaml
Last active October 4, 2025 17:20
Show Gist options
  • Select an option

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

Select an option

Save yougotborked/878998f50c7a7ad4d5d2e62b16bcb45e to your computer and use it in GitHub Desktop.
Send a greetings TTS message using tts.google_say service after opening the door, considering who arrived in the configured minutes.
blueprint:
name: TTS on Door Opening with Recent Arrivals (rev B2)
description: >
Speaks a greeting via TTS when a door opens **and** one or more selected
people have arrived home within the last N minutes. (Fix: robust list typing.)
domain: automation
input:
door_sensor:
name: Door sensor
selector:
entity:
domain: binary_sensor
device_class:
- door
- opening
persons:
name: Persons to watch
selector:
entity:
domain: person
multiple: true
minutes:
name: Minutes window
default: 5
selector:
number:
min: 1
max: 60
step: 1
unit_of_measurement: min
mode: slider
settle_seconds:
name: Debounce after door opens (sec)
default: 3
selector:
number:
min: 0
max: 30
step: 1
unit_of_measurement: s
mode: slider
message_single:
name: Message (single person)
description: Use <person> to insert the person name
default: "Welcome home <person>!"
selector:
text:
multiline: true
message_multiple:
name: Message (multiple people)
description: Use <persons> to insert names (comma-and logic built in)
default: "Welcome home <persons>!"
selector:
text:
multiline: true
persons_concat:
name: Two-name joiner
default: " and "
selector:
text:
media_player:
name: Media player
selector:
entity:
domain: media_player
tts_device:
name: TTS entity
description: The TTS entity that renders speech (e.g., tts.piper, tts.cloud)
selector:
entity:
domain: tts
volume_level:
name: Temporary volume (optional)
default: null
selector:
number:
min: 0
max: 1
step: 0.01
mode: slider
pre_actions:
name: Pre-actions
default: []
selector:
action: {}
post_actions:
name: Post-actions
default: []
selector:
action: {}
quiet_start:
name: Quiet hours start (optional)
selector:
time:
quiet_end:
name: Quiet hours end (optional)
selector:
time:
max_every_minutes:
name: Max announcement frequency
description: Minimum minutes between announcements; 0 disables the guard
default: 2
selector:
number:
min: 0
max: 120
step: 1
unit_of_measurement: min
mode: slider
source_url: https://gist.github.com/yougotborked/878998f50c7a7ad4d5d2e62b16bcb45e
variables:
persons_list: !input persons
minutes_window: !input minutes
settle_seconds: !input settle_seconds
concat_str: !input persons_concat
tts_device: !input tts_device
media_player: !input media_player
volume_level: !input volume_level
quiet_start: !input quiet_start
quiet_end: !input quiet_end
max_every_minutes: !input max_every_minutes
# seconds math
window_seconds: "{{ (minutes_window | int(0)) * 60 }}"
max_every_seconds: "{{ (max_every_minutes | int(0)) * 60 }}"
# Build JSON list, then parse to a real list
recent_arrivals_json: >
{%- set now_ts = as_timestamp(now()) -%}
{%- set win = window_seconds | int(0) -%}
{%- set names = [] -%}
{%- for p in expand(persons_list)
if p is not none and p.state == 'home' -%}
{%- set last = as_timestamp(p.last_changed) -%}
{%- if last is number and (now_ts - last) <= win -%}
{%- set _ = names.append(p.name) -%}
{%- endif -%}
{%- endfor -%}
{{ names | tojson }}
recent_arrivals: "{{ recent_arrivals_json | from_json }}"
# Render nice names list with Oxford comma & custom 2-name joiner
persons_rendered: >
{%- set lst = recent_arrivals -%}
{%- if lst | length == 0 -%}{% elif lst | length == 1 -%}
{{ lst[0] }}
{%- elif lst | length == 2 -%}
{{ lst | join(concat_str) }}
{%- else -%}
{{ lst[:-1] | join(', ') ~ ', and ' ~ lst[-1] }}
{%- endif -%}
message_single: !input message_single
message_multiple: !input message_multiple
tts_message: >
{%- if recent_arrivals | length > 1 -%}
{{ message_multiple.replace('<persons>', persons_rendered) }}
{%- elif recent_arrivals | length == 1 -%}
{{ message_single.replace('<person>', persons_rendered) }}
{%- else -%}{%- endif -%}
# Quiet-hours suppression (boolean)
is_quiet: >
{%- set qs = quiet_start -%}
{%- set qe = quiet_end -%}
{%- set has = (qs is not none) and (qe is not none) and (qs|string) and (qe|string) -%}
{%- if has -%}
{%- set start = today_at(qs) -%}
{%- set end = today_at(qe) -%}
{%- if end <= start -%}
{{ (now() >= start) or (now() < end) }}
{%- else -%}
{{ (start <= now()) and (now() < end) }}
{%- endif -%}
{%- else -%}false{%- endif -%}
# Throttle using this automation's own last_triggered
last_fired_ok: >
{%- set guard = max_every_seconds | int(0) -%}
{%- if guard <= 0 -%}true
{%- else -%}
{%- set lt = this.attributes.last_triggered -%}
{%- if lt is not none -%}
{{ (as_timestamp(now()) - as_timestamp(lt)) >= guard }}
{%- else -%}true{%- endif -%}
{%- endif -%}
trigger:
- platform: state
entity_id: !input door_sensor
to: 'on'
- platform: state
entity_id: !input door_sensor
to: 'open'
condition: []
action:
- if:
- condition: template
value_template: "{{ not is_quiet }}"
- condition: template
value_template: "{{ last_fired_ok }}"
then:
- delay:
seconds: "{{ settle_seconds | int(0) }}"
- service: logbook.log
data:
name: "Arrival TTS"
message: >
Window: {{ minutes_window }} min ({{ window_seconds }} s),
Recent: {{ recent_arrivals }},
Persons: {{ persons_list }},
Quiet(start/end): {{ is_quiet }} ({{ quiet_start }}–{{ quiet_end }}),
LastFiredOK: {{ last_fired_ok }}
- condition: template
value_template: "{{ (recent_arrivals | length) > 0 and (tts_message | trim) != '' }}"
- choose: []
default: !input pre_actions
- variables:
__orig_vol: >
{{ state_attr(media_player, 'volume_level') if (volume_level is number) else none }}
- choose:
- conditions:
- condition: template
value_template: "{{ volume_level is number }}"
sequence:
- service: media_player.volume_set
target:
entity_id: "{{ media_player }}"
data:
volume_level: "{{ volume_level }}"
default: []
- service: tts.speak
target:
entity_id: "{{ tts_device }}"
data:
media_player_entity_id: "{{ media_player }}"
message: "{{ tts_message }}"
- choose:
- conditions:
- condition: template
value_template: "{{ __orig_vol is number }}"
sequence:
- service: media_player.volume_set
target:
entity_id: "{{ media_player }}"
data:
volume_level: "{{ __orig_vol }}"
default: []
- choose: []
default: !input post_actions
mode: single
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment