Skip to content

Instantly share code, notes, and snippets.

@yougotborked
Forked from rokam/tts-door-opening.yaml
Last active December 27, 2025 19:10
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 (fallback once)
description: >-
Speaks a welcome message on a door opening, but only if one or more selected
persons have transitioned to home within the last N minutes.
Optional fallback: if someone arrives home but you don't open the door within
the window, it will announce them once on the next door opening (within an
expiry you choose), then clear the pending list.
domain: automation
input:
door_sensor:
name: Door sensor
selector:
entity:
domain:
- binary_sensor
persons:
name: Persons
description: Persons to consider for arrivals.
selector:
entity:
domain:
- person
multiple: true
media_player:
name: Media player
selector:
entity:
domain:
- media_player
tts_device:
name: TTS entity
selector:
entity:
domain:
- tts
minutes:
name: Minutes window
description: How many minutes back to consider someone a recent arrival.
default: 10
selector:
number:
min: 1
max: 120
step: 1
unit_of_measurement: min
mode: slider
settle_seconds:
name: Settle delay (seconds)
description: Delay after door opens to let trackers settle.
default: 3
selector:
number:
min: 0
max: 60
step: 1
unit_of_measurement: s
mode: slider
max_every_minutes:
name: Minimum time between announcements (minutes)
default: 2
selector:
number:
min: 0
max: 120
step: 1
unit_of_measurement: min
mode: slider
enable_fallback_once:
name: Enable one-time fallback announcement
default: true
selector:
boolean: {}
fallback_expire_minutes:
name: Fallback expiry (minutes)
description: Discard pending arrivals if the door isn't opened within this many minutes.
default: 240
selector:
number:
min: 5
max: 1440
step: 5
unit_of_measurement: min
mode: slider
pending_names:
name: Pending arrivals list (input_text)
description: >-
input_text used to store a JSON list of pending arrival names.
Create an input_text helper and select it here.
selector:
entity:
domain: input_text
pending_since:
name: Pending arrivals since (input_datetime)
description: >-
input_datetime (date+time) used to store when pending arrivals were first recorded.
Create an input_datetime helper (date+time) and select it here.
selector:
entity:
domain: input_datetime
quiet_start:
name: Quiet hours start
default: "01:00:00"
selector:
time: {}
quiet_end:
name: Quiet hours end
default: "06:00:00"
selector:
time: {}
volume_level:
name: Volume level (optional)
description: If set, temporarily sets volume while speaking then restores original.
default: 0.48
selector:
number:
min: 0
max: 1
step: 0.01
mode: slider
message_single:
name: Message (single)
default: "Welcome home <person>!"
selector:
text: {}
message_multiple:
name: Message (multiple)
default: "Welcome home <persons>!"
selector:
text: {}
mode: single
trigger:
# Door opened
- platform: state
entity_id: !input door_sensor
to: "on"
- platform: state
entity_id: !input door_sensor
to: "open"
# Person arrived home
- platform: state
entity_id: !input persons
to: "home"
variables:
door_entity: !input door_sensor
persons_list: !input persons
minutes_window: !input minutes
settle_seconds: !input settle_seconds
max_every_minutes: !input max_every_minutes
enable_fallback_once: !input enable_fallback_once
fallback_expire_minutes: !input fallback_expire_minutes
pending_names_entity: !input pending_names
pending_since_entity: !input pending_since
quiet_start: !input quiet_start
quiet_end: !input quiet_end
media_player: !input media_player
tts_device: !input tts_device
volume_level: !input volume_level
message_single: !input message_single
message_multiple: !input message_multiple
is_person_trigger: >-
{{ trigger.platform == 'state'
and (trigger.entity_id in persons_list)
and (trigger.to_state is not none)
and (trigger.to_state.state == 'home') }}
is_door_trigger: >-
{{ trigger.platform == 'state' and trigger.entity_id == door_entity }}
concat_str: " and "
window_seconds: "{{ (minutes_window | int(0)) * 60 }}"
max_every_seconds: "{{ (max_every_minutes | int(0)) * 60 }}"
# Build list of people who became 'home' within the window.
recent_arrivals_json: >-
{%- set now_ts = as_timestamp(now()) -%}
{%- set win = window_seconds | int(0) -%}
{%- set ns = namespace(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 ns.names = ns.names + [p.name] -%}
{%- endif -%}
{%- endfor -%}
{{ ns.names | tojson }}
recent_arrivals: "{{ recent_arrivals_json | trim | from_json(default=[]) }}"
# Pending fallback list stored as JSON in input_text.
pending_raw: >-
{{ states(pending_names_entity) if pending_names_entity is not none else '[]' }}
pending_json: >-
{%- set s = (pending_raw | string | trim) -%}
{%- if s in ['', 'unknown', 'unavailable', 'none', 'None'] -%}[]
{%- else -%}{{ s }}{%- endif -%}
pending_list: "{{ pending_json | from_json(default=[]) }}"
pending_since_raw: >-
{{ states(pending_since_entity) if pending_since_entity is not none else '' }}
pending_since_ts: >-
{%- set s = (pending_since_raw | string | trim) -%}
{%- if s in ['', 'unknown', 'unavailable', 'none', 'None'] -%}{{ 0 }}
{%- else -%}{{ as_timestamp(s) | int(0) }}{%- endif -%}
pending_not_expired: >-
{%- if not enable_fallback_once -%}false
{%- elif pending_list | length == 0 -%}false
{%- elif (pending_since_ts | int(0)) <= 0 -%}true
{%- else -%}
{{ (as_timestamp(now()) - (pending_since_ts | int(0))) <= ((fallback_expire_minutes | int(0)) * 60) }}
{%- endif -%}
effective_arrivals: >-
{%- set a = recent_arrivals -%}
{%- set b = (pending_list if pending_not_expired else []) -%}
{{ (a + b) | unique | list }}
effective_persons_rendered: >-
{%- set lst = effective_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 -%}
effective_tts_message: >-
{%- if effective_arrivals | length > 1 -%}
{{ message_multiple.replace('<persons>', effective_persons_rendered) }}
{%- elif effective_arrivals | length == 1 -%}
{{ message_single.replace('<person>', effective_persons_rendered) }}
{%- else -%}
{%- endif -%}
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 -%}
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 -%}
action:
# A) Person-arrival path: record pending names (fallback) and return.
- choose:
- conditions:
- condition: template
value_template: "{{ is_person_trigger and enable_fallback_once }}"
sequence:
- variables:
_arrived_name: >-
{%- set p = states[trigger.entity_id] -%}
{{ p.name if p is not none else trigger.entity_id }}
_now_iso: "{{ now().isoformat() }}"
_existing: "{{ pending_json | from_json(default=[]) }}"
_updated: "{{ ((_existing + [_arrived_name]) | unique | list) | tojson }}"
- service: input_text.set_value
target:
entity_id: "{{ pending_names_entity }}"
data:
value: "{{ _updated }}"
- choose:
- conditions:
- condition: template
value_template: "{{ (pending_since_ts | int(0)) <= 0 }}"
sequence:
- service: input_datetime.set_datetime
target:
entity_id: "{{ pending_since_entity }}"
data:
datetime: "{{ _now_iso }}"
# B) Door-open path: speak if we have effective arrivals.
- if:
- condition: template
value_template: "{{ is_door_trigger }}"
- condition: template
value_template: "{{ not is_quiet }}"
- condition: template
value_template: "{{ last_fired_ok }}"
then:
- delay:
seconds: "{{ settle_seconds | int(0) }}"
- condition: template
value_template: "{{ (effective_arrivals | length) > 0 and (effective_tts_message | trim) != '' }}"
- 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 }}"
- service: tts.speak
target:
entity_id: "{{ tts_device }}"
data:
media_player_entity_id: "{{ media_player }}"
message: "{{ effective_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 }}"
# Clear pending arrivals if we used them.
- choose:
- conditions:
- condition: template
value_template: "{{ enable_fallback_once and (pending_list | length) > 0 and pending_not_expired }}"
sequence:
- service: input_text.set_value
target:
entity_id: "{{ pending_names_entity }}"
data:
value: "[]"
- service: input_datetime.set_datetime
target:
entity_id: "{{ pending_since_entity }}"
data:
datetime: "1970-01-01T00:00:00"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment