-
-
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.
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: 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