# ============================================================================= # Cross-Device Timer Control Intent Scripts # ============================================================================= # These custom intents allow voice assistants to control timers in other areas. # Add to configuration.yaml alongside intent_scripts.yaml (merge into intent_script:) # # NOTE: This file requires custom_sentences to define the trigger phrases. # See: custom_sentences/en/cross_device_timers.yaml # # Example commands: # - "Start the kitchen timer for 5 minutes" # - "Stop the bedroom timer" # - "Check the playroom timer" # - "Move the timer to the kitchen" # # CONFIGURING TARGET LOCATIONS: # The target_to_area mapping below defines which spoken words map to which # area IDs. Add your areas and their aliases here. # ============================================================================= # ----------------------------------------------------------------------------- # StartOtherTimer - Start timer in another area # ----------------------------------------------------------------------------- # Example: "Start the kitchen timer for 5 minutes" # ----------------------------------------------------------------------------- StartOtherTimer: async_action: false action: - variables: total_seconds: "{{ (hours | default(0) | int) * 3600 + (minutes | default(0) | int) * 60 + (seconds | default(0) | int) }}" source_area: "{{ preferred_area_id | default('') | lower | replace(' ', '_') }}" # Map spoken location to area ID - customize this for your setup explicit_target: "{{ target_location | default('') | lower }}" target_area: >- {% set target_to_area = { 'playroom': 'playroom', 'playroom timer': 'playroom', 'kitchen': 'kitchen', 'kitchen timer': 'kitchen', 'dining': 'kitchen', 'dining room': 'kitchen', 'bedroom': 'bedroom', 'bedroom timer': 'bedroom', 'my bedroom': 'bedroom' } %} {{ target_to_area.get(explicit_target, '') }} timer_entity: "{{ 'timer.' ~ target_area if target_area else 'none' }}" duration_formatted: >- {% set total = (hours | default(0) | int) * 3600 + (minutes | default(0) | int) * 60 + (seconds | default(0) | int) %} {% set h = (total // 3600) | int %} {% set m = ((total % 3600) // 60) | int %} {% set s = (total % 60) | int %} {{ '%02d:%02d:%02d' | format(h, m, s) }} - choose: - conditions: - condition: template value_template: "{{ timer_entity != 'none' and total_seconds > 0 }}" sequence: - service: timer.start target: entity_id: "{{ timer_entity }}" data: duration: "{{ duration_formatted }}" speech: text: > {% set total_seconds = (hours | default(0) | int) * 3600 + (minutes | default(0) | int) * 60 + (seconds | default(0) | int) %} {% set explicit_target = target_location | default('') | lower %} {% set target_to_area = { 'playroom': 'playroom', 'playroom timer': 'playroom', 'kitchen': 'kitchen', 'kitchen timer': 'kitchen', 'dining': 'kitchen', 'dining room': 'kitchen', 'bedroom': 'bedroom', 'bedroom timer': 'bedroom', 'my bedroom': 'bedroom' } %} {% set target_area = target_to_area.get(explicit_target, '') %} {% if not target_area %} I couldn't determine which timer to start. Please specify a valid location. {% elif total_seconds == 0 %} I need a duration for the timer. {% else %} {% set m = (total_seconds // 60) | int %} {% set s = total_seconds % 60 %} {% if m > 0 and s > 0 %} Starting {{ target_area }} timer for {{ m }} minute{{ 's' if m != 1 else '' }} and {{ s }} second{{ 's' if s != 1 else '' }}. {% elif m > 0 %} Starting {{ target_area }} timer for {{ m }} minute{{ 's' if m != 1 else '' }}. {% else %} Starting {{ target_area }} timer for {{ s }} second{{ 's' if s != 1 else '' }}. {% endif %} {% endif %} # ----------------------------------------------------------------------------- # CancelOtherTimer - Cancel timer in another area # ----------------------------------------------------------------------------- # Example: "Cancel the kitchen timer" # ----------------------------------------------------------------------------- CancelOtherTimer: async_action: false action: - variables: explicit_target: "{{ target_location | default('') | lower }}" target_area: >- {% set target_to_area = { 'playroom': 'playroom', 'playroom timer': 'playroom', 'kitchen': 'kitchen', 'kitchen timer': 'kitchen', 'dining': 'kitchen', 'dining room': 'kitchen', 'bedroom': 'bedroom', 'bedroom timer': 'bedroom', 'my bedroom': 'bedroom' } %} {{ target_to_area.get(explicit_target, '') }} timer_entity: "{{ 'timer.' ~ target_area if target_area else 'none' }}" timer_state_before: "{{ states('timer.' ~ target_area) if target_area else 'unknown' }}" helper_entity: "{{ 'input_text.' ~ target_area ~ '_timer_previous_state' if target_area else 'none' }}" # Store the pre-action state in helper - choose: - conditions: - condition: template value_template: "{{ helper_entity != 'none' and states(helper_entity) is not none }}" sequence: - service: input_text.set_value target: entity_id: "{{ helper_entity }}" data: value: "{{ timer_state_before }}" # Cancel if active or paused - choose: - conditions: - condition: template value_template: "{{ timer_entity != 'none' and timer_state_before in ['active', 'paused'] }}" sequence: - service: timer.cancel target: entity_id: "{{ timer_entity }}" speech: text: > {% set explicit_target = target_location | default('') | lower %} {% set target_to_area = { 'playroom': 'playroom', 'playroom timer': 'playroom', 'kitchen': 'kitchen', 'kitchen timer': 'kitchen', 'dining': 'kitchen', 'dining room': 'kitchen', 'bedroom': 'bedroom', 'bedroom timer': 'bedroom', 'my bedroom': 'bedroom' } %} {% set target_area = target_to_area.get(explicit_target, '') %} {% set helper_entity = 'input_text.' ~ target_area ~ '_timer_previous_state' %} {% set timer_state_before = states(helper_entity) if target_area and states(helper_entity) is not none else 'unknown' %} {% if not target_area %} I couldn't determine which timer to cancel. Please specify a valid location. {% elif timer_state_before in ['active', 'paused'] %} {{ target_area | title }} timer cancelled. {% else %} There's no active timer in the {{ target_area }} to cancel. {% endif %} # ----------------------------------------------------------------------------- # GetOtherTimerStatus - Check timer status in another area # ----------------------------------------------------------------------------- # Example: "How much time on the kitchen timer?" # ----------------------------------------------------------------------------- GetOtherTimerStatus: async_action: false action: - variables: explicit_target: "{{ target_location | default('') | lower }}" target_area: >- {% set target_to_area = { 'playroom': 'playroom', 'playroom timer': 'playroom', 'kitchen': 'kitchen', 'kitchen timer': 'kitchen', 'dining': 'kitchen', 'dining room': 'kitchen', 'bedroom': 'bedroom', 'bedroom timer': 'bedroom', 'my bedroom': 'bedroom' } %} {{ target_to_area.get(explicit_target, '') }} speech: text: > {% set explicit_target = target_location | default('') | lower %} {% set target_to_area = { 'playroom': 'playroom', 'playroom timer': 'playroom', 'kitchen': 'kitchen', 'kitchen timer': 'kitchen', 'dining': 'kitchen', 'dining room': 'kitchen', 'bedroom': 'bedroom', 'bedroom timer': 'bedroom', 'my bedroom': 'bedroom' } %} {% set target_area = target_to_area.get(explicit_target, '') %} {% if not target_area %} I couldn't determine which timer to check. Please specify a valid location. {% else %} {% set timer = states.timer[target_area] %} {% if timer is none %} No timer is configured for the {{ target_area }}. {% elif timer.state == 'active' and timer.attributes.finishes_at is defined %} {% set finish = as_datetime(timer.attributes.finishes_at) %} {% set remaining = (finish - now()).total_seconds() | int %} {% set remaining = [remaining, 0] | max %} {% set m = (remaining // 60) | int %} {% set s = remaining % 60 %} The {{ target_area }} timer has {%- if m > 0 and s > 0 -%} {{ ' ' }}{{ m }} minute{{ 's' if m != 1 else '' }} and {{ s }} second{{ 's' if s != 1 else '' }} {%- elif m > 0 -%} {{ ' ' }}{{ m }} minute{{ 's' if m != 1 else '' }} {%- elif s > 0 -%} {{ ' ' }}{{ s }} second{{ 's' if s != 1 else '' }} {%- else -%} less than a second {%- endif %} remaining. {% elif timer.state == 'paused' and timer.attributes.remaining is defined %} {% set parts = timer.attributes.remaining.split(':') %} {% set remaining = (parts[0] | int) * 3600 + (parts[1] | int) * 60 + (parts[2] | float | int) %} {% set m = (remaining // 60) | int %} {% set s = remaining % 60 %} The {{ target_area }} timer is paused with {%- if m > 0 and s > 0 -%} {{ ' ' }}{{ m }} minute{{ 's' if m != 1 else '' }} and {{ s }} second{{ 's' if s != 1 else '' }} {%- elif m > 0 -%} {{ ' ' }}{{ m }} minute{{ 's' if m != 1 else '' }} {%- else -%} {{ ' ' }}{{ s }} second{{ 's' if s != 1 else '' }} {%- endif %} remaining. {% else %} There's no active timer in the {{ target_area }}. {% endif %} {% endif %} # ----------------------------------------------------------------------------- # RestartOtherTimer - Restart timer in another area with original duration # ----------------------------------------------------------------------------- # Example: "Restart the kitchen timer" # ----------------------------------------------------------------------------- RestartOtherTimer: async_action: false action: - variables: explicit_target: "{{ target_location | default('') | lower }}" target_area: >- {% set target_to_area = { 'playroom': 'playroom', 'playroom timer': 'playroom', 'kitchen': 'kitchen', 'kitchen timer': 'kitchen', 'dining': 'kitchen', 'dining room': 'kitchen', 'bedroom': 'bedroom', 'bedroom timer': 'bedroom', 'my bedroom': 'bedroom' } %} {{ target_to_area.get(explicit_target, '') }} timer_entity: "{{ 'timer.' ~ target_area if target_area else 'none' }}" timer_state_before: "{{ states('timer.' ~ target_area) if target_area else 'unknown' }}" timer_duration: "{{ state_attr('timer.' ~ target_area, 'duration') if target_area else '00:00:00' }}" helper_entity: "{{ 'input_text.' ~ target_area ~ '_timer_previous_state' if target_area else 'none' }}" # Store the pre-action state in helper - choose: - conditions: - condition: template value_template: "{{ helper_entity != 'none' and states(helper_entity) is not none }}" sequence: - service: input_text.set_value target: entity_id: "{{ helper_entity }}" data: value: "{{ timer_state_before }}" # Restart if has a duration - choose: - conditions: - condition: template value_template: "{{ timer_entity != 'none' and timer_state_before in ['active', 'paused'] and timer_duration not in ['00:00:00', None, ''] }}" sequence: - service: timer.start target: entity_id: "{{ timer_entity }}" data: duration: "{{ timer_duration }}" speech: text: > {% set explicit_target = target_location | default('') | lower %} {% set target_to_area = { 'playroom': 'playroom', 'playroom timer': 'playroom', 'kitchen': 'kitchen', 'kitchen timer': 'kitchen', 'dining': 'kitchen', 'dining room': 'kitchen', 'bedroom': 'bedroom', 'bedroom timer': 'bedroom', 'my bedroom': 'bedroom' } %} {% set target_area = target_to_area.get(explicit_target, '') %} {% set helper_entity = 'input_text.' ~ target_area ~ '_timer_previous_state' %} {% set timer_state_before = states(helper_entity) if target_area and states(helper_entity) is not none else 'unknown' %} {% set timer_duration = state_attr('timer.' ~ target_area, 'duration') if target_area else '00:00:00' %} {% if not target_area %} I couldn't determine which timer to restart. Please specify a valid location. {% elif timer_state_before not in ['active', 'paused'] %} There's no active timer in the {{ target_area }} to restart. {% elif timer_duration in ['00:00:00', None, ''] %} The {{ target_area }} timer has no duration to restart with. {% else %} {% set parts = timer_duration.split(':') %} {% set total_seconds = (parts[0] | int) * 3600 + (parts[1] | int) * 60 + (parts[2] | float | int) %} {% set m = (total_seconds // 60) | int %} {% set s = total_seconds % 60 %} {{ target_area | title }} timer restarted {%- if m > 0 and s > 0 -%} {{ ' ' }}for {{ m }} minute{{ 's' if m != 1 else '' }} and {{ s }} second{{ 's' if s != 1 else '' }}. {%- elif m > 0 -%} {{ ' ' }}for {{ m }} minute{{ 's' if m != 1 else '' }}. {%- elif s > 0 -%} {{ ' ' }}for {{ s }} second{{ 's' if s != 1 else '' }}. {%- else -%} . {%- endif %} {% endif %} # ----------------------------------------------------------------------------- # MoveTimerToOtherArea - Move timer from current area to another area # ----------------------------------------------------------------------------- # Example: "Move the timer to the kitchen" # Cancels timer in source area and starts it in target area with remaining time # ----------------------------------------------------------------------------- MoveTimerToOtherArea: async_action: false action: - variables: # Source: where the voice assistant is source_area: "{{ preferred_area_id | default('') | lower | replace(' ', '_') }}" source_timer: "{{ 'timer.' ~ source_area if source_area else 'none' }}" source_state: "{{ states(source_timer) if source_timer != 'none' else 'unknown' }}" # Target: spoken location explicit_target: "{{ target_location | default('') | lower }}" target_to_area: "{{ {'playroom': 'playroom', 'kitchen': 'kitchen', 'dining': 'kitchen', 'dining room': 'kitchen', 'bedroom': 'bedroom', 'my bedroom': 'bedroom'} }}" target_area: "{{ target_to_area.get(explicit_target, '') }}" target_timer: "{{ 'timer.' ~ target_area if target_area else 'none' }}" # Calculate remaining seconds remaining_seconds: "{{ ((state_attr(source_timer, 'finishes_at') | as_datetime | as_timestamp) - (now() | as_timestamp)) | int if source_state == 'active' and state_attr(source_timer, 'finishes_at') else ((state_attr(source_timer, 'remaining') | default('0:0:0')).split(':')[0] | int * 3600 + (state_attr(source_timer, 'remaining') | default('0:0:0')).split(':')[1] | int * 60 + (state_attr(source_timer, 'remaining') | default('0:0:0')).split(':')[2] | float | int) if source_state == 'paused' else 0 }}" remaining_int: "{{ [remaining_seconds | int, 0] | max }}" duration_formatted: "{{ '%02d:%02d:%02d' | format((remaining_int // 3600) | int, ((remaining_int % 3600) // 60) | int, (remaining_int % 60) | int) }}" # Validation is_valid_source: "{{ source_area != '' }}" is_valid_target: "{{ target_area != '' }}" is_different: "{{ source_area != target_area }}" has_active_timer: "{{ source_state in ['active', 'paused'] and remaining_int > 0 }}" can_move: "{{ is_valid_source and is_valid_target and is_different and has_active_timer }}" # Store move state for speech - service: input_text.set_value target: entity_id: input_text.timer_move_state data: value: "{{ 'success:' ~ target_area ~ ':' ~ remaining_int | string if can_move else 'error:no_source' if not is_valid_source else 'error:no_target' if not is_valid_target else 'error:same_device' if not is_different else 'error:no_timer' }}" # Move if valid - choose: - conditions: - condition: template value_template: "{{ can_move }}" sequence: - service: timer.cancel target: entity_id: "{{ source_timer }}" - service: timer.start target: entity_id: "{{ target_timer }}" data: duration: "{{ duration_formatted }}" speech: text: > {% set move_state = states('input_text.timer_move_state') %} {% set parts = move_state.split(':') %} {% set status = parts[0] | default('error') %} {% if status == 'success' %} {% set target_area = parts[1] | default('other') %} {% set remaining = parts[2] | default('0') | int %} {% set m = (remaining // 60) | int %} {% set s = remaining % 60 %} Timer moved to the {{ target_area }} with {%- if m > 0 and s > 0 -%} {{ ' ' }}{{ m }} minute{{ 's' if m != 1 else '' }} and {{ s }} second{{ 's' if s != 1 else '' }} {%- elif m > 0 -%} {{ ' ' }}{{ m }} minute{{ 's' if m != 1 else '' }} {%- else -%} {{ ' ' }}{{ s }} second{{ 's' if s != 1 else '' }} {%- endif %} remaining. {% elif 'no_source' in move_state %} I couldn't determine which timer to move. {% elif 'no_target' in move_state %} I couldn't determine where to move the timer. Please specify a valid location. {% elif 'same_device' in move_state %} The timer is already here. {% else %} There's no active timer to move. {% endif %}