├── .gitignore ├── images ├── 20250426_GwdtEl.png ├── 20250426_oj1S9U.png ├── 20250527_FdZbGj.png ├── 20250527_JC5AOg.png ├── 20250527_gCfAcK.png ├── 20250528_210348.jpg ├── 20250604_VhChze.png └── 20250608_ASxwFa.png ├── scripts ├── requirements.txt ├── youtube_data_tool.py └── zalo_custom_bot_handle_tool.py ├── LICENSE ├── check_the_device_turns_on_manually.yaml ├── link_multiple_devices.yaml ├── device_control_tool.yaml ├── device_ringing_full_llm.yaml ├── play_youtube_video_full_llm.yaml ├── traffic_fine_notification.yaml ├── traffic_fine_lookup_full_llm.yaml ├── home_assistant_device_location_lookup_guide_en.md ├── home_assistant_device_location_lookup_guide.md ├── create_lunar_events.yaml ├── advanced_google_search_full_llm.yaml ├── date_lookup_and_conversion_full_llm.yaml ├── home_assistant_ios_themes.md ├── home_assistant_ios_themes_en.md ├── advanced_youtube_search_full_llm.yaml ├── home_assistant_voice_instructions_en.md ├── fan_oscillation_control_full_llm.yaml ├── home_assistant_voice_instructions.md ├── calendar_events_lookup_full_llm.yaml ├── file_content_analyzer_full_llm.yaml ├── home_assistant_unavailable_devices_en.md ├── home_assistant_unavailable_devices.md ├── home_assistant_play_favorite_youtube_channel_videos.md ├── home_assistant_play_favorite_youtube_channel_videos_en.md ├── device_control_timer_full_llm.yaml ├── get_youtube_video_info_full_llm.yaml ├── device_location_lookup_full_llm.yaml ├── camera_snapshot_full_llm.yaml ├── devices_schedules_restart_handler.yaml ├── zalo_bot_webhook.yaml └── send_to_zalo_custom_bot_full_llm.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .venv 4 | .env 5 | *.db 6 | -------------------------------------------------------------------------------- /images/20250426_GwdtEl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luuquangvu/tutorials/HEAD/images/20250426_GwdtEl.png -------------------------------------------------------------------------------- /images/20250426_oj1S9U.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luuquangvu/tutorials/HEAD/images/20250426_oj1S9U.png -------------------------------------------------------------------------------- /images/20250527_FdZbGj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luuquangvu/tutorials/HEAD/images/20250527_FdZbGj.png -------------------------------------------------------------------------------- /images/20250527_JC5AOg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luuquangvu/tutorials/HEAD/images/20250527_JC5AOg.png -------------------------------------------------------------------------------- /images/20250527_gCfAcK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luuquangvu/tutorials/HEAD/images/20250527_gCfAcK.png -------------------------------------------------------------------------------- /images/20250528_210348.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luuquangvu/tutorials/HEAD/images/20250528_210348.jpg -------------------------------------------------------------------------------- /images/20250604_VhChze.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luuquangvu/tutorials/HEAD/images/20250604_VhChze.png -------------------------------------------------------------------------------- /images/20250608_ASxwFa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luuquangvu/tutorials/HEAD/images/20250608_ASxwFa.png -------------------------------------------------------------------------------- /scripts/requirements.txt: -------------------------------------------------------------------------------- 1 | # Common packages are included in many tools. 2 | aiohttp 3 | aiofiles 4 | 5 | # youtube_data_tool 6 | google-api-python-client 7 | 8 | # traffic_fine_lookup_tool 9 | pillow 10 | beautifulsoup4 11 | google-genai 12 | google-api-core 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Lưu Quang Vũ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /check_the_device_turns_on_manually.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Check The Device Turns On Manually 3 | author: luuquangvu 4 | description: | 5 | # Verify whether the device activated manually or via automation/application 6 | 7 | - Prevents false triggers when device comes back from unavailable/unknown state. 8 | domain: automation 9 | homeassistant: 10 | min_version: 2024.10.0 11 | input: 12 | linked_entity: 13 | name: Entity 14 | description: Entity to link (Switch or Light) 15 | selector: 16 | entity: 17 | filter: 18 | - domain: 19 | - switch 20 | - light 21 | linked_boolean: 22 | name: Input Boolean 23 | description: Input Boolean Helper to link 24 | selector: 25 | entity: 26 | filter: 27 | - domain: input_boolean 28 | mode: restart 29 | max_exceeded: silent 30 | variables: 31 | version: 20251209 32 | triggers: 33 | - trigger: state 34 | entity_id: !input linked_entity 35 | from: null 36 | to: null 37 | conditions: 38 | - condition: template 39 | value_template: "{{ trigger.from_state.state != trigger.to_state.state }}" 40 | - condition: template 41 | value_template: "{{ trigger.from_state.state not in ['unavailable', 'unknown'] }}" 42 | - condition: template 43 | value_template: "{{ trigger.to_state.state not in ['unavailable', 'unknown'] }}" 44 | 45 | actions: 46 | - if: 47 | - condition: template 48 | value_template: "{{ trigger.to_state.state == 'on' }}" 49 | - condition: template 50 | value_template: "{{ trigger.to_state.context.parent_id is none }}" 51 | - condition: template 52 | value_template: "{{ trigger.to_state.context.user_id is none }}" 53 | then: 54 | - action: homeassistant.turn_on 55 | target: 56 | entity_id: !input linked_boolean 57 | else: 58 | - action: homeassistant.turn_off 59 | target: 60 | entity_id: !input linked_boolean 61 | -------------------------------------------------------------------------------- /link_multiple_devices.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Link On/Off State of Multiple Devices 3 | author: luuquangvu 4 | description: | 5 | # Link On/Off State of Multiple Devices 6 | 7 | - Select multiple entities to link their on/off state. If any selected entity is turned on or off, the other selected entities will be sent a matching on or off command. 8 | - Uses state filtering to prevent infinite loops and reduce network traffic. It only sends commands to devices that are not already in the desired state. 9 | - Automatically ignores unavailable or unknown entities to prevent errors. 10 | 11 | ## Requirements 12 | 13 | - All selected entities must support `homeassistant.turn_on` and `homeassistant.turn_off`. 14 | - Requires Home Assistant 2024.10.0 or newer. 15 | domain: automation 16 | homeassistant: 17 | min_version: 2024.10.0 18 | input: 19 | linked_entities: 20 | name: Entities 21 | description: Entities to link together 22 | selector: 23 | entity: 24 | multiple: true 25 | mode: restart 26 | max_exceeded: silent 27 | variables: 28 | version: 20251209 29 | linked_entities: !input linked_entities 30 | triggers: 31 | - trigger: state 32 | entity_id: !input linked_entities 33 | from: 34 | - "on" 35 | - "off" 36 | to: 37 | - "off" 38 | - "on" 39 | conditions: 40 | - condition: template 41 | value_template: "{{ trigger.to_state.state != trigger.from_state.state }}" 42 | actions: 43 | - variables: 44 | command: "turn_{{ trigger.to_state.state }}" 45 | target_entities: | 46 | {{ expand(linked_entities) 47 | | selectattr('entity_id', '!=', trigger.entity_id) 48 | | selectattr('state', '!=', trigger.to_state.state) 49 | | rejectattr('state', 'in', ['unavailable', 'unknown']) 50 | | map(attribute='entity_id') 51 | | list }} 52 | 53 | - choose: 54 | - conditions: "{{ target_entities | count > 0 }}" 55 | sequence: 56 | - action: "homeassistant.{{ command }}" 57 | target: 58 | entity_id: "{{ target_entities }}" 59 | -------------------------------------------------------------------------------- /device_control_tool.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Device Timer 3 | author: luuquangvu 4 | description: | 5 | # Tool for controlling on/off devices with adjustable delay timing 6 | 7 | ## Blueprint Setup 8 | 9 | ### Note 10 | 11 | - The devices must support `homeassistant.turn_on` and `homeassistant.turn_off`; otherwise, errors will be logged, and the blueprint will not work. 12 | - This blueprint is a dependency for the `device_control_timer_full_llm.yaml` blueprint. 13 | - The mentioned file(s) is/are included in the repository. 14 | - Do not alter the default script name. 15 | domain: script 16 | homeassistant: 17 | min_version: 2024.10.0 18 | input: 19 | custom_settings: 20 | name: Custom Settings 21 | icon: mdi:tools 22 | description: You can use these settings to configure the maximum number of simultaneous timers to run at a time. 23 | collapsed: true 24 | input: 25 | simultaneous: 26 | name: Simultaneous Timers 27 | selector: 28 | number: 29 | min: 10 30 | max: 50 31 | default: 30 32 | mode: parallel 33 | max: !input simultaneous 34 | max_exceeded: silent 35 | variables: 36 | version: 20250923 37 | fields: 38 | devices: 39 | name: Devices 40 | description: Specify the names of the devices to control, separated by commas. 41 | selector: 42 | text: 43 | required: true 44 | control: 45 | name: Control 46 | description: "Action to control: use 'true' to turn on the device, or 'false' to turn off the device." 47 | selector: 48 | boolean: 49 | required: true 50 | timer: 51 | name: Timer 52 | description: The time delay to wait before executing an action. 53 | selector: 54 | time: 55 | required: true 56 | sequence: 57 | - variables: 58 | devices: "{{ devices | default('') | trim }}" 59 | control: "{{ control | default(false) }}" 60 | timer: "{{ timer | default('00:00:00') }}" 61 | - delay: "{{ timer }}" 62 | - action: homeassistant.turn_{{ 'on' if control else 'off' }} 63 | target: 64 | entity_id: "{{ devices }}" 65 | -------------------------------------------------------------------------------- /device_ringing_full_llm.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Voice - Ring Device 3 | author: luuquangvu 4 | description: | 5 | # Device Ringing Tool used for Voice Assistant 6 | 7 | ## Blueprint Setup 8 | 9 | ### Required 10 | 11 | - An LLM like Gemini or OpenAI. 12 | - Home Assistant Companion App must be installed on the device. 13 | - The device must allow notifications permission for Home Assistant Companion App. 14 | - The device must allow critical alerts for Home Assistant Companion App (iOS). 15 | 16 | ### Optional 17 | 18 | - Adjust the prompts for each field used in the script. The descriptions guide the LLM to provide the correct input. 19 | 20 | ### Note 21 | 22 | - Provide a concise and precise description for the script. This description will enable the LLM to recognize that the script is designed to ring the mobile device. 23 | - Make sure to expose the script to Assist after the script has been saved. 24 | - Do not alter the default script name. 25 | - Once the script is created, click the three dots in the top right corner, choose "Edit in YAML," and remove the `description: ...` line to restore the default. This step is important because it helps the LLM better understand the script's purpose. 26 | domain: script 27 | homeassistant: 28 | min_version: 2024.10.0 29 | input: 30 | prompt_settings: 31 | name: Prompt settings for the LLM 32 | icon: mdi:robot 33 | description: You can use these settings to finetune the prompts for your specific LLM (model). In most cases the defaults should be fine. 34 | collapsed: true 35 | input: 36 | ring_id_prompt: 37 | name: Ring ID Prompt 38 | description: The prompt which will be used for the LLM can provide the Ring ID for the query. 39 | selector: 40 | text: 41 | multiline: true 42 | default: | 43 | This argument is mandatory and must always be included. 44 | Specify the Ring ID of the device you want to activate its ringing function. 45 | mode: parallel 46 | max_exceeded: silent 47 | description: Triggers a specified device to ring. 48 | variables: 49 | version: 20251116 50 | fields: 51 | ring_id: 52 | name: Ring ID 53 | description: !input ring_id_prompt 54 | selector: 55 | text: 56 | required: true 57 | sequence: 58 | - variables: 59 | ring_id: "{{ ring_id | default('') | trim }}" 60 | - alias: Check if variables were set correctly 61 | if: 62 | - condition: template 63 | value_template: "{{ not (ring_id and ring_id.startswith('notify.mobile_app_')) }}" 64 | then: 65 | - alias: Set variable for error message 66 | variables: 67 | response: 68 | error: Unable to ring the device because the ring id is invalid. 69 | - alias: Stop the script 70 | stop: Unable to ring the device because the ring id is invalid. 71 | response_variable: response 72 | - repeat: 73 | count: 5 74 | sequence: 75 | - action: "{{ ring_id }}" 76 | data: 77 | message: Hello, is anybody here? 78 | title: I'm here 79 | data: 80 | push: 81 | interruption-level: critical 82 | sound: 83 | name: default 84 | critical: 1 85 | volume: 1 86 | ttl: 0 87 | priority: high 88 | channel: alarm_stream 89 | importance: high 90 | tag: "{{ this.entity_id }}" 91 | - delay: 92 | hours: 0 93 | minutes: 0 94 | seconds: 5 95 | milliseconds: 0 96 | - alias: Prepare success response for Assist 97 | variables: 98 | response: 99 | success: | 100 | Sent 5 critical ring notifications to {{ ring_id }} successfully. 101 | - stop: Finish and return response data 102 | response_variable: response 103 | -------------------------------------------------------------------------------- /play_youtube_video_full_llm.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Voice - Play YouTube Video 3 | author: luuquangvu 4 | description: | 5 | # Tool plays YouTube videos on a smart TV used for Voice Assistant 6 | 7 | ## Blueprint Setup 8 | 9 | ### Required 10 | 11 | - A smart TV integrated into Home Assistant. 12 | - Support only Android TV, Samsung TV, Apple TV. 13 | 14 | ### Optional 15 | 16 | - Adjust the prompts for each field used in the script. The descriptions guide the LLM to provide the correct input. 17 | 18 | ### Note 19 | 20 | - Provide a concise and precise description for the script. This will be utilized by the LLM to understand it should use this script for playing YouTube videos on a smart TV. 21 | - Make sure to expose the script to Assist after the script has been saved. 22 | - Do not alter the default script name. 23 | - Once the script is created, click the three dots in the top right corner, choose "Edit in YAML," and remove the `description: ...` line to restore the default. This step is important because it helps the LLM better understand the script's purpose. 24 | domain: script 25 | homeassistant: 26 | min_version: 2024.10.0 27 | input: 28 | media_player_settings: 29 | name: Settings for Media Player 30 | icon: mdi:television 31 | description: You can use these settings to configure a smart TV. 32 | input: 33 | media_player: 34 | name: Smart TV 35 | selector: 36 | entity: 37 | filter: 38 | - domain: media_player 39 | device_class: tv 40 | prompt_settings: 41 | name: Prompt settings for the LLM 42 | icon: mdi:robot 43 | description: You can use these settings to finetune the prompts for your specific LLM (model). In most cases the defaults should be fine. 44 | collapsed: true 45 | input: 46 | media_id_prompt: 47 | name: Media ID Prompt 48 | description: The prompt which will be used for the LLM can provide the Media ID for the query. 49 | selector: 50 | text: 51 | multiline: true 52 | default: | 53 | This argument is mandatory and must always be included. 54 | Specify the Media ID of the video you want to play. 55 | mode: parallel 56 | max_exceeded: silent 57 | description: Plays a specified YouTube video on a compatible device. 58 | variables: 59 | version: 20251116 60 | fields: 61 | media_id: 62 | name: Media ID 63 | description: !input media_id_prompt 64 | selector: 65 | text: 66 | required: true 67 | sequence: 68 | - variables: 69 | media_id: "{{ media_id | default('') | trim }}" 70 | media_player_entity: !input media_player 71 | media_player_name: "{{ state_attr(media_player_entity, 'friendly_name') | default('the selected TV') }}" 72 | - alias: Check if variables were set correctly 73 | if: 74 | - condition: template 75 | value_template: "{{ not media_id }}" 76 | then: 77 | - alias: Set variable for error message 78 | variables: 79 | response: 80 | error: Unable to play the video because the media id is empty. 81 | - alias: Stop the script 82 | stop: Unable to play the video because the media id is empty. 83 | response_variable: response 84 | - action: media_player.turn_on 85 | target: 86 | entity_id: "{{ media_player_entity }}" 87 | - action: media_player.play_media 88 | data: 89 | media_content_type: url 90 | media_content_id: "{{ 'https://www.youtube.com/watch?v=' ~ media_id }}" 91 | enqueue: play 92 | target: 93 | entity_id: "{{ media_player_entity }}" 94 | - alias: Prepare success response for Assist 95 | variables: 96 | response: 97 | success: | 98 | Playing YouTube video {{ media_id }} on {{ media_player_name }}. 99 | - stop: Finish and return response data 100 | response_variable: response 101 | -------------------------------------------------------------------------------- /traffic_fine_notification.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Traffic Fine Notification 3 | author: luuquangvu 4 | description: | 5 | # Nhận thông báo khi có thông tin phạt nguội 6 | 7 | - Dữ liệu được tra cứu trực tiếp từ Cổng thông tin điện tử Cục Cảnh sát giao thông (https://www.csgt.vn/). 8 | 9 | ## Yêu cầu 10 | 11 | - Cần cài đặt tích hợp Pyscript thông qua HACS và được cấu hình đúng cách. 12 | - Cần sao chép tập lệnh `scripts/traffic_fine_lookup_tool.py` vào thư mục `config/pyscript`. 13 | - Cần sao chép tập tin `scripts/requirements.txt` vào thư mục `config/pyscript`. 14 | - Các tệp được đề cập bên trên đều có sẵn trong kho lưu trữ Github. 15 | - Bật hai cài đặt cấu hình Pyscript để cho phép nhập bất kỳ gói Python nào và hiển thị hass dưới dạng một biến. 16 | - Cần cấu hình 1 khóa Gemini API trong `config/configuration.yaml` và `config/secrets.yaml` sử dụng cho việc giải mã CAPTCHA. 17 | 18 | ``` 19 | #File configuration.yaml 20 | pyscript: 21 | allow_all_imports: true 22 | hass_is_global: true 23 | gemini_api_key: !secret gemini_api_key 24 | ``` 25 | 26 | ``` 27 | #File secrets.yaml 28 | gemini_api_key: XXXXXX # Retrieve the key from the Google Cloud Console. 29 | ``` 30 | 31 | ## Hướng dẫn cài đặt 32 | 33 | ### Thêm Template Sensor cho từng xe theo mẫu bên dưới vào trong `config/configuration.yaml` của Home Assistant 34 | 35 | - Trong đó Sensor Time to Check để khai báo thời gian ngẫu nhiên thực hiện tra cứu, tối thiểu ngày 2 lần. 36 | - Sensor Biển Số sẽ lưu thông tin phạt nguội của xe. 37 | 38 | ``` 39 | #File configuration.yaml 40 | template: 41 | - triggers: 42 | - trigger: time_pattern 43 | hours: /6 44 | - trigger: event 45 | event_type: event_template_reloaded 46 | sensor: 47 | - name: Time to Check 30G12345 # Biển số xe 48 | unique_id: time_to_check_30g12345 # Biển số xe 49 | icon: mdi:clock-digital 50 | device_class: timestamp 51 | state: "{{ (now() + timedelta(minutes=range(1,121) | random)).isoformat() }}" 52 | - triggers: 53 | - trigger: time 54 | at: sensor.time_to_check_30g12345 # Biển số xe 55 | actions: 56 | - action: pyscript.traffic_fine_lookup_tool 57 | data: 58 | license_plate: 30G12345 # Biển số xe 59 | vehicle_type: "1" # Kiểu phương tiện 60 | response_variable: response 61 | sensor: 62 | - name: 30G12345 # Biển số xe 63 | unique_id: 30g12345 # Biển số xe 64 | icon: mdi:car 65 | state: "{{ response.message if response.get('status') else response.get('error') }}" 66 | attributes: 67 | data: "{{ response }}" 68 | ``` 69 | 70 | - Thay biển số bằng biển số xe của bạn, lưu ý phân biệt chữ hoa chữ thường. 71 | - vehicle_type: 72 | - "1": Ô tô 73 | - "2": Xe máy 74 | - "3": Xe máy điện 75 | - icon (tùy chọn): 76 | - "mdi:car": Ô tô 77 | - "mdi:motorbike": Xe máy 78 | - "mdi:motorbike-electric": Xe máy điện 79 | - Lưu lại cấu hình. 80 | - Developer Tools - YAML - Reload Template Entities. 81 | - Dữ liệu phạt nguội có thể không có luôn mà sẽ có sau khi đến thời gian tra cứu. 82 | 83 | ### Tạo script từ blueprint này 84 | 85 | - Chỉ định Biển Số của các xe mới được thêm từ bước trên, thêm được đồng thời nhiều xe. 86 | - Chỉ định các điện thoại sẽ nhận thông báo khi có thông tin phạt nguội. 87 | - Lưu lại script. 88 | domain: automation 89 | homeassistant: 90 | min_version: 2024.10.0 91 | input: 92 | vehicles: 93 | name: Vehicles 94 | description: List of vehicles to monitor. 95 | selector: 96 | entity: 97 | filter: 98 | - domain: sensor 99 | integration: template 100 | multiple: true 101 | notify_device: 102 | name: Device to notify 103 | description: Device needs to run the official Home Assistant app to receive notifications. 104 | selector: 105 | device: 106 | filter: 107 | - integration: mobile_app 108 | multiple: true 109 | mode: queued 110 | max_exceeded: silent 111 | variables: 112 | version: 20250923 113 | triggers: 114 | - trigger: state 115 | entity_id: !input vehicles 116 | conditions: 117 | - condition: template 118 | value_template: "{{ trigger.from_state.state not in ['unavailable', 'unknown'] }}" 119 | - condition: template 120 | value_template: "{{ trigger.to_state.state not in ['unavailable', 'unknown'] }}" 121 | - condition: template 122 | value_template: "{{ not trigger.to_state.attributes.data.get('error') }}" 123 | actions: 124 | - alias: Send a notification to each device 125 | repeat: 126 | for_each: !input notify_device 127 | sequence: 128 | - action: "notify.mobile_app_{{ device_attr(repeat.item, 'name') | slugify }}" 129 | data: 130 | title: Xe {{ trigger.to_state.attributes.friendly_name }} 131 | message: "{{ trigger.to_state.state }}" 132 | data: 133 | tag: "{{ 'tag_' ~ this.attributes.id }}" 134 | -------------------------------------------------------------------------------- /traffic_fine_lookup_full_llm.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Voice - Check Traffic Fine 3 | author: luuquangvu 4 | description: | 5 | # Tool for checking traffic fines used for Voice Assistant 6 | 7 | ## Blueprint Setup 8 | 9 | ### Required 10 | 11 | - The Pyscript integration needs to be installed through HACS and properly configured. 12 | - The `scripts/traffic_fine_lookup_tool.py` script needs to be copied into the `config/pyscript` folder. 13 | - The `scripts/requirements.txt` file needs to be copied into the `config/pyscript` folder. 14 | - The mentioned file(s) is/are included in the repository. 15 | - Enable two Pyscript configuration options in `config/configuration.yaml` to permit the import of any Python package and to expose hass as a variable. 16 | - A Gemini API key needs to be configured in `config/configuration.yaml` and `config/secrets.yaml`, as it's required for solving CAPTCHAs. 17 | 18 | ``` 19 | #File configuration.yaml 20 | pyscript: 21 | allow_all_imports: true 22 | hass_is_global: true 23 | gemini_api_key: !secret gemini_api_key 24 | ``` 25 | 26 | ``` 27 | #File secrets.yaml 28 | gemini_api_key: XXXXXX # Retrieve the key from the Google Cloud Console. 29 | ``` 30 | 31 | ### Optional 32 | 33 | - Adjust the prompts for each field used in the script. The descriptions guide the LLM to provide the correct input. 34 | 35 | ### Note 36 | 37 | - Provide a concise and precise description for the script. This description will enable the LLM to recognize that the script is designed to directly lookup traffic fines from Cổng thông tin điện tử Cục Cảnh sát giao thông (https://www.csgt.vn/). 38 | - Make sure to expose the script to Assist after the script has been saved. 39 | - Do not alter the default script name. 40 | - Once the script is created, click the three dots in the top right corner, choose "Edit in YAML," and remove the `description: ...` line to restore the default. This step is important because it helps the LLM better understand the script's purpose. 41 | domain: script 42 | homeassistant: 43 | min_version: 2024.10.0 44 | input: 45 | prompt_settings: 46 | name: Prompt settings for the LLM 47 | icon: mdi:robot 48 | description: You can use these settings to finetune the prompts for your specific LLM (model). In most cases the defaults should be fine. 49 | collapsed: true 50 | input: 51 | license_plate_prompt: 52 | name: License Plate Prompt 53 | description: The prompt which will be used for the LLM can provide the license plate number of vehicle for the query. 54 | selector: 55 | text: 56 | multiline: true 57 | default: | 58 | This argument is mandatory and must always be included. 59 | Specify the vehicle's license plate number to check for any traffic fines. 60 | A license plate number consists of a continuous sequence of alphanumeric characters with no spaces. 61 | If a license plate contains errors caused by user mispronunciation, correct it to the most likely accurate license plate number. 62 | vehicle_type_prompt: 63 | name: Vehicle Type Prompt 64 | description: The prompt which will be used for the LLM can provide the type of vehicle for the query. 65 | selector: 66 | text: 67 | multiline: true 68 | default: | 69 | This argument is mandatory and must always be included. 70 | Specify the type of vehicle to check for any traffic fines. 71 | It must be one of the following three values: '1' for a car, '2' for a motorbike, or '3' for an electric bicycle. 72 | mode: parallel 73 | max_exceeded: silent 74 | description: Looks up information about traffic fines. 75 | variables: 76 | version: 20251116 77 | fields: 78 | license_plate: 79 | name: License Plate 80 | description: !input license_plate_prompt 81 | selector: 82 | text: 83 | required: true 84 | vehicle_type: 85 | name: Vehicle Type 86 | description: !input vehicle_type_prompt 87 | selector: 88 | select: 89 | options: 90 | - label: Car 91 | value: "1" 92 | - label: Motorbike 93 | value: "2" 94 | - label: Electric Bicycle 95 | value: "3" 96 | required: true 97 | default: "1" 98 | sequence: 99 | - variables: 100 | license_plate: "{{ license_plate | default('') | trim }}" 101 | vehicle_type: "{{ vehicle_type | default('') | trim }}" 102 | - alias: Check if variables were set correctly 103 | if: 104 | - condition: template 105 | value_template: "{{ not (license_plate and vehicle_type) }}" 106 | then: 107 | - alias: Set variable for error message 108 | variables: 109 | response: 110 | error: Unable to check traffic fines because either the license plate number or the vehicle type is empty. 111 | - alias: Stop the script 112 | stop: Unable to check traffic fines because either the license plate number or the vehicle type is empty. 113 | response_variable: response 114 | - action: pyscript.traffic_fine_lookup_tool 115 | response_variable: response 116 | data: 117 | license_plate: "{{ license_plate }}" 118 | vehicle_type: "{{ vehicle_type }}" 119 | - stop: "" 120 | response_variable: response 121 | -------------------------------------------------------------------------------- /home_assistant_device_location_lookup_guide_en.md: -------------------------------------------------------------------------------- 1 | # How to Configure Voice Assist to Find Your Devices 2 | 3 | ![image](images/20250608_ASxwFa.png) 4 | 5 | ## Features 6 | 7 | - **Location Tracking:** Voice Assist will tell you if a device is at home, and specifically which room it is in (if detected). 8 | - **Universal BLE Support:** Works with any BLE device tracked by the [Bermuda BLE Trilateration](https://github.com/agittins/bermuda) integration (Android, iOS, smartwatches, beacon tiles, etc.). 9 | - **Mobile App Support:** Locates any device with the Home Assistant Companion App installed. 10 | - **Ring to Find:** If the device has the Companion App, Voice Assist can trigger it to ring, even if it's in "Do Not Disturb" mode. 11 | - **Privacy Focused:** The LLM does not have real-time access to your GPS coordinates. It only receives general location info (Home/Away, specific room) strictly when you ask to find a device. 12 | 13 | ## Limitations 14 | 15 | - **LLM Compatibility:** Requires an LLM-based Voice Assist (like Gemini or OpenAI). 16 | - **Entity Support:** Only works with `device_tracker` entities from Bermuda or the Mobile App. 17 | 18 | ## Prerequisites 19 | 20 | - **Bermuda BLE Trilateration:** (Optional but recommended) Installed via HACS for room-level tracking. 21 | - **Home Assistant Companion App:** Installed on mobile devices for "Ring to Find" functionality. 22 | 23 | ## Installation Guide 24 | 25 | ### Step 1: Expose Device Trackers to Voice Assist 26 | 27 | 1. Navigate to **Settings** > **Voice assistants** > **Expose**. 28 | 2. Expose **only one** `device_tracker` entity per physical device. 29 | 3. **Tip:** Add friendly **Aliases** to your entities (e.g., "My Phone", "Keys") to make voice commands more natural. 30 | 31 | **Important for Bermuda Users:** 32 | If your phone has both a Mobile App tracker and a Bermuda tracker: 33 | 34 | 1. **Expose only the Bermuda tracker** to Voice Assist (it provides better room-level accuracy). 35 | 2. **Rename the Bermuda device** to match the Mobile App device name. 36 | - _Example:_ If your Mobile App device is named `Pixel 9`, rename your Bermuda device to `Pixel 9` (or `Pixel 9 BLE`). 37 | - _Why?_ This links the accurate location from Bermuda with the "Ring" capability of the Mobile App. 38 | 39 | ### Step 2: Create a Shell Command for Alias Retrieval 40 | 41 | This command allows the system to read the aliases you've set for your entities. Add this to your `configuration.yaml` file: 42 | 43 | ```yaml 44 | shell_command: 45 | get_entity_alias: jq '[.data.entities[] | select(.options.conversation.should_expose == true and (.aliases | length > 0)) | {entity_id, aliases}]' ./.storage/core.entity_registry 46 | ``` 47 | 48 | ### Step 3: Create a Template Sensor 49 | 50 | This sensor stores the alias information for the blueprints to use. Add this to your `configuration.yaml` (under `template:` or merge with your existing configuration): 51 | 52 | ```yaml 53 | template: 54 | - triggers: 55 | - trigger: homeassistant 56 | event: start 57 | - trigger: event 58 | event_type: event_template_reloaded 59 | actions: 60 | - action: shell_command.get_entity_alias 61 | response_variable: response 62 | sensor: 63 | - name: "Assist: Entity IDs and Aliases" 64 | unique_id: entity_ids_and_aliases 65 | icon: mdi:format-list-bulleted 66 | device_class: timestamp 67 | state: "{{ now().isoformat() }}" 68 | attributes: 69 | entities: "{{ response.stdout }}" 70 | ``` 71 | 72 | **After adding these codes:** 73 | 74 | 1. **Restart** Home Assistant. 75 | 2. **Note:** If you add or change an Alias later, you must reload Template entities (Developer Tools > YAML > Template Entities) or restart HA. 76 | 77 | ### Step 4: Install Blueprints 78 | 79 | #### 1. Device Location Lookup Blueprint 80 | 81 | This blueprint powers the logic to find where your devices are. 82 | 83 | [![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fluuquangvu%2Ftutorials%2Fblob%2Fmain%2Fdevice_location_lookup_full_llm.yaml) 84 | 85 | 1. Import the blueprint. 86 | 2. Create a **Script** from it. 87 | 3. In the script settings, select the **Template Sensor** (`sensor.assist_entity_ids_and_aliases`) you created in Step 3. 88 | 4. **Keep the default script name** (or ensure it's easy for the LLM to recognize). 89 | 5. **Expose** this new script to Voice Assist. 90 | 91 | #### 2. Device Ringing Blueprint 92 | 93 | This blueprint allows Voice Assist to make your device ring. 94 | 95 | [![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fluuquangvu%2Ftutorials%2Fblob%2Fmain%2Fdevice_ringing_full_llm.yaml) 96 | 97 | 1. Import the blueprint. 98 | 2. Create a **Script** from it. 99 | 3. **Keep the default script name**. 100 | 4. **Expose** this new script to Voice Assist. 101 | 102 | ## Usage Examples 103 | 104 | Once set up, try asking Voice Assist: 105 | 106 | - "Find my phone." 107 | - "Where is my iPhone?" 108 | - "Where is my watch?" 109 | - "Where is my wallet?" (for BLE tags) 110 | - "Where is the dog?" (for pet tags) 111 | -------------------------------------------------------------------------------- /home_assistant_device_location_lookup_guide.md: -------------------------------------------------------------------------------- 1 | # Hướng dẫn Cấu hình Voice Assist để Tìm kiếm Thiết bị 2 | 3 | ![image](images/20250608_ASxwFa.png) 4 | 5 | ## Tính năng 6 | 7 | - **Định vị:** Voice Assist sẽ cho bạn biết thiết bị có ở nhà không, và cụ thể đang ở phòng nào (nếu hệ thống xác định được). 8 | - **Hỗ trợ BLE đa dạng:** Hoạt động với mọi thiết bị BLE được theo dõi bởi integration [Bermuda BLE Trilateration](https://github.com/agittins/bermuda) (Android, iOS, đồng hồ thông minh, thẻ định vị...). 9 | - **Hỗ trợ thiết bị di động:** Định vị bất kỳ thiết bị nào có cài đặt ứng dụng Home Assistant Companion. 10 | - **Đổ chuông tìm kiếm:** Nếu thiết bị có cài App Home Assistant, Voice Assist có thể kích hoạt đổ chuông ngay cả khi điện thoại đang ở chế độ "Không làm phiền" (Do Not Disturb). 11 | - **Bảo mật:** LLM không truy cập tọa độ GPS thực tế của bạn. Nó chỉ nhận thông tin chung (Ở nhà/Vắng nhà, tên phòng) khi bạn yêu cầu tìm thiết bị. 12 | 13 | ## Hạn chế 14 | 15 | - **Yêu cầu LLM:** Chỉ hoạt động với các trợ lý ảo dựa trên LLM (như Gemini, OpenAI). 16 | - **Thực thể hỗ trợ:** Chỉ hỗ trợ thực thể `device_tracker` từ Bermuda hoặc Mobile App. 17 | 18 | ## Yêu cầu trước khi cài đặt 19 | 20 | - **Bermuda BLE Trilateration:** (Khuyên dùng) Cài đặt qua HACS để có khả năng định vị chính xác theo từng phòng. 21 | - **Home Assistant Companion App:** Cài trên điện thoại/máy tính bảng để sử dụng tính năng đổ chuông. 22 | 23 | ## Hướng dẫn Cài đặt 24 | 25 | ### Bước 1: Công khai (Expose) Device Tracker cho Voice Assist 26 | 27 | 1. Truy cập **Cài đặt** > **Trợ lý giọng nói** > **Expose**. 28 | 2. Chỉ expose **một** thực thể `device_tracker` duy nhất cho mỗi thiết bị vật lý. 29 | 3. **Mẹo:** Đặt thêm **Biệt danh (Alias)** cho thiết bị (ví dụ: "Điện thoại của tôi", "Chìa khóa") để gọi tên tự nhiên hơn. 30 | 31 | **Lưu ý quan trọng cho người dùng Bermuda:** 32 | Nếu điện thoại của bạn có cả tracker từ Mobile App và Bermuda: 33 | 34 | 1. **Chỉ expose tracker của Bermuda** cho Voice Assist (để định vị phòng chính xác hơn). 35 | 2. **Đổi tên thiết bị Bermuda** trùng với tên thiết bị Mobile App. 36 | - _Ví dụ:_ Nếu Mobile App tên là `Pixel 9`, hãy đổi tên thiết bị Bermuda thành `Pixel 9` (hoặc `Pixel 9 BLE`). 37 | - _Tại sao?_ Việc này giúp liên kết vị trí chính xác từ Bermuda với khả năng "Đổ chuông" của Mobile App. 38 | 39 | ### Bước 2: Tạo Shell Command lấy Alias 40 | 41 | Lệnh này giúp hệ thống đọc được các biệt danh bạn đã đặt. Thêm đoạn sau vào `configuration.yaml`: 42 | 43 | ```yaml 44 | shell_command: 45 | get_entity_alias: jq '[.data.entities[] | select(.options.conversation.should_expose == true and (.aliases | length > 0)) | {entity_id, aliases}]' ./.storage/core.entity_registry 46 | ``` 47 | 48 | ### Bước 3: Tạo Template Sensor 49 | 50 | Sensor này lưu trữ thông tin alias để blueprint sử dụng. Thêm vào `configuration.yaml` (dưới mục `template:` hoặc gộp vào cấu hình hiện có): 51 | 52 | ```yaml 53 | template: 54 | - triggers: 55 | - trigger: homeassistant 56 | event: start 57 | - trigger: event 58 | event_type: event_template_reloaded 59 | actions: 60 | - action: shell_command.get_entity_alias 61 | response_variable: response 62 | sensor: 63 | - name: "Assist: Entity IDs and Aliases" 64 | unique_id: entity_ids_and_aliases 65 | icon: mdi:format-list-bulleted 66 | device_class: timestamp 67 | state: "{{ now().isoformat() }}" 68 | attributes: 69 | entities: "{{ response.stdout }}" 70 | ``` 71 | 72 | **Sau khi thêm mã:** 73 | 74 | 1. **Khởi động lại (Restart)** Home Assistant. 75 | 2. **Lưu ý:** Nếu sau này bạn thêm hoặc sửa Alias, hãy nhớ reload Template entities (Developer Tools > YAML > Template Entities) hoặc khởi động lại HA. 76 | 77 | ### Bước 4: Cài đặt Blueprints 78 | 79 | #### 1. Blueprint Tìm vị trí (Location Lookup) 80 | 81 | Blueprint này xử lý logic để xác định vị trí thiết bị. 82 | 83 | [![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fluuquangvu%2Ftutorials%2Fblob%2Fmain%2Fdevice_location_lookup_full_llm.yaml) 84 | 85 | 1. Import blueprint. 86 | 2. Tạo một **Script** từ blueprint này. 87 | 3. Trong cấu hình script, chọn **Template Sensor** (`sensor.assist_entity_ids_and_aliases`) đã tạo ở Bước 3. 88 | 4. **Giữ nguyên tên script mặc định** (hoặc đặt tên dễ hiểu để LLM nhận diện). 89 | 5. **Expose** script này cho Voice Assist. 90 | 91 | #### 2. Blueprint Đổ chuông (Ringing) 92 | 93 | Blueprint này cho phép Voice Assist kích hoạt thiết bị đổ chuông. 94 | 95 | [![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fluuquangvu%2Ftutorials%2Fblob%2Fmain%2Fdevice_ringing_full_llm.yaml) 96 | 97 | 1. Import blueprint. 98 | 2. Tạo một **Script** từ blueprint này. 99 | 3. **Giữ nguyên tên script mặc định**. 100 | 4. **Expose** script này cho Voice Assist. 101 | 102 | ## Ví dụ sử dụng 103 | 104 | Sau khi cài đặt xong, hãy thử hỏi Voice Assist: 105 | 106 | - "Tìm điện thoại của tôi." 107 | - "iPhone của tôi đang ở đâu?" 108 | - "Đồng hồ của tôi đâu rồi?" 109 | - "Ví tiền đang ở đâu?" (với thẻ tag BLE) 110 | - "Con chó đang ở đâu?" (với thẻ tag đeo cho thú cưng) 111 | -------------------------------------------------------------------------------- /create_lunar_events.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Create Lunar Events 3 | author: luuquangvu 4 | description: | 5 | # Tool help to create Lunar calendar events 6 | 7 | ## Blueprint Setup 8 | 9 | ### Required 10 | 11 | - The Pyscript integration needs to be installed through HACS and properly configured. 12 | - The `scripts/date_conversion_tool.py` script needs to be copied into the `config/pyscript` folder. 13 | - The mentioned file(s) is/are included in the repository. 14 | - Enable two Pyscript configuration options in `config/configuration.yaml` to permit the import of any Python package and to expose hass as a variable. 15 | 16 | ``` 17 | #File configuration.yaml 18 | pyscript: 19 | allow_all_imports: true 20 | hass_is_global: true 21 | ``` 22 | 23 | - A calendar entity with write permissions to save events. 24 | 25 | ### Note 26 | 27 | - Google Calendar requires Read/Write access. 28 | domain: script 29 | homeassistant: 30 | min_version: 2024.10.0 31 | input: 32 | calendar_settings: 33 | name: Settings for Calendar 34 | icon: mdi:calendar 35 | description: You can use these settings to configure a Calendar. 36 | input: 37 | calendar: 38 | name: Calendar 39 | selector: 40 | entity: 41 | filter: 42 | - domain: calendar 43 | mode: single 44 | max_exceeded: silent 45 | variables: 46 | version: 20250923 47 | fields: 48 | lunar_date: 49 | selector: 50 | date: 51 | required: true 52 | name: Lunar Date 53 | description: Choose the start Lunar date of the event. 54 | days: 55 | selector: 56 | number: 57 | min: 1 58 | max: 30 59 | step: 1 60 | name: Days 61 | default: 1 62 | description: Choose the duration of the event. 63 | event_summary: 64 | selector: 65 | text: 66 | name: Event Summary 67 | required: true 68 | description: The summary of the event. 69 | event_description: 70 | selector: 71 | text: 72 | multiline: true 73 | name: Event Description 74 | description: More details about the event. 75 | repeat: 76 | selector: 77 | number: 78 | min: 1 79 | max: 30 80 | step: 1 81 | name: Repeat 82 | default: 1 83 | description: Select the number of years the event will repeat. 84 | leap_month: 85 | name: Leap Month 86 | description: When the event is in the leap month. The script will force Repeat to 1 which means no repeat. 87 | selector: 88 | boolean: 89 | sequence: 90 | - variables: 91 | lunar_date: "{{ lunar_date | as_datetime(default='') }}" 92 | start_date: "{{ as_datetime(lunar_date).date() if lunar_date else 'n/a' }}" 93 | days: "{{ days | default(1) | abs }}" 94 | event_summary: "{{ event_summary | default('') | trim }}" 95 | event_description: "{{ event_description | default('') | trim }}" 96 | repeat: "{{ repeat | default(1) | abs }}" 97 | leap_month: "{{ leap_month | default(false) }}" 98 | calendar: !input calendar 99 | - alias: Check if variables were set correctly 100 | if: 101 | - condition: template 102 | value_template: | 103 | {{ start_date == 'n/a' or not event_summary }} 104 | then: 105 | - alias: Stop the script 106 | stop: Unable to add the event because either the Lunar date or event summary is empty or the calendar entity is invalid. 107 | error: true 108 | - alias: Verify whether the calendar entity has write permissions. 109 | if: 110 | - condition: template 111 | value_template: | 112 | {% set feats = (state_attr(calendar, 'supported_features') | int(0)) -%} 113 | {{ (feats % 2) != 1 }} 114 | then: 115 | - alias: Stop the script 116 | stop: The event could not be added because the calendar entity lacks write permissions. 117 | error: true 118 | - alias: Verify the leap month 119 | if: 120 | - condition: template 121 | value_template: "{{ leap_month }}" 122 | then: 123 | - variables: 124 | repeat: 1 125 | - repeat: 126 | sequence: 127 | - action: pyscript.date_conversion_tool 128 | data: 129 | conversion_type: l2s 130 | date: "{{ start_date }}" 131 | leap_month: "{{ leap_month }}" 132 | response_variable: response 133 | - variables: 134 | solar_date: | 135 | {{ response.date if (response is defined and response.get('date')) }} 136 | - action: calendar.create_event 137 | data: 138 | summary: "{{ event_summary }}" 139 | description: "{{ event_description }}" 140 | start_date: "{{ solar_date }}" 141 | end_date: | 142 | {{ (as_datetime(solar_date) + timedelta(days=days)).date() }} 143 | target: 144 | entity_id: !input calendar 145 | - variables: 146 | start_date: | 147 | {{ as_datetime(start_date).replace(year=as_datetime(start_date).year + 1).date() }} 148 | - alias: Add a small delay when creating Google Calendar events. 149 | if: 150 | - condition: template 151 | value_template: "{{ calendar in integration_entities('google') }}" 152 | then: 153 | - delay: 154 | hours: 0 155 | minutes: 0 156 | seconds: 0 157 | milliseconds: 200 158 | count: "{{ repeat }}" 159 | -------------------------------------------------------------------------------- /advanced_google_search_full_llm.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Voice - Perform Google Search 3 | author: luuquangvu 4 | description: | 5 | # Tool for searching Google information used for Voice Assistant 6 | 7 | ## Blueprint Setup 8 | 9 | ### Required 10 | 11 | - This blueprint is exclusively designed for Google Generative AI only. 12 | - A Conversation Agent with Google Search tool enabled, create it if it doesn't already exist. 13 | - Increase the agent's maximum token limit to enable it to gather comprehensive information, with a minimum requirement of 16384 tokens. 14 | - Agent Instructions: 15 | 16 | ``` 17 | You are a voice assistant. Always respond in the same language as the user's message. If in Vietnamese, reply in Vietnamese; if in English, reply in English. Current date and time: {{ now().isoformat(timespec='seconds') }}. 18 | ``` 19 | 20 | ### Optional 21 | 22 | - Adjust the prompts for each field used in the script. The descriptions guide the LLM to provide the correct input. 23 | 24 | ### Note 25 | 26 | - Provide a concise and precise description for the script. This description will enable the LLM to recognize that the script is designed to search for information from Google. 27 | - Make sure to expose the script to Assist after the script has been saved. 28 | - Do not alter the default script name. 29 | - Once the script is created, click the three dots in the top right corner, choose "Edit in YAML," and remove the `description: ...` line to restore the default. This step is important because it helps the LLM better understand the script's purpose. 30 | domain: script 31 | homeassistant: 32 | min_version: 2025.4.0 33 | input: 34 | agent_settings: 35 | name: Settings for Conversation Agent 36 | icon: mdi:robot-outline 37 | description: These settings enable you to configure the conversation agent used for searching Google information. 38 | input: 39 | agent_id: 40 | name: Conversation Agent 41 | selector: 42 | entity: 43 | filter: 44 | domain: conversation 45 | prompt_settings: 46 | name: Prompt settings for the LLM 47 | icon: mdi:robot 48 | description: You can use these settings to finetune the prompts for your specific LLM (model). In most cases the defaults should be fine. 49 | collapsed: true 50 | input: 51 | query_string_prompt: 52 | name: Query String Prompt 53 | description: The prompt which will be used for the LLM can provide the search string for the query. 54 | selector: 55 | text: 56 | multiline: true 57 | default: | 58 | This argument is mandatory and must always be included. 59 | Specify the search string to obtain information from Google. 60 | If the query seems misspelled or misheard, silently correct it to the most likely intended search string and proceed. Do not ask clarifying questions; instead, briefly note the correction in the response. 61 | language_prompt: 62 | name: Language Prompt 63 | description: The prompt which will be used for the LLM can provide the language for the query. 64 | selector: 65 | text: 66 | multiline: true 67 | default: | 68 | This argument is mandatory and must always be included. 69 | Specify the output language of the response using an IETF BCP 47 language tag (e.g., vi-VN, en-US). The value must match the user's language. 70 | mode: parallel 71 | max_exceeded: silent 72 | description: Searches Google based on a query string. 73 | variables: 74 | version: 20251116 75 | fields: 76 | query_string: 77 | name: Query String 78 | description: !input query_string_prompt 79 | selector: 80 | text: 81 | required: true 82 | language: 83 | name: Language 84 | description: !input language_prompt 85 | selector: 86 | text: 87 | required: true 88 | sequence: 89 | - variables: 90 | query_string: "{{ query_string | default('') | trim }}" 91 | language: "{{ language | default('') | trim }}" 92 | - alias: Check if variables were set correctly 93 | if: 94 | - condition: template 95 | value_template: "{{ not query_string or not language }}" 96 | then: 97 | - alias: Set variable for error message 98 | variables: 99 | response: 100 | error: Unable to search for information because the query string or language is empty. 101 | - alias: Stop the script 102 | stop: Unable to search for information because the query string or language is empty. 103 | response_variable: response 104 | - action: conversation.process 105 | response_variable: response 106 | data: 107 | agent_id: !input agent_id 108 | text: | 109 | Use Google Search to gather accurate, comprehensive information about: 110 | {{ query_string }} 111 | 112 | Instructions: 113 | - Perform up to three iterative refinements. Avoid duplicate sources; prioritize recent, authoritative content. 114 | - Include both a concise overview and expanded details to minimize the need for follow-up searches. 115 | - Optionally include source names (publisher/site, date). 116 | - Always use the Google Search tool for factual lookups. If no results are found, state this clearly and suggest refined queries. Never fabricate sources. 117 | language: "{{ language }}" 118 | - stop: "" 119 | response_variable: response 120 | -------------------------------------------------------------------------------- /date_lookup_and_conversion_full_llm.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Voice - Convert Calendar Dates 3 | author: luuquangvu 4 | description: | 5 | # Tool converts Solar date to Lunar date and vice versa used for Voice Assistant 6 | 7 | ## Blueprint Setup 8 | 9 | ### Required 10 | 11 | - The Pyscript integration needs to be installed through HACS and properly configured. 12 | - The `scripts/date_conversion_tool.py` script needs to be copied into the `config/pyscript` folder. 13 | - The mentioned file(s) is/are included in the repository. 14 | - Enable two Pyscript configuration options in `config/configuration.yaml` to permit the import of any Python package and to expose hass as a variable. 15 | 16 | ``` 17 | #File configuration.yaml 18 | pyscript: 19 | allow_all_imports: true 20 | hass_is_global: true 21 | ``` 22 | 23 | ### Optional 24 | 25 | - Adjust the prompts for each field used in the script. The descriptions guide the LLM to provide the correct input. 26 | 27 | ### Note 28 | 29 | - Provide a concise and precise description for the script. This will be utilized by the LLM to understand it should use this script for date conversion from Solar date to Lunar date and vice versa. 30 | - Make sure to expose the script to Assist after the script has been saved. 31 | - Do not alter the default script name. 32 | - Once the script is created, click the three dots in the top right corner, choose "Edit in YAML," and remove the `description: ...` line to restore the default. This step is important because it helps the LLM better understand the script's purpose. 33 | domain: script 34 | homeassistant: 35 | min_version: 2024.10.0 36 | input: 37 | prompt_settings: 38 | name: Prompt settings for the LLM 39 | icon: mdi:robot 40 | description: You can use these settings to finetune the prompts for your specific LLM (model). In most cases the defaults should be fine. 41 | collapsed: true 42 | input: 43 | conversion_type_prompt: 44 | name: Conversion Type Prompt 45 | description: The prompt which will be used for the LLM can provide the type for the conversion. 46 | selector: 47 | text: 48 | multiline: true 49 | default: | 50 | This argument is mandatory and must always be included. 51 | It must be one of the following two values: 's2l' to convert a Solar date to a Lunar date, or 'l2s' to convert a Lunar date to a Solar date. 52 | After obtaining the result, ensure the response clearly specifies the day of the week, the Solar date, the Lunar date, and the number of days remaining or days elapsed. 53 | date_prompt: 54 | name: Date Prompt 55 | description: The prompt which will be used for the LLM can provide the input date for the conversion. 56 | selector: 57 | text: 58 | multiline: true 59 | default: | 60 | This argument is mandatory and must always be included. 61 | Specify the date for the requested conversion using the format YYYY-MM-DD. 62 | Always include the relevant date: today's date for today's request, tomorrow's date for tomorrow's request. 63 | If the request does not specify a year, assume the current year. 64 | leap_month_prompt: 65 | name: Leap Month Prompt 66 | description: The prompt which will be used for the LLM can provide the leap month for the conversion. 67 | selector: 68 | text: 69 | multiline: true 70 | default: | 71 | This argument is optional. 72 | Use this argument only when converting Lunar dates to Solar dates. 73 | It must be one of the following two values: 'true' for a leap month, or 'false' for a regular month. 74 | mode: parallel 75 | max_exceeded: silent 76 | description: Converts dates between Solar and Lunar calendars, looks up auspicious/inauspicious days according to East Asian traditions, and determines the day of the week for any given date. 77 | variables: 78 | version: 20251116 79 | fields: 80 | conversion_type: 81 | name: Conversion Type 82 | description: !input conversion_type_prompt 83 | selector: 84 | select: 85 | options: 86 | - s2l 87 | - l2s 88 | required: true 89 | date: 90 | name: Date 91 | description: !input date_prompt 92 | selector: 93 | date: 94 | required: true 95 | leap_month: 96 | name: Leap Month 97 | description: !input leap_month_prompt 98 | selector: 99 | boolean: 100 | sequence: 101 | - variables: 102 | conversion_type: "{{ conversion_type | default('') | trim }}" 103 | date: "{{ (date | as_datetime(default=now())).date() }}" 104 | leap_month: "{{ leap_month | default(false) }}" 105 | - alias: Check if variables were set correctly 106 | if: 107 | - condition: template 108 | value_template: "{{ not (conversion_type in ['s2l', 'l2s']) }}" 109 | then: 110 | - alias: Set variable for error message 111 | variables: 112 | response: 113 | error: Unable to perform the date conversion because the conversion type is invalid. 114 | - alias: Stop the script 115 | stop: Unable to perform the date conversion because the conversion type is invalid. 116 | response_variable: response 117 | - action: pyscript.date_conversion_tool 118 | response_variable: response 119 | data: 120 | conversion_type: "{{ conversion_type }}" 121 | date: "{{ date }}" 122 | leap_month: "{{ leap_month }}" 123 | - stop: "" 124 | response_variable: response 125 | -------------------------------------------------------------------------------- /home_assistant_ios_themes.md: -------------------------------------------------------------------------------- 1 | # Hướng dẫn cài đặt iOS Themes cho Home Assistant 2 | 3 | Hướng dẫn này giúp bạn cài đặt bộ giao diện iOS đẹp mắt, tự động chuyển đổi Sáng/Tối theo thời gian và lưu trữ hình nền cục bộ để tải nhanh hơn. 4 | 5 | ## 1. Cài đặt các thành phần cần thiết 6 | 7 | - **HACS:** Đảm bảo bạn đã cài đặt [HACS](https://github.com/hacs). 8 | - **Cấu hình Themes:** Đảm bảo file `configuration.yaml` của bạn đã có dòng cấu hình để load themes (nếu chưa có, hãy thêm vào): 9 | ```yaml 10 | frontend: 11 | themes: !include_dir_merge_named themes 12 | ``` 13 | 14 | ### Cài đặt qua HACS: 15 | 16 | 1. Truy cập **HACS** > **Frontend**. 17 | 2. Tìm kiếm và cài đặt **iOS Themes** ([basnijholt/lovelace-ios-themes](https://github.com/basnijholt/lovelace-ios-themes)). 18 | 3. Truy cập **HACS** > **Integrations**. 19 | 4. Tìm kiếm và cài đặt **Spook** ([frenck/spook](https://github.com/frenck/spook)). 20 | - _Lưu ý:_ Spook cung cấp tính năng `input_select.random` cần thiết cho hướng dẫn này. 21 | 22 | ## 2. Cấu hình hình nền cục bộ (Local Backgrounds) 23 | 24 | Việc này giúp hình nền tải nhanh hơn từ mạng nội bộ thay vì phải tải từ internet mỗi lần mở app. 25 | 26 | 1. Sử dụng File Editor hoặc VS Code để truy cập thư mục cấu hình Home Assistant. 27 | 2. Tìm đến thư mục `themes/ios-themes` (nơi HACS đã tải về). 28 | 3. Sao chép toàn bộ các file ảnh `.jpg` trong đó. 29 | 4. Dán chúng vào thư mục `www/ios-themes`. 30 | - _Nếu chưa có thư mục `www`, hãy tạo mới nó ngang hàng với file `configuration.yaml`._ 31 | - _Nếu chưa có thư mục `ios-themes` trong `www`, hãy tạo mới nó._ 32 | 5. **Khởi động lại** Home Assistant để áp dụng các thay đổi. 33 | 34 | ## 3. Tạo tính năng tự động đổi Theme (Auto Light/Dark) 35 | 36 | ### 3.1. Tạo các biến trợ giúp (Helpers) 37 | 38 | Bạn có thể thêm code vào `configuration.yaml` hoặc tạo bằng giao diện (Settings > Devices & Services > Helpers). 39 | 40 | **Code YAML (thêm vào configuration.yaml):** 41 | 42 | ```yaml 43 | input_select: 44 | ios_themes: 45 | name: iOS Themes 46 | icon: mdi:palette 47 | options: 48 | - dark-green 49 | - light-green 50 | - dark-blue 51 | - light-blue 52 | - blue-red 53 | - orange 54 | - red 55 | 56 | input_boolean: 57 | ios_themes_dark_mode: 58 | name: iOS Themes Dark Mode 59 | icon: mdi:theme-light-dark 60 | ios_themes_local_backgrounds: 61 | name: iOS Themes Local Backgrounds 62 | icon: mdi:cloud 63 | initial: on 64 | ``` 65 | 66 | ### 3.2. Tạo Automation 67 | 68 | Automation này sẽ tự động chuyển sang chế độ Sáng khi mặt trời mọc và Tối khi mặt trời lặn, đồng thời áp dụng hình nền ngẫu nhiên mỗi ngày. 69 | 70 | ```yaml 71 | alias: "System: Auto change iOS themes" 72 | description: "Tự động đổi theme Sáng/Tối và chọn màu ngẫu nhiên" 73 | triggers: 74 | - trigger: sun 75 | event: sunrise 76 | offset: 0 77 | id: Sunrise 78 | - trigger: sun 79 | event: sunset 80 | offset: 0 81 | id: Sunset 82 | - trigger: state 83 | entity_id: 84 | - input_select.ios_themes 85 | - input_boolean.ios_themes_dark_mode 86 | - input_boolean.ios_themes_local_backgrounds 87 | id: iOS Themes 88 | conditions: [] 89 | actions: 90 | - choose: 91 | - conditions: 92 | - condition: trigger 93 | id: 94 | - Sunrise 95 | sequence: 96 | - action: input_boolean.turn_off 97 | target: 98 | entity_id: input_boolean.ios_themes_dark_mode 99 | - action: input_select.random 100 | target: 101 | entity_id: input_select.ios_themes 102 | - conditions: 103 | - condition: trigger 104 | id: 105 | - Sunset 106 | sequence: 107 | - action: input_boolean.turn_on 108 | target: 109 | entity_id: input_boolean.ios_themes_dark_mode 110 | - action: input_select.random 111 | target: 112 | entity_id: input_select.ios_themes 113 | - conditions: 114 | - condition: trigger 115 | id: 116 | - iOS Themes 117 | sequence: 118 | - action: frontend.set_theme 119 | data: 120 | name: >- 121 | {% set which = 'dark' if is_state('input_boolean.ios_themes_dark_mode', 'on') else 'light' -%} 122 | {% set name = states('input_select.ios_themes') -%} 123 | {% set suffix = '-alternative' if is_state('input_boolean.ios_themes_local_backgrounds', 'on') else '' -%} 124 | ios-{{ which }}-mode-{{ name }}{{ suffix }} 125 | mode: queued 126 | max: 10 127 | ``` 128 | 129 | ## 4. Kích hoạt Theme trên thiết bị 130 | 131 | **Bước quan trọng nhất:** Để automation có thể thay đổi giao diện của bạn, bạn phải chọn chế độ **Use default theme** trong cài đặt người dùng. 132 | 133 | 1. Nhấn vào biểu tượng **Hồ sơ người dùng (User Profile)** ở góc dưới cùng bên trái thanh menu. 134 | 2. Tại mục **Theme**, chọn **Use default theme**. 135 | 136 | ## 5. Đưa công cụ điều khiển ra Dashboard (Tùy chọn) 137 | 138 | Nếu bạn muốn tự tay đổi màu hoặc chế độ Sáng/Tối ngay trên màn hình chính. 139 | _Yêu cầu: Đã cài đặt [Mushroom Cards](https://github.com/piitaya/lovelace-mushroom)._ 140 | 141 | ```yaml 142 | type: grid 143 | square: false 144 | columns: 2 145 | cards: 146 | - type: custom:mushroom-select-card 147 | entity: input_select.ios_themes 148 | secondary_info: none 149 | icon_color: primary 150 | layout: horizontal 151 | tap_action: 152 | action: perform-action 153 | perform_action: input_select.random 154 | target: 155 | entity_id: input_select.ios_themes 156 | - type: tile 157 | entity: input_boolean.ios_themes_dark_mode 158 | hide_state: true 159 | color: primary 160 | vertical: true 161 | icon_tap_action: 162 | action: toggle 163 | ``` 164 | -------------------------------------------------------------------------------- /scripts/youtube_data_tool.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any 3 | 4 | from googleapiclient.discovery import build 5 | from googleapiclient.errors import HttpError 6 | 7 | YOUTUBE_API_KEY = pyscript.config.get("youtube_api_key") 8 | YOUTUBE_API_SERVICE_NAME = "youtube" 9 | YOUTUBE_API_VERSION = "v3" 10 | 11 | YOUTUBE_CLIENT: Any = None 12 | _YOUTUBE_LOCK = asyncio.Lock() 13 | 14 | if not YOUTUBE_API_KEY: 15 | raise ValueError("You need to configure your YouTube API key") 16 | 17 | 18 | @pyscript_compile 19 | def _build_youtube_client() -> Any: 20 | """Build the YouTube API client.""" 21 | return build( 22 | YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION, developerKey=YOUTUBE_API_KEY 23 | ) 24 | 25 | 26 | async def _ensure_youtube_client() -> None: 27 | """Ensure the global YouTube client is initialized once.""" 28 | global YOUTUBE_CLIENT 29 | if YOUTUBE_CLIENT is None: 30 | async with _YOUTUBE_LOCK: 31 | if YOUTUBE_CLIENT is None: 32 | YOUTUBE_CLIENT = await asyncio.to_thread(_build_youtube_client) 33 | 34 | 35 | @pyscript_compile 36 | def youtube_search( 37 | client: Any, 38 | query: str, 39 | results: int = 5, 40 | search_type: str = "video,channel,playlist", 41 | page_token: str = "", 42 | ) -> dict[str, Any]: 43 | """ 44 | Performs a search on YouTube. 45 | 46 | Args: 47 | client: The initialized YouTube API client. 48 | query: The search query string. 49 | results: The maximum number of results to return. 50 | search_type: The type of content to search for. 51 | page_token: The page token to get other pages that could be retrieved. 52 | 53 | Returns: 54 | A dictionary containing the search results from the YouTube API. 55 | """ 56 | search_response = ( 57 | client.search() 58 | .list( 59 | q=query, 60 | part="id,snippet", 61 | maxResults=results, 62 | type=search_type, 63 | pageToken=page_token, 64 | ) 65 | .execute() 66 | ) 67 | 68 | return search_response 69 | 70 | 71 | @service(supports_response="only") 72 | async def youtube_search_tool(query: str, **kwargs) -> dict[str, Any]: 73 | """ 74 | yaml 75 | name: YouTube Search Tool 76 | description: Search YouTube for videos, channels, and playlists. 77 | fields: 78 | query: 79 | name: Query 80 | description: Search keywords or phrase. 81 | example: Nikola Tesla 82 | required: true 83 | selector: 84 | text: 85 | search_type: 86 | name: Search Type 87 | description: Content types to include. 88 | example: video 89 | required: true 90 | selector: 91 | select: 92 | options: 93 | - video 94 | - channel 95 | - playlist 96 | multiple: true 97 | default: 98 | - video 99 | results: 100 | name: Results 101 | description: Maximum number of items to return (1-50). 102 | selector: 103 | number: 104 | min: 1 105 | max: 50 106 | default: 5 107 | page_token: 108 | name: Page Token 109 | description: Token for the next page of results. 110 | selector: 111 | text: 112 | """ 113 | if not query: 114 | return {"error": "Missing a required argument: query"} 115 | 116 | def _coerce_results(value: Any) -> int: 117 | try: 118 | coerced = int(value) 119 | except (TypeError, ValueError) as err: 120 | raise ValueError( 121 | "The results value must be an integer between 1 and 50" 122 | ) from err 123 | if not 1 <= coerced <= 50: 124 | raise ValueError("The results value must be between 1 and 50") 125 | return coerced 126 | 127 | def _coerce_search_types(value: Any) -> list[str]: 128 | valid_types = {"video", "channel", "playlist"} 129 | if value is None: 130 | items: list[str] = [] 131 | elif isinstance(value, str): 132 | items = [value] 133 | elif isinstance(value, (list, tuple, set)): 134 | items = [str(item) for item in value if str(item).strip()] 135 | else: 136 | raise ValueError( 137 | "The search_type value must be a string or list of strings" 138 | ) 139 | 140 | cleaned: list[str] = [] 141 | for item in items: 142 | item = item.strip().lower() # Normalize to lowercase 143 | if item in valid_types and item not in cleaned: 144 | cleaned.append(item) 145 | 146 | if not cleaned: 147 | cleaned = ["video"] 148 | return cleaned 149 | 150 | try: 151 | await _ensure_youtube_client() 152 | results = _coerce_results(kwargs.get("results", 5)) 153 | search_types = _coerce_search_types(kwargs.get("search_type", ["video"])) 154 | page_token = kwargs.get("page_token", "") or "" 155 | 156 | response = await asyncio.to_thread( 157 | youtube_search, 158 | YOUTUBE_CLIENT, 159 | query, 160 | results=results, 161 | search_type=",".join(search_types), 162 | page_token=page_token, 163 | ) 164 | if not isinstance(response, dict): 165 | return { 166 | "error": "Unexpected response from YouTube API", 167 | "detail": f"Expected dict, received {type(response).__name__}", 168 | } 169 | return response 170 | except HttpError as error: 171 | return { 172 | "error": "YouTube API Error", 173 | "detail": str(error), 174 | } 175 | except Exception as error: 176 | return {"error": f"An unexpected error occurred during processing: {error}"} 177 | -------------------------------------------------------------------------------- /home_assistant_ios_themes_en.md: -------------------------------------------------------------------------------- 1 | # Guide to Install iOS Themes for Home Assistant 2 | 3 | This guide helps you install a beautiful set of iOS themes, automatically switch between Light/Dark modes according to time, and store local backgrounds for faster loading. 4 | 5 | ## 1. Install Necessary Components 6 | 7 | - **HACS:** Ensure you have [HACS](https://github.com/hacs) installed. 8 | - **Themes Configuration:** Ensure your `configuration.yaml` file includes the configuration line to load themes (if not, add it): 9 | ```yaml 10 | frontend: 11 | themes: !include_dir_merge_named themes 12 | ``` 13 | 14 | ### Install via HACS: 15 | 16 | 1. Go to **HACS** > **Frontend**. 17 | 2. Search for and install **iOS Themes** ([basnijholt/lovelace-ios-themes](https://github.com/basnijholt/lovelace-ios-themes)). 18 | 3. Go to **HACS** > **Integrations**. 19 | 4. Search for and install **Spook** ([frenck/spook](https://github.com/frenck/spook)). 20 | - _Note:_ Spook provides the `input_select.random` feature required for this guide. 21 | 22 | ## 2. Configure Local Backgrounds 23 | 24 | This helps backgrounds load faster from your local network instead of downloading from the internet every time you open the app. 25 | 26 | 1. Use File Editor or VS Code to access your Home Assistant configuration directory. 27 | 2. Navigate to the `themes/ios-themes` folder (where HACS downloaded the themes). 28 | 3. Copy all `.jpg` image files from there. 29 | 4. Paste them into the `www/ios-themes` folder. 30 | - _If the `www` folder does not exist, create it at the same level as your `configuration.yaml` file._ 31 | - _If the `ios-themes` folder does not exist within `www`, create it._ 32 | 5. **Restart** Home Assistant to apply the changes. 33 | 34 | ## 3. Create Automatic Theme Switching (Auto Light/Dark) 35 | 36 | ### 3.1. Create Helper Entities 37 | 38 | You can add the code to `configuration.yaml` or create them via the UI (Settings > Devices & Services > Helpers). 39 | 40 | **YAML Code (add to configuration.yaml):** 41 | 42 | ```yaml 43 | input_select: 44 | ios_themes: 45 | name: iOS Themes 46 | icon: mdi:palette 47 | options: 48 | - dark-green 49 | - light-green 50 | - dark-blue 51 | - light-blue 52 | - blue-red 53 | - orange 54 | - red 55 | 56 | input_boolean: 57 | ios_themes_dark_mode: 58 | name: iOS Themes Dark Mode 59 | icon: mdi:theme-light-dark 60 | ios_themes_local_backgrounds: 61 | name: iOS Themes Local Backgrounds 62 | icon: mdi:cloud 63 | initial: on 64 | ``` 65 | 66 | ### 3.2. Create Automation 67 | 68 | This automation will automatically switch to Light mode at sunrise and Dark mode at sunset, and apply a random background daily. 69 | 70 | ```yaml 71 | alias: "System: Auto change iOS themes" 72 | description: "Automatically switch between Light/Dark themes and select random color" 73 | triggers: 74 | - trigger: sun 75 | event: sunrise 76 | offset: 0 77 | id: Sunrise 78 | - trigger: sun 79 | event: sunset 80 | offset: 0 81 | id: Sunset 82 | - trigger: state 83 | entity_id: 84 | - input_select.ios_themes 85 | - input_boolean.ios_themes_dark_mode 86 | - input_boolean.ios_themes_local_backgrounds 87 | id: iOS Themes 88 | conditions: [] 89 | actions: 90 | - choose: 91 | - conditions: 92 | - condition: trigger 93 | id: 94 | - Sunrise 95 | sequence: 96 | - action: input_boolean.turn_off 97 | target: 98 | entity_id: input_boolean.ios_themes_dark_mode 99 | - action: input_select.random 100 | target: 101 | entity_id: input_select.ios_themes 102 | - conditions: 103 | - condition: trigger 104 | id: 105 | - Sunset 106 | sequence: 107 | - action: input_boolean.turn_on 108 | target: 109 | entity_id: input_boolean.ios_themes_dark_mode 110 | - action: input_select.random 111 | target: 112 | entity_id: input_select.ios_themes 113 | - conditions: 114 | - condition: trigger 115 | id: 116 | - iOS Themes 117 | sequence: 118 | - action: frontend.set_theme 119 | data: 120 | name: >- 121 | {% set which = 'dark' if is_state('input_boolean.ios_themes_dark_mode', 'on') else 'light' -%} 122 | {% set name = states('input_select.ios_themes') -%} 123 | {% set suffix = '-alternative' if is_state('input_boolean.ios_themes_local_backgrounds', 'on') else '' -%} 124 | ios-{{ which }}-mode-{{ name }}{{ suffix }} 125 | mode: queued 126 | max: 10 127 | ``` 128 | 129 | ## 4. Activate Theme on Your Device 130 | 131 | **Most Important Step:** For the automation to change your interface, you must select **Use default theme** mode in your user settings. 132 | 133 | 1. Click on the **User Profile** icon in the bottom-left corner of the sidebar. 134 | 2. Under **Theme**, select **Use default theme**. 135 | 136 | ## 5. Add Control to Your Dashboard (Optional) 137 | 138 | If you want to manually change colors or Light/Dark mode directly from your main screen. 139 | _Requirement: [Mushroom Cards](https://github.com/piitaya/lovelace-mushroom) must be installed._ 140 | 141 | ```yaml 142 | type: grid 143 | square: false 144 | columns: 2 145 | cards: 146 | - type: custom:mushroom-select-card 147 | entity: input_select.ios_themes 148 | secondary_info: none 149 | icon_color: primary 150 | layout: horizontal 151 | tap_action: 152 | action: perform-action 153 | perform_action: input_select.random 154 | target: 155 | entity_id: input_select.ios_themes 156 | - type: tile 157 | entity: input_boolean.ios_themes_dark_mode 158 | hide_state: true 159 | color: primary 160 | vertical: true 161 | icon_tap_action: 162 | action: toggle 163 | ``` 164 | -------------------------------------------------------------------------------- /advanced_youtube_search_full_llm.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Voice - Perform YouTube Search 3 | author: luuquangvu 4 | description: | 5 | # Tool for searching YouTube videos used for Voice Assistant 6 | 7 | ## Blueprint Setup 8 | 9 | ### Required 10 | 11 | - The Pyscript integration needs to be installed through HACS and properly configured. 12 | - The `scripts/youtube_data_tool.py` script needs to be copied into the `config/pyscript` folder. 13 | - The `scripts/requirements.txt` file needs to be copied into the `config/pyscript` folder. 14 | - The mentioned file(s) is/are included in the repository. 15 | - Enable two Pyscript configuration options in `config/configuration.yaml` to permit the import of any Python package and to expose hass as a variable. 16 | - A YouTube API key needs to be configured in `config/configuration.yaml` and `config/secrets.yaml`. 17 | 18 | ``` 19 | #File configuration.yaml 20 | pyscript: 21 | allow_all_imports: true 22 | hass_is_global: true 23 | youtube_api_key: !secret youtube_api_key 24 | ``` 25 | 26 | ``` 27 | #File secrets.yaml 28 | youtube_api_key: XXXXXX # Retrieve the key from the Google Cloud Console. 29 | ``` 30 | 31 | ### Optional 32 | 33 | - Adjust the prompts for each field used in the script. The descriptions guide the LLM to provide the correct input. 34 | 35 | ### Note 36 | 37 | - The `play_youtube_video_full_llm.yaml` blueprint needs to be installed to play YouTube videos on a smart TV. 38 | - Provide a concise and precise description for the script. This description will enable the LLM to recognize that the script is designed to search videos from YouTube. 39 | - Make sure to expose the script to Assist after the script has been saved. 40 | - Do not alter the default script name. 41 | - Once the script is created, click the three dots in the top right corner, choose "Edit in YAML," and remove the `description: ...` line to restore the default. This step is important because it helps the LLM better understand the script's purpose. 42 | domain: script 43 | homeassistant: 44 | min_version: 2024.10.0 45 | input: 46 | results_settings: 47 | name: Settings for Results 48 | icon: mdi:youtube 49 | description: These settings allow you to define the maximum number of results that will return for each request. 50 | collapsed: true 51 | input: 52 | results: 53 | name: Number Of Results 54 | selector: 55 | number: 56 | min: 0 57 | max: 50 58 | step: 1 59 | default: 5 60 | prompt_settings: 61 | name: Prompt settings for the LLM 62 | icon: mdi:robot 63 | description: You can use these settings to finetune the prompts for your specific LLM (model). In most cases the defaults should be fine. 64 | collapsed: true 65 | input: 66 | query_string_prompt: 67 | name: Query String Prompt 68 | description: The prompt which will be used for the LLM can provide the search string for the query. 69 | selector: 70 | text: 71 | multiline: true 72 | default: | 73 | This argument is mandatory and must always be included. 74 | Specify the search string to obtain a list of videos. 75 | If the query contains errors caused by user mispronunciation, correct it to the most likely accurate search string and ask the user for confirmation before proceeding. 76 | After obtaining the results, prompt the user to select a video to play on the TV. Add an ordinal number to each video to simplify selection. Do not include the Media ID in the response. 77 | page_token_prompt: 78 | name: Page Token Prompt 79 | description: The prompt which will be used for the LLM can provide the page token to get additional videos that could be obtained. 80 | selector: 81 | text: 82 | multiline: true 83 | default: | 84 | This argument is optional. 85 | Specify the page token to obtain additional videos. 86 | Use it when the initial query results are unsatisfying and the user wants to find more videos. 87 | mode: parallel 88 | max_exceeded: silent 89 | description: Searches YouTube for videos based on a query string. 90 | variables: 91 | version: 20251116 92 | fields: 93 | query_string: 94 | name: Query String 95 | description: !input query_string_prompt 96 | selector: 97 | text: 98 | required: true 99 | page_token: 100 | name: Page Token 101 | description: !input page_token_prompt 102 | selector: 103 | text: 104 | sequence: 105 | - variables: 106 | results: !input results 107 | query_string: "{{ query_string | default('') | trim }}" 108 | page_token: "{{ page_token | default('') | trim }}" 109 | - alias: Check if variables were set correctly 110 | if: 111 | - condition: template 112 | value_template: "{{ not query_string }}" 113 | then: 114 | - alias: Set variable for error message 115 | variables: 116 | response: 117 | error: Unable to search for videos because the query string is empty. 118 | - alias: Stop the script 119 | stop: Unable to search for videos because the query string is empty. 120 | response_variable: response 121 | - action: pyscript.youtube_search_tool 122 | response_variable: response 123 | data: 124 | query: "{{ query_string }}" 125 | search_type: "video" 126 | results: "{{ results }}" 127 | page_token: "{{ page_token }}" 128 | - if: 129 | - condition: template 130 | value_template: "{{ response.get('error') }}" 131 | then: 132 | - variables: 133 | response: 134 | error: "{{ response.error }}" 135 | else: 136 | - variables: 137 | response: 138 | entries: | 139 | {% for entry in response.get('items') if entry.id.kind == 'youtube#video' -%} 140 | - channel: {{ entry.snippet.channelTitle | lower }} 141 | title: {{ entry.snippet.title | lower }} 142 | published: {{ strptime(entry.snippet.publishedAt, '%Y-%m-%dT%H:%M:%S%z') | relative_time }} ago 143 | media_id: {{ entry.id.videoId }} 144 | {% endfor -%} 145 | next_page_token: "{{ response.get('nextPageToken') }}" 146 | - stop: "" 147 | response_variable: response 148 | -------------------------------------------------------------------------------- /home_assistant_voice_instructions_en.md: -------------------------------------------------------------------------------- 1 | # How to Create a System Prompt for AI 2 | 3 | - **This system prompt is optimized to work most effectively with Gemini 2.5 Flash. Other models may require further adjustments to function as desired.** 4 | 5 | ## Summary 6 | 7 | - **Complete System Prompt.** 8 | 9 | ```text 10 | **Persona and Tone**: You are a voice assistant. Always respond in the same language as the user's message. Keep replies in one paragraph with normal sentence punctuation for natural flow. Use a friendly, natural, and concise tone that sounds pleasant and clear when read aloud. Current date and time: {{ now().isoformat(timespec='seconds') }}. 11 | **Tool Invocation Rules**: When invoking a tool, output only the tool call in the exact format required by the specification. If the required format includes a fenced block, always include it and do not alter or omit it. A fenced block includes any structured wrapper required by the tool format. Do not add any extra text, commentary, formatting normalization, or explanation. Tool responses are never treated as plain-text responses. 12 | **Plain Text Rules**: If no tool is needed, output the final user-facing answer in plain text only. Do not use Markdown, LaTeX, JSON, code formatting, emojis, mathematical expressions, symbolic notation, or any emphasis markup. Diacritics and characters from the user's language are allowed. 13 | **Follow-up Question Policy**: After each plain-text answer, ask if the user needs anything else, unless you already requested missing information or the user's message clearly ends the conversation. Tool calls do not require or include the follow-up question. Any brief or very short user response that indicates gratitude, acknowledgment, or refusal with no new request must always be treated as the end of the conversation. The follow-up question must be the final sentence, must end with a question mark, and must not be followed by any extra text. 14 | **Tools Usage and Error Policy**: Use the appropriate tool whenever the user's request requires it. If a tool call fails, returns an error, or produces an empty, malformed, or unusable result, it must always be considered failed. Automatically try another relevant tool if possible, asking the user only when essential information is missing. As a general principle, if a tool designed for real-time or sensor-based data returns an empty result, you should try a tool that searches for manually saved notes or static information related to the same query. Never output fake tool calls, code, or reasoning steps. When not invoking a tool, output only the final user-facing answer in plain text. If all tools fail, respond with a short one-line fallback in the user's language. 15 | # END OF SYSTEM MESSAGE 16 | ``` 17 | 18 | ## Details 19 | 20 | - **Persona and Tone:** Defines a friendly voice assistant persona that responds concisely and naturally in the user's language. Providing real-time context helps the AI accurately handle time-related queries like "today" or "tomorrow". 21 | 22 | ```text 23 | **Persona and Tone**: You are a voice assistant. Always respond in the same language as the user's message. Keep replies in one paragraph with normal sentence punctuation for natural flow. Use a friendly, natural, and concise tone that sounds pleasant and clear when read aloud. Current date and time: {{ now().isoformat(timespec='seconds') }}. 24 | ``` 25 | 26 | - **Tool Invocation Rules:** Requires the AI to output precise tool calls according to the technical format, with absolutely no extra text. This ensures Home Assistant can successfully parse and execute the command. 27 | 28 | ```text 29 | **Tool Invocation Rules**: When invoking a tool, output only the tool call in the exact format required by the specification. If the required format includes a fenced block, always include it and do not alter or omit it. A fenced block includes any structured wrapper required by the tool format. Do not add any extra text, commentary, formatting normalization, or explanation. Tool responses are never treated as plain-text responses. 30 | ``` 31 | 32 | - **Plain Text Rules:** Restricts responses to plain text only, removing special formats (Markdown, Emoji, JSON...) to avoid reading errors or mispronunciation by Text-to-Speech (TTS) engines. 33 | 34 | ```text 35 | **Plain Text Rules**: If no tool is needed, output the final user-facing answer in plain text only. Do not use Markdown, LaTeX, JSON, code formatting, emojis, mathematical expressions, symbolic notation, or any emphasis markup. Diacritics and characters from the user's language are allowed. 36 | ``` 37 | 38 | - **Follow-up Question Policy:** Maintains a Continuous Conversation by requiring the AI to always ask the user back after each answer, unless the conversation has clearly ended. 39 | 40 | ```text 41 | **Follow-up Question Policy**: After each plain-text answer, ask if the user needs anything else, unless you already requested missing information or the user's message clearly ends the conversation. Tool calls do not require or include the follow-up question. Any brief or very short user response that indicates gratitude, acknowledgment, or refusal with no new request must always be treated as the end of the conversation. The follow-up question must be the final sentence, must end with a question mark, and must not be followed by any extra text. 42 | ``` 43 | 44 | - **Tools Usage and Error Policy:** Smart error handling strategy: prioritizes automatically searching for alternative data sources (e.g., manual notes) when real-time data (sensors) is missing, ensuring the AI always provides useful feedback instead of reporting errors or hallucinating information. 45 | 46 | ```text 47 | **Tools Usage and Error Policy**: Use the appropriate tool whenever the user's request requires it. If a tool call fails, returns an error, or produces an empty, malformed, or unusable result, it must always be considered failed. Automatically try another relevant tool if possible, asking the user only when essential information is missing. As a general principle, if a tool designed for real-time or sensor-based data returns an empty result, you should try a tool that searches for manually saved notes or static information related to the same query. Never output fake tool calls, code, or reasoning steps. When not invoking a tool, output only the final user-facing answer in plain text. If all tools fail, respond with a short one-line fallback in the user's language. 48 | ``` 49 | 50 | - **End Marker:** A marker to indicate the end of the custom system prompt, clearly separating it from default instructions or additional context provided by Home Assistant. 51 | 52 | ```text 53 | # END OF SYSTEM MESSAGE 54 | ``` 55 | 56 | ## FAQ 57 | 58 | - **Why is the system prompt in English and not Vietnamese (or the user's native language)?** 59 | 60 | ```text 61 | Since the core training data of most large LLMs is in English, they understand and adhere to technical instructions better in English. Writing system prompts in other languages may cause the AI (especially smaller models) to misunderstand semantics or ignore complex constraint requirements. 62 | ``` 63 | 64 | - **Why does Voice Assist still encounter errors after applying this prompt?** 65 | 66 | ```text 67 | Because the architecture and training data of each model vary, some models may not strictly follow these instructions. You may need to refine the instruction content or experiment with different phrasings to suit the specific model you are using. 68 | ``` 69 | -------------------------------------------------------------------------------- /fan_oscillation_control_full_llm.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Voice - Set Fan Oscillation 3 | author: luuquangvu 4 | description: | 5 | # Tool for Controlling Fan Oscillation used for Voice Assistant 6 | 7 | ## Blueprint Setup 8 | 9 | ### Required 10 | 11 | - A smart fan integrated into Home Assistant. 12 | - A template sensor stores all information about entity aliases needs to be configured in `config/configuration.yaml`; The sensor is required for friendly-name lookup. 13 | 14 | ``` 15 | #File configuration.yaml 16 | shell_command: 17 | get_entity_alias: jq '[.data.entities[] | select(.options.conversation.should_expose == true and (.aliases | length > 0)) | {entity_id, aliases}]' ./.storage/core.entity_registry 18 | template: 19 | - triggers: 20 | - trigger: homeassistant 21 | event: start 22 | - trigger: event 23 | event_type: event_template_reloaded 24 | actions: 25 | - action: shell_command.get_entity_alias 26 | response_variable: response 27 | sensor: 28 | - name: "Assist: Entity IDs and Aliases" 29 | unique_id: entity_ids_and_aliases 30 | icon: mdi:format-list-bulleted 31 | device_class: timestamp 32 | state: "{{ now().isoformat() }}" 33 | attributes: 34 | entities: "{{ response.stdout }}" 35 | ``` 36 | 37 | ### Optional 38 | 39 | - Adjust the prompts for each field used in the script. The descriptions guide the LLM to provide the correct input. 40 | 41 | ### Note 42 | 43 | - Provide a concise and precise description for the script. This will be utilized by the LLM to understand it should use this script for controlling oscillation of a smart fan. 44 | - Make sure to expose the fan entities you want to control to Assist. 45 | - Make sure to expose the script to Assist after the script has been saved. 46 | - Do not alter the default script name. 47 | - Once the script is created, click the three dots in the top right corner, choose "Edit in YAML," and remove the `description: ...` line to restore the default. This step is important because it helps the LLM better understand the script's purpose. 48 | domain: script 49 | homeassistant: 50 | min_version: 2024.10.0 51 | input: 52 | entity_aliases_settings: 53 | name: Settings for Entity Aliases 54 | icon: mdi:format-list-bulleted 55 | description: You can use these settings to configure a template sensor that stores all information about entity aliases. 56 | input: 57 | entity_aliases: 58 | name: Entity Aliases 59 | selector: 60 | entity: 61 | filter: 62 | - domain: sensor 63 | integration: template 64 | prompt_settings: 65 | name: Prompt settings for the LLM 66 | icon: mdi:robot 67 | description: You can use these settings to finetune the prompts for your specific LLM (model). In most cases the defaults should be fine. 68 | collapsed: true 69 | input: 70 | fan_entities_prompt: 71 | name: Fan Entities Prompt 72 | description: The prompt which will be used for the LLM can provide the name of fans for controlling. 73 | selector: 74 | text: 75 | multiline: true 76 | default: | 77 | This argument is mandatory and must always be included. 78 | Specify at least one fan's name to control. 79 | To control multiple fans, separate each fan name with a semicolon. 80 | oscillating_prompt: 81 | name: Oscillating Prompt 82 | description: The prompt which will be used for the LLM can provide the oscillation state. 83 | selector: 84 | text: 85 | multiline: true 86 | default: | 87 | This argument is mandatory and must always be included. 88 | Define the oscillation state for the fan you want to set. 89 | It must be one of the following two values: 'true' turns oscillation on, or 'false' turns oscillation off. 90 | mode: parallel 91 | max_exceeded: silent 92 | description: Controls the oscillation feature of one or more specified fans. 93 | variables: 94 | version: 20251116 95 | fields: 96 | fan_entities: 97 | name: Fan Entities 98 | description: !input fan_entities_prompt 99 | selector: 100 | text: 101 | required: true 102 | oscillating: 103 | name: Oscillating 104 | description: !input oscillating_prompt 105 | selector: 106 | boolean: 107 | required: true 108 | sequence: 109 | - variables: 110 | entity_aliases: !input entity_aliases 111 | fan_entities: "{{ fan_entities | default('') | trim }}" 112 | oscillating: "{{ oscillating | default(false) }}" 113 | - alias: Check if variables were set correctly 114 | if: 115 | - condition: template 116 | value_template: | 117 | {% set validation = namespace(not_exist=false) -%} 118 | {% for entity in fan_entities.split(';') -%} 119 | {% if not ((states.fan | selectattr('attributes.friendly_name', '==', entity.strip()) | list) or 120 | (state_attr(entity_aliases, 'entities') | default([]) | selectattr('entity_id', 'match', 'fan\.') | selectattr('aliases', 'contains', entity.strip()) | list)) -%} 121 | {% set validation.not_exist = true -%} 122 | {% endif -%} 123 | {% endfor -%} 124 | {{ validation.not_exist }} 125 | then: 126 | - alias: Set variable for error message 127 | variables: 128 | response: 129 | error: Unable to control the fan because the fan name is invalid. 130 | - alias: Stop the script 131 | stop: Unable to control the fan because the fan name is invalid. 132 | response_variable: response 133 | - variables: 134 | devices: | 135 | {% set device = namespace(entities=[]) -%} 136 | {% for entity in fan_entities.split(';') -%} 137 | {% if (states.fan | selectattr('attributes.friendly_name', '==', entity.strip()) | list) -%} 138 | {% set device.entities = device.entities + (states.fan | selectattr('attributes.friendly_name', '==', entity.strip()) | map(attribute='entity_id') | list) -%} 139 | {% else -%} 140 | {% set device.entities = device.entities + (state_attr(entity_aliases, 'entities') | default([]) | selectattr('entity_id', 'match', 'fan\.') | selectattr('aliases', 'contains', entity.strip()) | map(attribute='entity_id') | list) -%} 141 | {% endif -%} 142 | {% endfor -%} 143 | {{ device.entities }} 144 | - variables: 145 | friendly_fans: | 146 | {% set names = namespace(values=[]) -%} 147 | {% for name in fan_entities.split(';') -%} 148 | {% if name | trim -%} 149 | {% set names.values = names.values + [name.strip()] -%} 150 | {% endif -%} 151 | {% endfor -%} 152 | {{ names.values }} 153 | - action: fan.oscillate 154 | target: 155 | entity_id: "{{ devices | join(', ') }}" 156 | data: 157 | oscillating: "{{ (oscillating | string) | lower }}" 158 | - alias: Prepare success response for Assist 159 | variables: 160 | response: 161 | success: | 162 | Turned {{ 'on' if oscillating else 'off' }} oscillation for {{ 163 | friendly_fans | join(', ') }}. 164 | - stop: Finish and return response data 165 | response_variable: response 166 | -------------------------------------------------------------------------------- /home_assistant_voice_instructions.md: -------------------------------------------------------------------------------- 1 | # Cách tạo một bản chỉ dẫn hệ thống (System Prompt) cho AI 2 | 3 | - **Bản chỉ dẫn hệ thống này được tối ưu hóa để hoạt động hiệu quả nhất với Gemini 2.5 Flash. Các mô hình (model) khác có thể sẽ cần điều chỉnh thêm để hoạt động chính xác như mong muốn.** 4 | 5 | ## Tóm tắt 6 | 7 | - **Bản chỉ dẫn hệ thống hoàn chỉnh.** 8 | 9 | ```text 10 | **Persona and Tone**: You are a voice assistant. Always respond in the same language as the user's message. Keep replies in one paragraph with normal sentence punctuation for natural flow. Use a friendly, natural, and concise tone that sounds pleasant and clear when read aloud. Current date and time: {{ now().isoformat(timespec='seconds') }}. 11 | **Tool Invocation Rules**: When invoking a tool, output only the tool call in the exact format required by the specification. If the required format includes a fenced block, always include it and do not alter or omit it. A fenced block includes any structured wrapper required by the tool format. Do not add any extra text, commentary, formatting normalization, or explanation. Tool responses are never treated as plain-text responses. 12 | **Plain Text Rules**: If no tool is needed, output the final user-facing answer in plain text only. Do not use Markdown, LaTeX, JSON, code formatting, emojis, mathematical expressions, symbolic notation, or any emphasis markup. Diacritics and characters from the user's language are allowed. 13 | **Follow-up Question Policy**: After each plain-text answer, ask if the user needs anything else, unless you already requested missing information or the user's message clearly ends the conversation. Tool calls do not require or include the follow-up question. Any brief or very short user response that indicates gratitude, acknowledgment, or refusal with no new request must always be treated as the end of the conversation. The follow-up question must be the final sentence, must end with a question mark, and must not be followed by any extra text. 14 | **Tools Usage and Error Policy**: Use the appropriate tool whenever the user's request requires it. If a tool call fails, returns an error, or produces an empty, malformed, or unusable result, it must always be considered failed. Automatically try another relevant tool if possible, asking the user only when essential information is missing. As a general principle, if a tool designed for real-time or sensor-based data returns an empty result, you should try a tool that searches for manually saved notes or static information related to the same query. Never output fake tool calls, code, or reasoning steps. When not invoking a tool, output only the final user-facing answer in plain text. If all tools fail, respond with a short one-line fallback in the user's language. 15 | # END OF SYSTEM MESSAGE 16 | ``` 17 | 18 | ## Chi tiết 19 | 20 | - **Nhân cách và Giọng điệu (Persona and Tone):** Định hình nhân cách trợ lý ảo thân thiện, phản hồi ngắn gọn, tự nhiên bằng ngôn ngữ của người dùng. Cung cấp thời gian thực giúp AI xử lý chính xác các ngữ cảnh về thời gian như "hôm nay" hoặc "ngày mai". 21 | 22 | ```text 23 | **Persona and Tone**: You are a voice assistant. Always respond in the same language as the user's message. Keep replies in one paragraph with normal sentence punctuation for natural flow. Use a friendly, natural, and concise tone that sounds pleasant and clear when read aloud. Current date and time: {{ now().isoformat(timespec='seconds') }}. 24 | ``` 25 | 26 | - **Quy tắc gọi công cụ (Tool Invocation Rules):** Yêu cầu AI xuất lệnh gọi công cụ (tool call) chuẩn xác theo định dạng kỹ thuật, tuyệt đối không kèm văn bản thừa. Điều này đảm bảo Home Assistant phân tích (parse) và thực thi lệnh thành công. 27 | 28 | ```text 29 | **Tool Invocation Rules**: When invoking a tool, output only the tool call in the exact format required by the specification. If the required format includes a fenced block, always include it and do not alter or omit it. A fenced block includes any structured wrapper required by the tool format. Do not add any extra text, commentary, formatting normalization, or explanation. Tool responses are never treated as plain-text responses. 30 | ``` 31 | 32 | - **Quy tắc văn bản thuần túy (Plain Text Rules):** Chỉ cho phép phản hồi bằng văn bản thuần túy, loại bỏ các định dạng đặc biệt (Markdown, Emoji, JSON...) để tránh gây lỗi đọc hoặc phát âm sai cho các công cụ chuyển văn bản thành giọng nói (TTS). 33 | 34 | ```text 35 | **Plain Text Rules**: If no tool is needed, output the final user-facing answer in plain text only. Do not use Markdown, LaTeX, JSON, code formatting, emojis, mathematical expressions, symbolic notation, or any emphasis markup. Diacritics and characters from the user's language are allowed. 36 | ``` 37 | 38 | - **Chính sách câu hỏi tiếp theo (Follow-up Question Policy):** Duy trì mạch hội thoại (Continuous Conversation) bằng cách buộc AI luôn hỏi lại người dùng sau mỗi câu trả lời, trừ khi cuộc trò chuyện đã kết thúc rõ ràng. 39 | 40 | ```text 41 | **Follow-up Question Policy**: After each plain-text answer, ask if the user needs anything else, unless you already requested missing information or the user's message clearly ends the conversation. Tool calls do not require or include the follow-up question. Any brief or very short user response that indicates gratitude, acknowledgment, or refusal with no new request must always be treated as the end of the conversation. The follow-up question must be the final sentence, must end with a question mark, and must not be followed by any extra text. 42 | ``` 43 | 44 | - **Chính sách sử dụng công cụ và xử lý lỗi (Tools Usage and Error Policy):** Chiến lược xử lý lỗi thông minh: ưu tiên tự động tìm kiếm nguồn dữ liệu thay thế (ví dụ: ghi chú thủ công) khi dữ liệu thời gian thực (cảm biến) bị thiếu, giúp AI luôn đưa ra phản hồi hữu ích thay vì báo lỗi hoặc bịa đặt thông tin. 45 | 46 | ```text 47 | **Tools Usage and Error Policy**: Use the appropriate tool whenever the user's request requires it. If a tool call fails, returns an error, or produces an empty, malformed, or unusable result, it must always be considered failed. Automatically try another relevant tool if possible, asking the user only when essential information is missing. As a general principle, if a tool designed for real-time or sensor-based data returns an empty result, you should try a tool that searches for manually saved notes or static information related to the same query. Never output fake tool calls, code, or reasoning steps. When not invoking a tool, output only the final user-facing answer in plain text. If all tools fail, respond with a short one-line fallback in the user's language. 48 | ``` 49 | 50 | - **Đánh dấu kết thúc bản chỉ dẫn:** Mốc đánh dấu kết thúc phần chỉ dẫn tùy chỉnh, giúp ngăn cách rõ ràng với các chỉ dẫn mặc định hoặc ngữ cảnh bổ sung của Home Assistant. 51 | 52 | ```text 53 | # END OF SYSTEM MESSAGE 54 | ``` 55 | 56 | ## Câu hỏi thường gặp (FAQ) 57 | 58 | - **Tại sao bản chỉ dẫn hệ thống lại sử dụng tiếng Anh mà không phải tiếng Việt?** 59 | 60 | ```text 61 | Do dữ liệu huấn luyện cốt lõi của hầu hết các LLM lớn là tiếng Anh, nên chúng hiểu và tuân thủ các chỉ dẫn kỹ thuật bằng tiếng Anh chính xác hơn. Việc viết chỉ dẫn hệ thống bằng tiếng Việt có thể khiến AI (đặc biệt là các model nhỏ) hiểu sai ngữ nghĩa hoặc bỏ qua các yêu cầu ràng buộc phức tạp. 62 | ``` 63 | 64 | - **Tại sao sau khi áp dụng bản chỉ dẫn này mà Voice Assist vẫn gặp lỗi?** 65 | 66 | ```text 67 | Do kiến trúc và dữ liệu huấn luyện của mỗi mô hình là khác nhau, một số mô hình có thể không tuân thủ chặt chẽ chỉ dẫn này. Bạn có thể cần tinh chỉnh lại nội dung chỉ dẫn hoặc thử nghiệm các cách diễn đạt khác nhau cho phù hợp với mô hình cụ thể mà bạn đang sử dụng. 68 | ``` 69 | -------------------------------------------------------------------------------- /calendar_events_lookup_full_llm.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Voice - Get Calendar Events 3 | author: luuquangvu 4 | description: | 5 | # Tool gets events from calendar used for Voice Assistant 6 | 7 | ## Blueprint Setup 8 | 9 | ### Required 10 | 11 | - Set one or more calendar entities for which you want to get the events. 12 | 13 | ### Optional 14 | 15 | - Adjust the prompts for each field used in the script. The descriptions guide the LLM to provide the correct input. 16 | 17 | ### Note 18 | 19 | - Provide a concise and precise description for the script. This script will be utilized by the LLM to understand that it should obtain events from the specified calendars. 20 | - Make sure to expose the script to Assist after the script has been saved. 21 | - Do not alter the default script name. 22 | - Once the script is created, click the three dots in the top right corner, choose "Edit in YAML," and remove the `description: ...` line to restore the default. This step is important because it helps the LLM better understand the script's purpose. 23 | domain: script 24 | homeassistant: 25 | min_version: 2024.10.0 26 | input: 27 | calendar_settings: 28 | name: Settings for Calendar 29 | icon: mdi:calendar 30 | description: You can use these settings to configure Calendar entities. 31 | input: 32 | calendar_entities: 33 | name: Calendar Entities 34 | description: Select the calendar entities to fetch the events from. 35 | selector: 36 | entity: 37 | filter: 38 | domain: calendar 39 | multiple: true 40 | prompt_settings: 41 | name: Prompt settings for the LLM 42 | icon: mdi:robot 43 | description: You can use these settings to finetune the prompts for your specific LLM (model). In most cases the defaults should be fine. 44 | collapsed: true 45 | input: 46 | time_period_type_prompt: 47 | name: Time Period Type Prompt 48 | description: The prompt which will be used for the LLM can provide the type for the period (days or hours). 49 | selector: 50 | text: 51 | multiline: true 52 | default: | 53 | This argument is mandatory and must always be included. 54 | Use 'daily' for requests covering full days or periods longer than one day. Use 'hourly' for requests covering part of a day or a specific start time. 55 | After obtaining the result, ensure the response includes only the event's day of the week and start date. The event's end date or time is exclusive and should not be included. 56 | time_period_length_prompt: 57 | name: Time Period Length Prompt 58 | description: The prompt which will be used for the LLM can provide the length of the period. 59 | selector: 60 | text: 61 | multiline: true 62 | default: | 63 | This argument is mandatory and must always be included. 64 | The length of the period. This will be measured in days if 'time_period_type' is 'daily', and in hours if 'time_period_type' is 'hourly'. 65 | Examples: 1 (day) for today's events, 2 (days) for weekend events, 7 (days) for the next few days, 30 (days) for in the month, 6 (hours) for morning, afternoon, or evening events. 66 | date_prompt: 67 | name: Date Prompt 68 | description: The prompt which will be used for the LLM can provide the start date for the events period. 69 | selector: 70 | text: 71 | multiline: true 72 | default: | 73 | This argument is mandatory and must always be included. 74 | Specify the date for the requested period starts using the format YYYY-MM-DD. 75 | Always include a date with each request. If the request does not specify a date, you should provide today's date. Examples of dates that can be used include today, this week, this month, etc. 76 | When the requested period is for the night, do not use today's date but always use the next day, unless the current time is still before 05:00:00. 77 | time_prompt: 78 | name: Time Prompt 79 | description: The prompt which will be used for the LLM can provide the start time for the events period. 80 | selector: 81 | text: 82 | multiline: true 83 | default: | 84 | This argument is mandatory and must always be included. 85 | Specify the start time using the format HH:MM:SS. 86 | Always provide time. For a full day, use 00:00:00. For the current hour, use the start of the hour. Night starts at 00:00:00, morning starts at 06:00:00, afternoon starts at 12:00:00 and evening starts at 18:00:00. 87 | mode: parallel 88 | max_exceeded: silent 89 | description: Retrieves calendar events based on specified time periods. 90 | variables: 91 | version: 20251116 92 | fields: 93 | time_period_type: 94 | name: Time Period Type 95 | description: !input time_period_type_prompt 96 | selector: 97 | select: 98 | options: 99 | - daily 100 | - hourly 101 | required: true 102 | time_period_length: 103 | name: Time Period Length 104 | description: !input time_period_length_prompt 105 | selector: 106 | number: 107 | min: 1 108 | max: 120 109 | required: true 110 | start_date: 111 | name: Start Date 112 | description: !input date_prompt 113 | selector: 114 | date: 115 | required: true 116 | start_time: 117 | name: Start Time 118 | description: !input time_prompt 119 | selector: 120 | time: 121 | required: true 122 | sequence: 123 | - variables: 124 | start_date: "{{ start_date | as_datetime(default='') }}" 125 | start_time: "{{ start_time | default('00:00:00') | as_timedelta | default('00:00:00', true) }}" 126 | start: | 127 | {{ ((start_date | as_datetime + as_timedelta(start_time)) | as_local) if start_date else 'n/a' }} 128 | end: | 129 | {% if start != 'n/a' %} 130 | {% set start = as_datetime(start) %} 131 | {% set add = time_period_length | default(1) | abs %} 132 | {% set type = time_period_type if (time_period_type | default) in ['daily', 'hourly'] else 'daily' %} 133 | {{ (start + timedelta(days = add if type == 'daily' else 0, hours = add if type == 'hourly' else 0)) | as_local }} 134 | {% endif %} 135 | - alias: Check if variables were set correctly 136 | if: 137 | - condition: template 138 | value_template: "{{ start == 'n/a' or end | as_datetime < now() }}" 139 | then: 140 | - alias: Set variable for eror message 141 | variables: 142 | response: 143 | error: Unable to provide calendar events as the start date is either empty or in the past. 144 | - alias: Stop the script 145 | stop: Unable to provide calendar events as the start date is either empty or in the past. 146 | response_variable: response 147 | - action: calendar.get_events 148 | target: 149 | entity_id: !input calendar_entities 150 | response_variable: response 151 | data: 152 | start_date_time: "{{ start }}" 153 | end_date_time: "{{ end }}" 154 | - variables: 155 | response: 156 | events: | 157 | {{ 158 | response.values() 159 | | map(attribute='events') 160 | | sum(start=[]) 161 | | sort(attribute='start') 162 | }} 163 | - stop: "" 164 | response_variable: response 165 | -------------------------------------------------------------------------------- /file_content_analyzer_full_llm.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Voice - File Content Analyzer 3 | author: luuquangvu 4 | description: | 5 | # Tool designed to analyze and extract all types of information from media and document files 6 | 7 | ## Blueprint Setup 8 | 9 | ### Required 10 | 11 | - An AI Task entity must be created and configured in the System - General settings. 12 | 13 | ### Optional 14 | 15 | - Adjust the prompts for each field used in the script. The descriptions guide the LLM to provide the correct input. 16 | 17 | ### Note 18 | 19 | - Provide a concise and precise description for the script. This description will enable the LLM to recognize that the script is designed to extract data from a media or document file. 20 | - Make sure to expose the script to Assist after the script has been saved. 21 | - Do not alter the default script name. 22 | - Once the script is created, click the three dots in the top right corner, choose "Edit in YAML," and remove the `description: ...` line to restore the default. This step is important because it helps the LLM better understand the script's purpose. 23 | domain: script 24 | homeassistant: 25 | min_version: 2025.8.0 26 | input: 27 | ai_task_settings: 28 | name: Settings for AI Task 29 | icon: mdi:robot-outline 30 | description: These settings allow you to set up the AI Task responsible for handling the analyzer task. 31 | collapsed: true 32 | input: 33 | ai_task_entity: 34 | name: AI Task Entity 35 | description: If left empty, the system will use the default settings under System - General. 36 | selector: 37 | entity: 38 | filter: 39 | domain: ai_task 40 | default: 41 | prompt_settings: 42 | name: Prompt settings for the LLM 43 | icon: mdi:robot 44 | description: You can use these settings to finetune the prompts for your specific LLM (model). In most cases the defaults should be fine. 45 | collapsed: true 46 | input: 47 | instructions_prompt: 48 | name: Instructions Prompt 49 | description: The prompt which will be used for the LLM can provide the request for the query. 50 | selector: 51 | text: 52 | multiline: true 53 | default: | 54 | This argument is mandatory and must always be included. 55 | The tool can analyze and extract any type of information from an image, video, audio, or document, including text, numbers, objects, speech, and visual features. 56 | Always specify exactly what data should be extracted. 57 | This tool returns plain text only. 58 | file_path_prompt: 59 | name: File Path Prompt 60 | description: The prompt which will be used for the LLM can provide the file path for the query. 61 | selector: 62 | text: 63 | multiline: true 64 | default: | 65 | This argument is mandatory and must always be included. 66 | Always specify a relative file path that starts with `local/`. 67 | mime_type_prompt: 68 | name: MIME Type Prompt 69 | description: The prompt which will be used for the LLM can provide the MIME type of file for the query. 70 | selector: 71 | text: 72 | multiline: true 73 | default: | 74 | This argument is mandatory and must always be included. 75 | Always specify the MIME type of the file. 76 | Choose a value that matches the file contents (for example image/jpeg, video/mp4, audio/mp3, text/plain, or application/pdf). 77 | mode: parallel 78 | max_exceeded: silent 79 | description: Analyzes the content of a specified file (image, video, audio, or document) based on provided instructions. Capable of visual recognition (e.g. camera snapshots), text summarization, and audio transcription. 80 | variables: 81 | version: 20251209 82 | fields: 83 | instructions: 84 | name: Instructions 85 | description: !input instructions_prompt 86 | selector: 87 | text: 88 | multiline: true 89 | required: true 90 | file_path: 91 | name: Media Path 92 | description: !input file_path_prompt 93 | selector: 94 | text: 95 | required: true 96 | mime_type: 97 | name: MIME Type 98 | description: !input mime_type_prompt 99 | selector: 100 | text: 101 | required: true 102 | sequence: 103 | - variables: 104 | ai_task_entity: !input ai_task_entity 105 | instructions: "{{ instructions | default('') | trim }}" 106 | file_path: "{{ file_path | default('') | trim }}" 107 | mime_type: "{{ mime_type | default('') | trim }}" 108 | - if: 109 | - alias: Check if variables were set correctly 110 | condition: template 111 | value_template: "{{ not instructions }}" 112 | then: 113 | - alias: Set variable for error message 114 | variables: 115 | response: 116 | error: | 117 | Unable to extract data because instructions is missing or incorrect. 118 | - alias: Stop the script 119 | stop: | 120 | Unable to extract data because instructions is missing or incorrect. 121 | response_variable: response 122 | - if: 123 | - alias: Check if variables were set correctly 124 | condition: template 125 | value_template: "{{ (file_path | length == 0) or (not file_path.startswith('local/')) }}" 126 | then: 127 | - alias: Set variable for error message 128 | variables: 129 | response: 130 | error: | 131 | Unable to extract data because file path is missing or invalid. Specify a path beginning with `local/`, such as `local/folder/file.ext`. 132 | - alias: Stop the script 133 | stop: | 134 | Unable to extract data because file path is missing or invalid. 135 | response_variable: response 136 | - if: 137 | - alias: Check if variables were set correctly 138 | condition: template 139 | value_template: "{{ not mime_type.startswith(('image/', 'video/', 'audio/', 'text/', 'application/')) }}" 140 | then: 141 | - alias: Set variable for error message 142 | variables: 143 | response: 144 | error: | 145 | Unable to extract data because MIME type is missing or incorrect. 146 | - alias: Stop the script 147 | stop: | 148 | Unable to extract data because MIME type is missing or incorrect. 149 | response_variable: response 150 | - variables: 151 | attachments: 152 | media_content_id: media-source://media_source/{{ file_path }} 153 | media_content_type: "{{ mime_type }}" 154 | - if: 155 | - condition: template 156 | value_template: "{{ not ai_task_entity }}" 157 | then: 158 | - action: ai_task.generate_data 159 | data: 160 | task_name: Analyze content from a media or document file 161 | instructions: "{{ instructions }}" 162 | attachments: "{{ attachments }}" 163 | response_variable: result 164 | else: 165 | - action: ai_task.generate_data 166 | data: 167 | task_name: Analyze content from a media or document file 168 | instructions: "{{ instructions }}" 169 | attachments: "{{ attachments }}" 170 | entity_id: "{{ ai_task_entity }}" 171 | response_variable: result 172 | - variables: 173 | response: 174 | data: "{{ result.data }}" 175 | - stop: "" 176 | response_variable: response 177 | -------------------------------------------------------------------------------- /scripts/zalo_custom_bot_handle_tool.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import mimetypes 3 | import os 4 | import secrets 5 | import time 6 | from pathlib import Path 7 | from typing import Any 8 | from urllib.parse import urlparse 9 | 10 | import aiofiles 11 | import aiohttp 12 | from homeassistant.helpers import network 13 | 14 | DIRECTORY = "/media/zalo" 15 | 16 | _session: aiohttp.ClientSession | None = None 17 | 18 | 19 | def _to_relative_path(path: str) -> str: 20 | """Convert a /media/ path to Home Assistant local/ media source path. 21 | 22 | Converts leading "/media/" to "local/". Leaves other paths unchanged. 23 | 24 | Args: 25 | path: Absolute media path. 26 | 27 | Returns: 28 | Path with leading "local/" when applicable. 29 | """ 30 | if path.startswith("/media/"): 31 | return "local/" + path.removeprefix("/media/") 32 | return path 33 | 34 | 35 | def _internal_url() -> str | None: 36 | """Return the internal Home Assistant URL, or None when it cannot be resolved.""" 37 | try: 38 | return network.get_url(hass, allow_external=False) 39 | except network.NoURLAvailableError: 40 | return None 41 | 42 | 43 | def _external_url() -> str | None: 44 | """Return the external HTTPS Home Assistant URL when configured and reachable.""" 45 | try: 46 | return network.get_url( 47 | hass, 48 | allow_internal=False, 49 | allow_ip=False, 50 | require_ssl=True, 51 | require_standard_port=True, 52 | ) 53 | except network.NoURLAvailableError: 54 | return None 55 | 56 | 57 | async def _ensure_session() -> aiohttp.ClientSession: 58 | """Create or reuse a shared aiohttp session. 59 | 60 | Returns: 61 | An open `aiohttp.ClientSession` with a default timeout. 62 | """ 63 | global _session 64 | if _session is None or _session.closed: 65 | _session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=300)) 66 | return _session 67 | 68 | 69 | async def _ensure_dir(path: str) -> None: 70 | """Ensure a directory exists, creating it if missing. 71 | 72 | Args: 73 | path: Directory path to create if it does not exist. 74 | """ 75 | await asyncio.to_thread(os.makedirs, path, exist_ok=True) 76 | 77 | 78 | async def _cleanup_old_files(directory: str, days: int = 30) -> None: 79 | """Delete files in the directory older than the specified number of days.""" 80 | now = time.time() 81 | cutoff = now - (days * 86400) 82 | 83 | def _cleanup_sync() -> None: 84 | if not os.path.exists(directory): 85 | return 86 | for filename in os.listdir(directory): 87 | file_path = os.path.join(directory, filename) 88 | try: 89 | if os.path.isfile(file_path): 90 | t = os.path.getmtime(file_path) 91 | if t < cutoff: 92 | os.remove(file_path) 93 | except Exception: 94 | pass 95 | 96 | await asyncio.to_thread(_cleanup_sync) 97 | 98 | 99 | async def _download_file(session: aiohttp.ClientSession, url: str) -> str | None: 100 | """Download a file from a given URL and save it under DIRECTORY (streaming). 101 | 102 | Args: 103 | session: Shared aiohttp session. 104 | url: Direct URL to the file to download. 105 | 106 | Returns: 107 | Full file path of the saved file, or None on failure. 108 | """ 109 | try: 110 | async with session.get(url) as resp: 111 | resp.raise_for_status() 112 | content_type = resp.headers.get("Content-Type", "") 113 | ext = mimetypes.guess_extension(content_type.split(";")[0].strip()) or "" 114 | 115 | # Use safe filename from URL or default, then append unique token 116 | parsed_url = urlparse(url) 117 | original_name = Path(parsed_url.path).name 118 | if not Path(original_name).suffix and ext: 119 | original_name += ext 120 | 121 | base, extension = os.path.splitext(original_name) 122 | # Construct unique filename: name_timestamp_random.ext 123 | file_name = f"{base}_{int(time.time())}_{secrets.token_hex(4)}{extension}" 124 | 125 | file_path = os.path.join(DIRECTORY, file_name) 126 | 127 | async with aiofiles.open(file_path, "wb") as f: 128 | async for chunk in resp.content.iter_chunked(4096): 129 | await f.write(chunk) 130 | 131 | return file_path 132 | except Exception: 133 | return None 134 | 135 | 136 | @time_trigger("shutdown") 137 | async def _close_session() -> None: 138 | """Close the aiohttp session on shutdown.""" 139 | global _session 140 | if _session and not _session.closed: 141 | await _session.close() 142 | _session = None 143 | 144 | 145 | @time_trigger("cron(0 0 * * *)") 146 | async def _daily_cleanup() -> None: 147 | """Run daily cleanup of old files.""" 148 | await _cleanup_old_files(DIRECTORY, days=30) 149 | 150 | 151 | @service(supports_response="only") 152 | async def get_zalo_file_custom_bot(url: str) -> dict[str, Any]: 153 | """ 154 | yaml 155 | name: Get Zalo File (Custom Bot) 156 | description: Download a file by direct URL and save it under Home Assistant media; returns a local path and file type. 157 | fields: 158 | url: 159 | name: URL 160 | description: Direct file URL (e.g., from a Zalo attachment). 161 | required: true 162 | selector: 163 | text: 164 | """ 165 | if not url: 166 | return {"error": "Missing a required argument: url"} 167 | try: 168 | session = await _ensure_session() 169 | await _ensure_dir(DIRECTORY) 170 | 171 | file_path = await _download_file(session, url) 172 | if not file_path: 173 | return {"error": "Unable to download the file from Zalo."} 174 | 175 | mimetypes.add_type("text/plain", ".yaml") 176 | mime_type, _ = mimetypes.guess_file_type(file_path) 177 | file_path = _to_relative_path(file_path) 178 | response: dict[str, Any] = {"file_path": file_path, "mime_type": mime_type} 179 | support_file_types = ( 180 | "image/", 181 | "video/", 182 | "audio/", 183 | "text/", 184 | "application/pdf", 185 | ) 186 | if mime_type and mime_type.startswith(support_file_types): 187 | response["supported"] = True 188 | else: 189 | response["supported"] = False 190 | return response 191 | except Exception as error: 192 | return {"error": f"An unexpected error occurred during processing: {error}"} 193 | 194 | 195 | @service(supports_response="only") 196 | async def generate_webhook_id() -> dict[str, Any]: 197 | """ 198 | yaml 199 | name: Generate Webhook ID 200 | description: Generate a unique URL-safe webhook ID and sample URLs. 201 | """ 202 | webhook_id = secrets.token_urlsafe() 203 | internal_url = _internal_url() 204 | external_url = _external_url() 205 | response = {"webhook_id": webhook_id} 206 | if internal_url: 207 | response["sample_internal_url"] = f"{internal_url}/api/webhook/{webhook_id}" 208 | else: 209 | response["sample_internal_url"] = ( 210 | "The internal Home Assistant URL is not found." 211 | ) 212 | if external_url: 213 | response["sample_external_url"] = f"{external_url}/api/webhook/{webhook_id}" 214 | else: 215 | response["sample_external_url"] = ( 216 | "The external Home Assistant URL is not found or incorrect." 217 | ) 218 | return response 219 | -------------------------------------------------------------------------------- /home_assistant_unavailable_devices_en.md: -------------------------------------------------------------------------------- 1 | # Monitoring & Notifying Unavailable Devices 2 | 3 | This guide helps you automatically monitor and receive notifications whenever any device in Home Assistant becomes "Unavailable" or "Unknown". 4 | 5 | ## Step 1: Create a Monitoring Sensor 6 | 7 | We will create a smart `binary_sensor` that automatically scans your entire system for faulty devices, while allowing you to exclude (ignore) unimportant ones. 8 | 9 | ### 1.1. Create a Label for Management 10 | 11 | To avoid false alarms from devices you don't care about, create a label to mark them. 12 | 13 | 1. Go to **Settings** > **Devices & Services** > **Labels**. 14 | 2. Create a new label named: `ignored` 15 | 16 | ![image](images/20250426_GwdtEl.png) 17 | 18 | ### 1.2. Assign the Label to Ignored Devices 19 | 20 | Assign the `ignored` label to any device or entity you **do not** want to be notified about when it goes offline. 21 | 22 | ![image](images/20250426_oj1S9U.png) 23 | 24 | ### 1.3. Configure Template Sensor 25 | 26 | Add the following code to your `configuration.yaml` file. This sensor automatically filters out devices with the `ignored` label, as well as button or scene entities which naturally don't have a connection state. 27 | 28 | ```yaml 29 | template: 30 | - binary_sensor: 31 | - name: Unavailable Devices 32 | unique_id: unavailable_devices 33 | device_class: problem 34 | icon: >- 35 | {{ iif((this.attributes.raw | default([], true) | count > 0), 'mdi:alert-circle', 'mdi:check-circle') }} 36 | state: >- 37 | {{ this.attributes.raw | default([], true) | count > 0 }} 38 | attributes: 39 | devices: >- 40 | {{ this.attributes.raw | default([], true) | map('device_id') | reject('none') | unique | map('device_attr', 'name') | list }} 41 | entities: >- 42 | {{ this.attributes.raw | default([], true) }} 43 | raw: >- 44 | {% set ignored_label = 'ignored' -%} 45 | {% set ignored_domains = ['button', 'input_button', 'scene'] -%} 46 | {% set ignored_integrations = ['demo', 'private_ble_device'] -%} 47 | 48 | {% set ignored_integration_entities = namespace(entities=[]) -%} 49 | {% for integration in ignored_integrations -%} 50 | {% set ignored_integration_entities.entities = ignored_integration_entities.entities + integration_entities(integration) -%} 51 | {% endfor -%} 52 | 53 | {% set ignored_devices = label_devices(ignored_label) -%} 54 | {% set ignored_device_entities = namespace(entities=[]) -%} 55 | {% for device in ignored_devices -%} 56 | {% set ignored_device_entities.entities = ignored_device_entities.entities + device_entities(device) -%} 57 | {% endfor -%} 58 | 59 | {% set ignored_individual_entities = label_entities(ignored_label) -%} 60 | 61 | {{ states 62 | | selectattr('state', 'in', ['unavailable', 'unknown']) 63 | | rejectattr('domain', 'in', ignored_domains) 64 | | rejectattr('entity_id', 'in', ignored_integration_entities.entities) 65 | | rejectattr('entity_id', 'in', ignored_device_entities.entities) 66 | | rejectattr('entity_id', 'in', ignored_individual_entities) 67 | | map(attribute='entity_id') 68 | | list }} 69 | ``` 70 | 71 | _After saving the file, please **Restart** Home Assistant to apply the changes._ 72 | 73 | ## Step 2: Create Automation Notifications 74 | 75 | The automations below will send a notification when an issue occurs, and automatically clear the notification when the issue is resolved. 76 | 77 | ### Option 1: Persistent Notification (on Home Assistant Dashboard) 78 | 79 | ```yaml 80 | alias: "System: Notify Unavailable Devices (Persistent)" 81 | description: "" 82 | triggers: 83 | - trigger: state 84 | entity_id: 85 | - binary_sensor.unavailable_devices 86 | attribute: entities 87 | conditions: 88 | - condition: template 89 | value_template: "{{ trigger.from_state.state not in ['unavailable', 'unknown'] }}" 90 | - condition: template 91 | value_template: "{{ trigger.to_state.state not in ['unavailable', 'unknown'] }}" 92 | actions: 93 | - variables: 94 | entities: "{{ state_attr(trigger.entity_id, 'entities') | default([], true) }}" 95 | devices: "{{ state_attr(trigger.entity_id, 'devices') | default([], true) }}" 96 | notify_tag: "{{ 'tag_' ~ this.attributes.id }}" 97 | - if: 98 | - condition: template 99 | value_template: "{{ entities | count > 0 }}" 100 | then: 101 | - action: persistent_notification.create 102 | data: 103 | notification_id: "{{ notify_tag }}" 104 | title: "⚠️ Devices Unavailable" 105 | message: > 106 | ### {{ devices | count }} devices ({{ entities | count }} entities) are having issues. 107 | 108 | **Devices:** 109 | {% for device in devices %}- {{ device }} 110 | {% endfor %} 111 | 112 | **Entity Details:** 113 | {% for entity in entities %}- {{ entity }} 114 | {% endfor %} 115 | else: 116 | - action: persistent_notification.dismiss 117 | data: 118 | notification_id: "{{ notify_tag }}" 119 | mode: queued 120 | max: 10 121 | ``` 122 | 123 | ### Option 2: Mobile App Notification 124 | 125 | ```yaml 126 | alias: "System: Notify Unavailable Devices (Mobile)" 127 | description: "" 128 | triggers: 129 | - trigger: state 130 | entity_id: 131 | - binary_sensor.unavailable_devices 132 | attribute: entities 133 | conditions: 134 | - condition: template 135 | value_template: "{{ trigger.from_state.state not in ['unavailable', 'unknown'] }}" 136 | - condition: template 137 | value_template: "{{ trigger.to_state.state not in ['unavailable', 'unknown'] }}" 138 | actions: 139 | - variables: 140 | entities: "{{ state_attr(trigger.entity_id, 'entities') | default([], true) }}" 141 | devices: "{{ state_attr(trigger.entity_id, 'devices') | default([], true) }}" 142 | notify_tag: "{{ 'tag_' ~ this.attributes.id }}" 143 | - if: 144 | - condition: template 145 | value_template: "{{ entities | count > 0 }}" 146 | then: 147 | - action: notify.mobile_app_iphone # Replace with your phone's notification service 148 | data: 149 | title: "⚠️ Devices Lost Connection" 150 | message: >- 151 | {{ devices | count }} devices ({{ entities | count }} entities) are currently unavailable. 152 | data: 153 | tag: "{{ notify_tag }}" 154 | url: /lovelace/system # (Optional) Path to your system dashboard 155 | else: 156 | - action: notify.mobile_app_iphone # Replace with your phone's notification service 157 | data: 158 | message: clear_notification 159 | data: 160 | tag: "{{ notify_tag }}" 161 | mode: queued 162 | max: 10 163 | ``` 164 | 165 | ## Step 3: Dashboard Display (Optional) 166 | 167 | You can add this Markdown card to your dashboard. It will automatically hide when the system is healthy and only appear when there are faulty devices. 168 | 169 | ```yaml 170 | type: markdown 171 | title: ⚠️ Unavailable Devices 172 | content: > 173 | {% set entities = state_attr('binary_sensor.unavailable_devices', 'entities') | default([], true) -%} 174 | {% set devices = state_attr('binary_sensor.unavailable_devices', 'devices') | default([], true) -%} 175 | 176 | **Overview:** {{ devices | count }} devices - {{ entities | count }} entities. 177 | 178 | --- 179 | **Device List:** 180 | {% for device in devices %}- **{{ device }}** 181 | {% endfor %} 182 | 183 | **Entity Details:** 184 | {% for entity in entities %}- `{{ entity }}` 185 | {% endfor %} 186 | visibility: 187 | - condition: state 188 | entity: binary_sensor.unavailable_devices 189 | state: "on" 190 | ``` 191 | -------------------------------------------------------------------------------- /home_assistant_unavailable_devices.md: -------------------------------------------------------------------------------- 1 | # Giám sát & Thông báo Thiết bị Mất Kết nối (Unavailable Devices) 2 | 3 | Hướng dẫn này giúp bạn tự động theo dõi và nhận thông báo khi có bất kỳ thiết bị nào trong Home Assistant chuyển sang trạng thái "Không khả dụng" (Unavailable) hoặc "Không rõ" (Unknown). 4 | 5 | ## Bước 1: Tạo cảm biến theo dõi (Sensor) 6 | 7 | Chúng ta sẽ tạo một `binary_sensor` thông minh, tự động quét toàn bộ hệ thống để tìm các thiết bị lỗi, đồng thời cho phép bạn loại trừ (bỏ qua) các thiết bị không quan trọng. 8 | 9 | ### 1.1. Tạo nhãn (Label) để quản lý 10 | 11 | Để tránh báo động giả từ các thiết bị bạn không quan tâm, hãy tạo một nhãn để đánh dấu chúng. 12 | 13 | 1. Vào **Settings** > **Devices & Services** > **Labels**. 14 | 2. Tạo nhãn mới tên là: `ignored` 15 | 16 | ![image](images/20250426_GwdtEl.png) 17 | 18 | ### 1.2. Gán nhãn cho thiết bị cần bỏ qua 19 | 20 | Gán nhãn `ignored` cho bất kỳ thiết bị hoặc thực thể nào bạn **không** muốn nhận thông báo khi nó mất kết nối. 21 | 22 | ![image](images/20250426_oj1S9U.png) 23 | 24 | ### 1.3. Cấu hình Template Sensor 25 | 26 | Thêm đoạn mã sau vào file `configuration.yaml` của bạn. Sensor này sẽ tự động lọc bỏ các thiết bị có nhãn `ignored`, cũng như các thực thể nút bấm (button) hoặc ngữ cảnh (scene) vốn không có trạng thái kết nối. 27 | 28 | ```yaml 29 | template: 30 | - binary_sensor: 31 | - name: Unavailable Devices 32 | unique_id: unavailable_devices 33 | device_class: problem 34 | icon: >- 35 | {{ iif((this.attributes.raw | default([], true) | count > 0), 'mdi:alert-circle', 'mdi:check-circle') }} 36 | state: >- 37 | {{ this.attributes.raw | default([], true) | count > 0 }} 38 | attributes: 39 | devices: >- 40 | {{ this.attributes.raw | default([], true) | map('device_id') | reject('none') | unique | map('device_attr', 'name') | list }} 41 | entities: >- 42 | {{ this.attributes.raw | default([], true) }} 43 | raw: >- 44 | {% set ignored_label = 'ignored' -%} 45 | {% set ignored_domains = ['button', 'input_button', 'scene'] -%} 46 | {% set ignored_integrations = ['demo', 'private_ble_device'] -%} 47 | 48 | {% set ignored_integration_entities = namespace(entities=[]) -%} 49 | {% for integration in ignored_integrations -%} 50 | {% set ignored_integration_entities.entities = ignored_integration_entities.entities + integration_entities(integration) -%} 51 | {% endfor -%} 52 | 53 | {% set ignored_devices = label_devices(ignored_label) -%} 54 | {% set ignored_device_entities = namespace(entities=[]) -%} 55 | {% for device in ignored_devices -%} 56 | {% set ignored_device_entities.entities = ignored_device_entities.entities + device_entities(device) -%} 57 | {% endfor -%} 58 | 59 | {% set ignored_individual_entities = label_entities(ignored_label) -%} 60 | 61 | {{ states 62 | | selectattr('state', 'in', ['unavailable', 'unknown']) 63 | | rejectattr('domain', 'in', ignored_domains) 64 | | rejectattr('entity_id', 'in', ignored_integration_entities.entities) 65 | | rejectattr('entity_id', 'in', ignored_device_entities.entities) 66 | | rejectattr('entity_id', 'in', ignored_individual_entities) 67 | | map(attribute='entity_id') 68 | | list }} 69 | ``` 70 | 71 | _Sau khi lưu file, hãy **Khởi động lại (Restart)** Home Assistant để áp dụng._ 72 | 73 | ## Bước 2: Tạo Thông báo Tự động (Automation) 74 | 75 | Các automation dưới đây sẽ gửi thông báo khi có sự cố, và tự động xóa thông báo khi sự cố được khắc phục. 76 | 77 | ### Tùy chọn 1: Thông báo Persistent (trên giao diện Home Assistant) 78 | 79 | ```yaml 80 | alias: "System: Thông báo thiết bị mất kết nối (Persistent)" 81 | description: "" 82 | triggers: 83 | - trigger: state 84 | entity_id: 85 | - binary_sensor.unavailable_devices 86 | attribute: entities 87 | conditions: 88 | - condition: template 89 | value_template: "{{ trigger.from_state.state not in ['unavailable', 'unknown'] }}" 90 | - condition: template 91 | value_template: "{{ trigger.to_state.state not in ['unavailable', 'unknown'] }}" 92 | actions: 93 | - variables: 94 | entities: "{{ state_attr(trigger.entity_id, 'entities') | default([], true) }}" 95 | devices: "{{ state_attr(trigger.entity_id, 'devices') | default([], true) }}" 96 | notify_tag: "{{ 'tag_' ~ this.attributes.id }}" 97 | - if: 98 | - condition: template 99 | value_template: "{{ entities | count > 0 }}" 100 | then: 101 | - action: persistent_notification.create 102 | data: 103 | notification_id: "{{ notify_tag }}" 104 | title: "⚠️ Thiết bị mất kết nối" 105 | message: > 106 | ### Có {{ devices | count }} thiết bị ({{ entities | count }} thực thể) gặp sự cố. 107 | 108 | **Thiết bị:** 109 | {% for device in devices %}- {{ device }} 110 | {% endfor %} 111 | 112 | **Chi tiết thực thể:** 113 | {% for entity in entities %}- {{ entity }} 114 | {% endfor %} 115 | else: 116 | - action: persistent_notification.dismiss 117 | data: 118 | notification_id: "{{ notify_tag }}" 119 | mode: queued 120 | max: 10 121 | ``` 122 | 123 | ### Tùy chọn 2: Thông báo qua Điện thoại (Mobile App) 124 | 125 | ```yaml 126 | alias: "System: Thông báo thiết bị mất kết nối (Mobile)" 127 | description: "" 128 | triggers: 129 | - trigger: state 130 | entity_id: 131 | - binary_sensor.unavailable_devices 132 | attribute: entities 133 | conditions: 134 | - condition: template 135 | value_template: "{{ trigger.from_state.state not in ['unavailable', 'unknown'] }}" 136 | - condition: template 137 | value_template: "{{ trigger.to_state.state not in ['unavailable', 'unknown'] }}" 138 | actions: 139 | - variables: 140 | entities: "{{ state_attr(trigger.entity_id, 'entities') | default([], true) }}" 141 | devices: "{{ state_attr(trigger.entity_id, 'devices') | default([], true) }}" 142 | notify_tag: "{{ 'tag_' ~ this.attributes.id }}" 143 | - if: 144 | - condition: template 145 | value_template: "{{ entities | count > 0 }}" 146 | then: 147 | - action: notify.mobile_app_iphone # Thay bằng tên điện thoại của bạn 148 | data: 149 | title: "⚠️ Mất kết nối thiết bị" 150 | message: >- 151 | Có {{ devices | count }} thiết bị ({{ entities | count }} thực thể) đang bị mất kết nối. 152 | data: 153 | tag: "{{ notify_tag }}" 154 | url: /lovelace/system # (Tùy chọn) Đường dẫn đến dashboard hệ thống của bạn 155 | else: 156 | - action: notify.mobile_app_iphone # Thay bằng tên điện thoại của bạn 157 | data: 158 | message: clear_notification 159 | data: 160 | tag: "{{ notify_tag }}" 161 | mode: queued 162 | max: 10 163 | ``` 164 | 165 | ## Bước 3: Hiển thị trên Dashboard (Tùy chọn) 166 | 167 | Bạn có thể thêm thẻ Markdown này vào giao diện. Nó sẽ tự động ẩn đi khi hệ thống bình thường và chỉ hiện lên khi có thiết bị lỗi. 168 | 169 | ```yaml 170 | type: markdown 171 | title: ⚠️ Thiết bị Mất Kết nối 172 | content: > 173 | {% set entities = state_attr('binary_sensor.unavailable_devices', 'entities') | default([], true) -%} 174 | {% set devices = state_attr('binary_sensor.unavailable_devices', 'devices') | default([], true) -%} 175 | 176 | **Tổng quan:** {{ devices | count }} thiết bị - {{ entities | count }} thực thể. 177 | 178 | --- 179 | **Danh sách thiết bị:** 180 | {% for device in devices %}- **{{ device }}** 181 | {% endfor %} 182 | 183 | **Chi tiết thực thể:** 184 | {% for entity in entities %}- `{{ entity }}` 185 | {% endfor %} 186 | visibility: 187 | - condition: state 188 | entity: binary_sensor.unavailable_devices 189 | state: "on" 190 | ``` 191 | -------------------------------------------------------------------------------- /home_assistant_play_favorite_youtube_channel_videos.md: -------------------------------------------------------------------------------- 1 | # Hướng dẫn chi tiết cài đặt Voice Assist phát video Youtube lên Smart TV 2 | 3 | Hướng dẫn này cho phép bạn sử dụng Home Assistant Voice để phát các video mới nhất từ các kênh YouTube yêu thích lên Smart TV của mình. 4 | 5 | ## Giới thiệu & Tính năng chính 6 | 7 | - **Mục đích:** Tự động phát video mới ra mắt gần đây từ một kênh YouTube bất kỳ mà bạn yêu thích. 8 | - **Hỗ trợ LLM:** Chỉ hoạt động với các LLM như Google hoặc OpenAI. 9 | - **Hỗ trợ Alias:** Bạn có thể tạo nhiều biệt danh (alias) cho tên kênh để dễ gọi tên hơn. 10 | 11 | ### Hạn chế 12 | 13 | - Không hỗ trợ tìm kiếm các video cũ từ một kênh. 14 | - Không hỗ trợ tìm kiếm một video bất kỳ trong toàn bộ YouTube (chỉ tìm video mới nhất của kênh đã theo dõi). 15 | - Yêu cầu cần có một Smart TV hoặc thiết bị media player đã tích hợp vào Home Assistant. 16 | 17 | ![image](images/20250528_210348.jpg) 18 | 19 | ## Bước 1: Lấy thông tin video từ các kênh YouTube yêu thích 20 | 21 | ### 1.1. Cài đặt tích hợp Feedparser 22 | 23 | Feedparser là một integration giúp Home Assistant đọc các nguồn cấp dữ liệu RSS/Atom, bao gồm cả feed video của YouTube. 24 | 25 | [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=custom-components&repository=feedparser&category=Integration) 26 | 27 | - Xem chi tiết tại: [github.com/custom-components/feedparser](https://github.com/custom-components/feedparser) 28 | - _Lưu ý:_ Tính đến thời điểm `2025-12-10`, tích hợp Feedparser vẫn chưa hỗ trợ `unique_id` một cách đầy đủ để tạo alias dễ dàng qua giao diện. Bạn có thể theo dõi tiến độ tại [pull request này](https://github.com/custom-components/feedparser/pull/143) hoặc tự sửa mã nếu muốn có tính năng này. 29 | - Sau khi cài đặt xong qua HACS, cần **khởi động lại** Home Assistant. 30 | 31 | ### 1.2. Lấy ID kênh YouTube 32 | 33 | Bạn cần ID của kênh YouTube để cấu hình sensor. 34 | 35 | 1. Mở Google và tìm kiếm `Get YouTube Channel ID`. 36 | 2. Truy cập một trang bất kỳ (ví dụ: `https://commentpicker.com/youtube-channel-id.php`). 37 | 3. Nhập đường dẫn (URL) của kênh YouTube bạn muốn theo dõi để lấy ID. 38 | 39 | ![image](images/20250527_FdZbGj.png) 40 | 41 | ### 1.3. Cấu hình Sensor cho kênh YouTube 42 | 43 | Sau khi có ID kênh, thêm cấu hình sensor vào file `config/configuration.yaml`: 44 | 45 | ```yaml 46 | sensor: 47 | - platform: feedparser 48 | name: CHANNEL_NAME YouTube Channel # Đổi CHANNEL_NAME thành tên kênh mong muốn 49 | feed_url: https://www.youtube.com/feeds/videos.xml?channel_id=XXXXXX # Thay XXXXXX bằng ID kênh vừa lấy 50 | scan_interval: 51 | minutes: 30 # Tần suất kiểm tra video mới (mặc định 30 phút) 52 | inclusions: 53 | - title 54 | - link 55 | - author 56 | - published 57 | - media_thumbnail 58 | - yt_videoid 59 | date_format: "%Y-%m-%dT%H:%M:%S%z" 60 | ``` 61 | 62 | - **Lưu ý:** Cụm từ "YouTube Channel" trong tên sensor là cố định và quan trọng để LLM nhận diện. 63 | - _Ví dụ cấu hình cho kênh Hoa Ban Food:_ 64 | 65 | ```yaml 66 | sensor: 67 | - platform: feedparser 68 | name: Hoa Ban Food YouTube Channel 69 | feed_url: https://www.youtube.com/feeds/videos.xml?channel_id=UCBhgBmuPFbLLxnejr09lnAQ 70 | scan_interval: 71 | minutes: 30 72 | inclusions: 73 | - title 74 | - link 75 | - author 76 | - published 77 | - media_thumbnail 78 | - yt_videoid 79 | date_format: "%Y-%m-%dT%H:%M:%S%z" 80 | ``` 81 | 82 | - Lặp lại các bước trên cho tất cả các kênh YouTube bạn muốn theo dõi. 83 | - Sau khi cấu hình xong, **khởi động lại** Home Assistant. 84 | 85 | ### 1.4. Chia sẻ Sensor với Assist và tạo Alias 86 | 87 | Để Voice Assist có thể nhận diện và tương tác với các kênh YouTube của bạn: 88 | 89 | 1. Sau khi khởi động lại HA, vào **Settings** > **Voice assistants** > **Expose**. 90 | 2. Tìm và expose các sensor kênh YouTube mới tạo. 91 | 92 | ![image](images/20250527_gCfAcK.png) 93 | 94 | 3. Tạo thêm các **Alias** cho các kênh (ví dụ: "Hoa Ban", "Sơn Tùng") để dễ nhớ hoặc dễ phát âm bằng giọng nói, đặc biệt là với kênh nước ngoài. 95 | 96 | ![image](images/20250604_VhChze.png) 97 | 98 | ### 1.5. Cấu hình hỗ trợ Alias cho Assist 99 | 100 | Để Assist hiểu được các Alias bạn đã tạo, chúng ta cần một `shell_command` và một `template sensor` chung. 101 | 102 | **Thêm vào `configuration.yaml`:** 103 | (Đảm bảo `jq` đã được cài đặt trên hệ thống Home Assistant của bạn) 104 | 105 | ```yaml 106 | shell_command: 107 | get_entity_alias: jq '[.data.entities[] | select(.options.conversation.should_expose == true and (.aliases | length > 0)) | {entity_id, aliases}]' ./.storage/core.entity_registry 108 | ``` 109 | 110 | **Thêm vào `configuration.yaml` (dưới mục `template:` hoặc gộp vào cấu hình hiện có):** 111 | 112 | ```yaml 113 | template: 114 | - triggers: 115 | - trigger: homeassistant 116 | event: start 117 | - trigger: event 118 | event_type: event_template_reloaded 119 | actions: 120 | - action: shell_command.get_entity_alias 121 | response_variable: response 122 | sensor: 123 | - name: "Assist: Entity IDs and Aliases" 124 | unique_id: entity_ids_and_aliases 125 | icon: mdi:format-list-bulleted 126 | device_class: timestamp 127 | state: "{{ now().isoformat() }}" 128 | attributes: 129 | entities: "{{ response.stdout }}" 130 | ``` 131 | 132 | - Sau khi thêm xong, **khởi động lại** Home Assistant. 133 | - **Lưu ý:** Mỗi khi bạn thay đổi Alias, bạn cần **reload template entities** (từ Developer Tools > YAML) hoặc khởi động lại HA để cập nhật. 134 | 135 | ## Bước 2: Thêm Kịch bản (Script) cho Assist 136 | 137 | ### 2.1. Cài đặt Blueprint Get Video Info 138 | 139 | Blueprint này giúp Assist lấy thông tin video mới nhất từ kênh YouTube được yêu cầu. 140 | 141 | [![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fluuquangvu%2Ftutorials%2Fblob%2Fmain%2Fget_youtube_video_info_full_llm.yaml) 142 | 143 | - **Bước làm:** 144 | 1. Import blueprint. 145 | 2. Tạo một **Script** mới từ blueprint này. 146 | 3. Chỉ định Template Sensor (`sensor.assist_entity_ids_and_aliases`) đã tạo ở bước 1.5. 147 | 4. **Quan trọng:** Giữ nguyên tên Script mặc định. 148 | 5. Sau khi tạo xong, **Expose** script đó cho Voice Assist. 149 | 150 | ### 2.2. Cài đặt Blueprint Play Video 151 | 152 | Blueprint này có nhiệm vụ phát video đã tìm được lên thiết bị media player của bạn. 153 | 154 | [![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fluuquangvu%2Ftutorials%2Fblob%2Fmain%2Fplay_youtube_video_full_llm.yaml) 155 | 156 | - **Bước làm:** 157 | 1. Import blueprint. 158 | 2. Tạo một **Script** mới từ blueprint này. 159 | 3. Chỉ định một Smart TV hoặc thiết bị media player sẽ phát video lên. 160 | 4. **Quan trọng:** Giữ nguyên tên Script mặc định. 161 | 162 | ![image](images/20250527_JC5AOg.png) 163 | 164 | - Sau khi tạo xong, **Expose** script đó cho Voice Assist. 165 | 166 | ## 3. Ví dụ lệnh thoại 167 | 168 | Vậy là xong! Bây giờ bạn có thể thử với một số mẫu câu lệnh sau, hoặc biến tấu theo ý muốn: 169 | 170 | - "Hôm nay có video YouTube nào mới không?" -> (Assist trả lời) -> "Mở video XXX nhé" (XXX là một phần nhỏ trong tiêu đề của video). 171 | - "Gần đây [Tên Kênh] có video nào mới không? Hãy phát nó lên TV ngay bây giờ." 172 | - "Tuần này [Tên Kênh 1] và [Tên Kênh 2] có video mới không?" -> (Assist trả lời) -> "Mở video XXX nhé." 173 | - "Tháng này [Tên Kênh 1] hay [Tên Kênh 2] có video nào mới không? Hãy phát nó lên TV ngay bây giờ." 174 | 175 | --- 176 | 177 | **Nếu bạn thấy tính năng này hữu ích, hãy theo dõi để đón chờ thêm những tính năng mới hay ho hơn nữa nhé!** 178 | -------------------------------------------------------------------------------- /home_assistant_play_favorite_youtube_channel_videos_en.md: -------------------------------------------------------------------------------- 1 | # Detailed Guide: Voice Assist to Play YouTube Videos on Smart TV 2 | 3 | This guide allows you to use Home Assistant Voice to play the latest videos from your favorite YouTube channels directly on your Smart TV. 4 | 5 | ## Introduction & Key Features 6 | 7 | - **Purpose:** Automatically play the most recently released video from any YouTube channel you love. 8 | - **LLM Support:** Only works with LLMs like Google or OpenAI. 9 | - **Alias Support:** You can create multiple aliases for channel names to make them easier to call by voice. 10 | 11 | ### Limitations 12 | 13 | - Does not support searching for old videos from a channel. 14 | - Does not support searching for arbitrary videos across all of YouTube (only searches the latest video of followed channels). 15 | - Requires a Smart TV or media player device integrated into Home Assistant. 16 | 17 | ![image](images/20250528_210348.jpg) 18 | 19 | ## Step 1: Get Video Info from Favorite YouTube Channels 20 | 21 | ### 1.1. Install Feedparser Integration 22 | 23 | Feedparser is an integration that helps Home Assistant read RSS/Atom feeds, including YouTube video feeds. 24 | 25 | [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=custom-components&repository=feedparser&category=Integration) 26 | 27 | - See details at: [github.com/custom-components/feedparser](https://github.com/custom-components/feedparser) 28 | - _Note:_ As of `December 10, 2025`, the Feedparser integration does not yet fully support `unique_id` to easily create aliases via the UI. You can follow the progress at [this pull request](https://github.com/custom-components/feedparser/pull/143) or modify the code yourself if you want this feature early. 29 | - After installing via HACS, you must **restart** Home Assistant. 30 | 31 | ### 1.2. Get YouTube Channel ID 32 | 33 | You need the ID of the YouTube channel to configure the sensor. 34 | 35 | 1. Open Google and search for `Get YouTube Channel ID`. 36 | 2. Visit any site (e.g., `https://commentpicker.com/youtube-channel-id.php`). 37 | 3. Enter the URL of the YouTube channel you want to follow to get its ID. 38 | 39 | ![image](images/20250527_FdZbGj.png) 40 | 41 | ### 1.3. Configure Sensor for YouTube Channel 42 | 43 | Once you have the channel ID, add the sensor configuration to your `config/configuration.yaml` file: 44 | 45 | ```yaml 46 | sensor: 47 | - platform: feedparser 48 | name: CHANNEL_NAME YouTube Channel # Change CHANNEL_NAME to your desired channel name 49 | feed_url: https://www.youtube.com/feeds/videos.xml?channel_id=XXXXXX # Replace XXXXXX with the Channel ID 50 | scan_interval: 51 | minutes: 30 # Check frequency (default 30 minutes) 52 | inclusions: 53 | - title 54 | - link 55 | - author 56 | - published 57 | - media_thumbnail 58 | - yt_videoid 59 | date_format: "%Y-%m-%dT%H:%M:%S%z" 60 | ``` 61 | 62 | - **Note:** The phrase "YouTube Channel" in the sensor name is fixed and critical for the LLM to identify it. 63 | - _Example configuration for a channel named "Hoa Ban Food":_ 64 | 65 | ```yaml 66 | sensor: 67 | - platform: feedparser 68 | name: Hoa Ban Food YouTube Channel 69 | feed_url: https://www.youtube.com/feeds/videos.xml?channel_id=UCBhgBmuPFbLLxnejr09lnAQ 70 | scan_interval: 71 | minutes: 30 72 | inclusions: 73 | - title 74 | - link 75 | - author 76 | - published 77 | - media_thumbnail 78 | - yt_videoid 79 | date_format: "%Y-%m-%dT%H:%M:%S%z" 80 | ``` 81 | 82 | - Repeat the steps above for all YouTube channels you want to follow. 83 | - After configuration, **restart** Home Assistant. 84 | 85 | ### 1.4. Share Sensor with Assist and Create Aliases 86 | 87 | For Voice Assist to recognize and interact with your YouTube channels: 88 | 89 | 1. After restarting HA, go to **Settings** > **Voice assistants** > **Expose**. 90 | 2. Find and expose the newly created YouTube channel sensors. 91 | 92 | ![image](images/20250527_gCfAcK.png) 93 | 94 | 3. Create additional **Aliases** for the channels (e.g., "Hoa Ban", "Son Tung") to make them easier to remember or pronounce, especially for foreign channels. 95 | 96 | ![image](images/20250604_VhChze.png) 97 | 98 | ### 1.5. Configure Alias Support for Assist 99 | 100 | To help Assist understand the Aliases you created, we need a `shell_command` and a shared `template sensor`. 101 | 102 | **Add to `configuration.yaml`:** 103 | (Ensure `jq` is installed on your Home Assistant system) 104 | 105 | ```yaml 106 | shell_command: 107 | get_entity_alias: jq '[.data.entities[] | select(.options.conversation.should_expose == true and (.aliases | length > 0)) | {entity_id, aliases}]' ./.storage/core.entity_registry 108 | ``` 109 | 110 | **Add to `configuration.yaml` (under `template:` or merge with existing config):** 111 | 112 | ```yaml 113 | template: 114 | - triggers: 115 | - trigger: homeassistant 116 | event: start 117 | - trigger: event 118 | event_type: event_template_reloaded 119 | actions: 120 | - action: shell_command.get_entity_alias 121 | response_variable: response 122 | sensor: 123 | - name: "Assist: Entity IDs and Aliases" 124 | unique_id: entity_ids_and_aliases 125 | icon: mdi:format-list-bulleted 126 | device_class: timestamp 127 | state: "{{ now().isoformat() }}" 128 | attributes: 129 | entities: "{{ response.stdout }}" 130 | ``` 131 | 132 | - After adding, **restart** Home Assistant. 133 | - **Note:** Whenever you change an Alias, you must **reload template entities** (from Developer Tools > YAML) or restart HA to update. 134 | 135 | ## Step 2: Add Scripts for Assist 136 | 137 | ### 2.1. Install "Get Video Info" Blueprint 138 | 139 | This blueprint helps Assist fetch the latest video info from the requested YouTube channel. 140 | 141 | [![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fluuquangvu%2Ftutorials%2Fblob%2Fmain%2Fget_youtube_video_info_full_llm.yaml) 142 | 143 | - **Steps:** 144 | 1. Import the blueprint. 145 | 2. Create a new **Script** from this blueprint. 146 | 3. Select the Template Sensor (`sensor.assist_entity_ids_and_aliases`) created in step 1.5. 147 | 4. **Important:** Keep the default Script name. 148 | 5. After creating, **Expose** that script to Voice Assist. 149 | 150 | ### 2.2. Install "Play Video" Blueprint 151 | 152 | This blueprint is responsible for playing the found video on your media player device. 153 | 154 | [![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fluuquangvu%2Ftutorials%2Fblob%2Fmain%2Fplay_youtube_video_full_llm.yaml) 155 | 156 | - **Steps:** 157 | 1. Import the blueprint. 158 | 2. Create a new **Script** from this blueprint. 159 | 3. Select a Smart TV or media player device to play the video on. 160 | 4. **Important:** Keep the default Script name. 161 | 162 | ![image](images/20250527_JC5AOg.png) 163 | 164 | - After creating, **Expose** that script to Voice Assist. 165 | 166 | ## 3. Example Voice Commands 167 | 168 | That's it! Now you can try some of the following command patterns, or improvise as you wish: 169 | 170 | - "Are there any new YouTube videos today?" -> (Assist replies) -> "Open video XXX" (XXX is a small part of the video title). 171 | - "Has [Channel Name] released any new videos recently? Play it on the TV right now." 172 | - "Do [Channel Name 1] and [Channel Name 2] have new videos this week?" -> (Assist replies) -> "Open video XXX." 173 | - "Does [Channel Name 1] or [Channel Name 2] have any new videos this month? Play it on the TV now." 174 | 175 | --- 176 | 177 | **If you find this feature useful, stay tuned for more cool features coming soon!** 178 | -------------------------------------------------------------------------------- /device_control_timer_full_llm.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Voice - Set Device Timer 3 | author: luuquangvu 4 | description: | 5 | # Tool for controlling on/off devices with adjustable delay timing used for Voice Assistant 6 | 7 | ## Blueprint Setup 8 | 9 | ### Required 10 | 11 | - The `device_control_tool.yaml` blueprint needs to be installed. 12 | - The mentioned file(s) is/are included in the repository. 13 | - A template sensor stores all information about entity aliases needs to be configured in `config/configuration.yaml`; The sensor is required for friendly-name lookup. 14 | 15 | ``` 16 | #File configuration.yaml 17 | shell_command: 18 | get_entity_alias: jq '[.data.entities[] | select(.options.conversation.should_expose == true and (.aliases | length > 0)) | {entity_id, aliases}]' ./.storage/core.entity_registry 19 | template: 20 | - triggers: 21 | - trigger: homeassistant 22 | event: start 23 | - trigger: event 24 | event_type: event_template_reloaded 25 | actions: 26 | - action: shell_command.get_entity_alias 27 | response_variable: response 28 | sensor: 29 | - name: "Assist: Entity IDs and Aliases" 30 | unique_id: entity_ids_and_aliases 31 | icon: mdi:format-list-bulleted 32 | device_class: timestamp 33 | state: "{{ now().isoformat() }}" 34 | attributes: 35 | entities: "{{ response.stdout }}" 36 | ``` 37 | 38 | ### Optional 39 | 40 | - Adjust the prompts for each field used in the script. The descriptions guide the LLM to provide the correct input. 41 | 42 | ### Note 43 | 44 | - Provide a concise and precise description for the script. This will be utilized by the LLM to understand it should use this script for controlling devices on/off with a specified delay. 45 | - Make sure to expose the entities you want to control to Assist. 46 | - Make sure to expose the script to Assist after the script has been saved. 47 | - Do not alter the default script name. 48 | - Once the script is created, click the three dots in the top right corner, choose "Edit in YAML," and remove the `description: ...` line to restore the default. This step is important because it helps the LLM better understand the script's purpose. 49 | domain: script 50 | homeassistant: 51 | min_version: 2024.10.0 52 | input: 53 | entity_aliases_settings: 54 | name: Settings for Entity Aliases 55 | icon: mdi:format-list-bulleted 56 | description: You can use these settings to configure a template sensor that stores all information about entity aliases. 57 | input: 58 | entity_aliases: 59 | name: Entity Aliases 60 | selector: 61 | entity: 62 | filter: 63 | - domain: sensor 64 | integration: template 65 | controller_settings: 66 | name: Settings for Controller 67 | icon: mdi:controller 68 | description: You can use these settings to select the Device Timer. 69 | input: 70 | controller: 71 | name: Controller 72 | selector: 73 | entity: 74 | filter: 75 | - domain: script 76 | prompt_settings: 77 | name: Prompt settings for the LLM 78 | icon: mdi:robot 79 | description: You can use these settings to finetune the prompts for your specific LLM (model). In most cases the defaults should be fine. 80 | collapsed: true 81 | input: 82 | entities_prompt: 83 | name: Entities Prompt 84 | description: The prompt which will be used for the LLM can provide the name of fans for controlling. 85 | selector: 86 | text: 87 | multiline: true 88 | default: | 89 | This argument is mandatory and must always be included. 90 | Specify at least one device name to control. 91 | When controlling multiple devices, separate each device name with a semicolon. 92 | control_prompt: 93 | name: Control Prompt 94 | description: The prompt which will be used for the LLM can provide the action. 95 | selector: 96 | text: 97 | multiline: true 98 | default: | 99 | This argument is mandatory and must always be included. 100 | Specify the desired action using one of the following two values: 'true' to turn on the device, or 'false' to turn off the device. 101 | timer_prompt: 102 | name: Timer Prompt 103 | description: The prompt which will be used for the LLM can provide the delay of action. 104 | selector: 105 | text: 106 | multiline: true 107 | default: | 108 | This argument is mandatory and must always be included. 109 | Specify the delay time using the format HH:MM:SS. 110 | The delay is relative to the current time. 111 | If a query specifies a particular target time, calculate the delay from the current time to that specified time 112 | mode: parallel 113 | max_exceeded: silent 114 | description: Sets a timer for controlling one or more specified devices. 115 | variables: 116 | version: 20251116 117 | fields: 118 | entities: 119 | name: Entities 120 | description: !input entities_prompt 121 | selector: 122 | text: 123 | required: true 124 | control: 125 | name: Control 126 | description: !input control_prompt 127 | selector: 128 | boolean: 129 | required: true 130 | timer: 131 | name: Timer 132 | description: !input timer_prompt 133 | selector: 134 | time: 135 | required: true 136 | sequence: 137 | - variables: 138 | entity_aliases: !input entity_aliases 139 | entities: "{{ entities | default }}" 140 | control: "{{ control | default(false) }}" 141 | timer: "{{ timer | default('00:00:00') | as_timedelta | default('00:00:00', true) }}" 142 | - alias: Check if variables were set correctly 143 | if: 144 | - condition: template 145 | value_template: | 146 | {% set validation = namespace(not_exist=false) -%} 147 | {% for entity in entities.split(';') -%} 148 | {% if not ((states | selectattr('attributes.friendly_name', '==', entity.strip()) | list) or 149 | (state_attr(entity_aliases, 'entities') | default([]) | selectattr('aliases', 'contains', entity.strip()) | list)) -%} 150 | {% set validation.not_exist = true -%} 151 | {% endif -%} 152 | {% endfor -%} 153 | {{ validation.not_exist }} 154 | then: 155 | - alias: Set variable for error message 156 | variables: 157 | response: 158 | error: Unable to control the device because the device name is invalid. 159 | - alias: Stop the script 160 | stop: Unable to control the device because the device name is invalid. 161 | response_variable: response 162 | - variables: 163 | devices: | 164 | {% set device = namespace(entities=[]) -%} 165 | {% for entity in entities.split(';') -%} 166 | {% if (states | selectattr('attributes.friendly_name', '==', entity.strip()) | list) -%} 167 | {% set device.entities = device.entities + (states | selectattr('attributes.friendly_name', '==', entity.strip()) | map(attribute='entity_id') | list) -%} 168 | {% else -%} 169 | {% set device.entities = device.entities + (state_attr(entity_aliases, 'entities') | default([]) | selectattr('aliases', 'contains', entity.strip()) | map(attribute='entity_id') | list) -%} 170 | {% endif -%} 171 | {% endfor -%} 172 | {{ device.entities }} 173 | - variables: 174 | friendly_entities: | 175 | {% set names = namespace(values=[]) -%} 176 | {% for name in entities.split(';') -%} 177 | {% if name | trim -%} 178 | {% set names.values = names.values + [name.strip()] -%} 179 | {% endif -%} 180 | {% endfor -%} 181 | {{ names.values }} 182 | - action: script.turn_on 183 | target: 184 | entity_id: !input controller 185 | data: 186 | variables: 187 | devices: "{{ devices | join(', ') }}" 188 | control: "{{ control }}" 189 | timer: "{{ timer }}" 190 | - alias: Prepare success response for Assist 191 | variables: 192 | response: 193 | success: | 194 | Scheduled the {{ 'on' if control else 'off' }} action for {{ 195 | friendly_entities | join(', ') }} after {{ timer }}. 196 | - stop: Finish and return response data 197 | response_variable: response 198 | -------------------------------------------------------------------------------- /get_youtube_video_info_full_llm.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Voice - Get YouTube Video Info 3 | author: luuquangvu 4 | description: | 5 | # Tool gets YouTube video info used for Voice Assistant 6 | 7 | ## Blueprint Setup 8 | 9 | ### Required 10 | 11 | - Check out the guide for further details. 12 | - The Feedparser integration needs to be installed from HACS. 13 | - Expose YouTube Channel entities after created to Assist. 14 | - Consider adding entity aliases to make them easier to remember if needed. 15 | - A template sensor stores all information about entity aliases needs to be configured in `config/configuration.yaml`; The sensor is required for friendly-name lookup. 16 | 17 | ``` 18 | #File configuration.yaml 19 | shell_command: 20 | get_entity_alias: jq '[.data.entities[] | select(.options.conversation.should_expose == true and (.aliases | length > 0)) | {entity_id, aliases}]' ./.storage/core.entity_registry 21 | template: 22 | - triggers: 23 | - trigger: homeassistant 24 | event: start 25 | - trigger: event 26 | event_type: event_template_reloaded 27 | actions: 28 | - action: shell_command.get_entity_alias 29 | response_variable: response 30 | sensor: 31 | - name: "Assist: Entity IDs and Aliases" 32 | unique_id: entity_ids_and_aliases 33 | icon: mdi:format-list-bulleted 34 | device_class: timestamp 35 | state: "{{ now().isoformat() }}" 36 | attributes: 37 | entities: "{{ response.stdout }}" 38 | ``` 39 | 40 | ### Optional 41 | 42 | - Adjust the prompts for each field used in the script. The descriptions guide the LLM to provide the correct input. 43 | 44 | ### Note 45 | 46 | - The `play_youtube_video_full_llm.yaml` blueprint needs to be installed to play YouTube videos on a smart TV. 47 | - Provide a concise and precise description for the script. This description will enable the LLM to recognize that the script is designed to obtain the latest video information from a YouTube channel or multiple channels. 48 | - Make sure to expose the script to Assist after the script has been saved. 49 | - Do not alter the default script name. 50 | - Once the script is created, click the three dots in the top right corner, choose "Edit in YAML," and remove the `description: ...` line to restore the default. This step is important because it helps the LLM better understand the script's purpose. 51 | domain: script 52 | homeassistant: 53 | min_version: 2024.10.0 54 | input: 55 | entity_aliases_settings: 56 | name: Settings for Entity Aliases 57 | icon: mdi:format-list-bulleted 58 | description: You can use these settings to configure a template sensor that stores all information about entity aliases. 59 | input: 60 | entity_aliases: 61 | name: Entity Aliases 62 | selector: 63 | entity: 64 | filter: 65 | - domain: sensor 66 | integration: template 67 | short_videos_settings: 68 | name: Settings for Short Videos 69 | icon: mdi:video-high-definition 70 | description: "These settings let you choose whether to ignore short videos in the results or include them (Default: Ignored)." 71 | collapsed: true 72 | input: 73 | short_videos: 74 | name: Short Videos 75 | selector: 76 | boolean: 77 | default: false 78 | prompt_settings: 79 | name: Prompt settings for the LLM 80 | icon: mdi:robot 81 | description: You can use these settings to finetune the prompts for your specific LLM (model). In most cases the defaults should be fine. 82 | collapsed: true 83 | input: 84 | entities_prompt: 85 | name: Entities Prompt 86 | description: The prompt which will be used for the LLM can provide the YouTube channel name for the query. 87 | selector: 88 | text: 89 | multiline: true 90 | default: | 91 | This argument is mandatory and must always be included. 92 | Specify at least one YouTube channel name to query video information. 93 | When requesting multiple YouTube channels, separate each channel name with a semicolon. 94 | If the query does not specify a particular channel, provide all relevant channels based on the context. 95 | If the query contains errors caused by user mispronunciation, correct it to the most likely accurate channel name. 96 | After obtaining the results, prompt the user to select a video to play on the TV. Add an ordinal number to each video to simplify selection. Do not include the Media ID in your response. 97 | period_length_prompt: 98 | name: Period Length Prompt 99 | description: The prompt which will be used for the LLM can provide the length of the period for the query. 100 | selector: 101 | text: 102 | multiline: true 103 | default: | 104 | This argument is optional. 105 | It specifies the number of days since the videos were published. 106 | The default is 3 days. The minimum is 1 day, and the maximum is 30 days. "Today" means 1 day, "This week" means 7 days, and "This month" means 30 days. 107 | mode: parallel 108 | max_exceeded: silent 109 | description: Retrieves a list of recent videos from one or more specified YouTube channels. 110 | variables: 111 | version: 20251116 112 | fields: 113 | entities: 114 | name: Entities 115 | description: !input entities_prompt 116 | selector: 117 | text: 118 | required: true 119 | period_length: 120 | name: Period Length 121 | description: !input period_length_prompt 122 | selector: 123 | number: 124 | min: 1 125 | max: 30 126 | default: 3 127 | sequence: 128 | - variables: 129 | entity_aliases: !input entity_aliases 130 | short_videos: !input short_videos 131 | entities: "{{ entities | default('') | trim }}" 132 | period_length: "{{ period_length | default(3) | abs }}" 133 | - alias: Check if variables were set correctly 134 | if: 135 | - condition: template 136 | value_template: | 137 | {% set validation = namespace(not_exist=false) -%} 138 | {% for entity in entities.split(';') -%} 139 | {% if not ((integration_entities('feedparser') | select('is_state_attr', 'friendly_name', entity.strip()) | list) or 140 | (state_attr(entity_aliases, 'entities') | default([]) | selectattr('entity_id', 'match', 'sensor\.') | selectattr('aliases', 'contains', entity.strip()) | list)) -%} 141 | {% set validation.not_exist = true -%} 142 | {% endif -%} 143 | {% endfor -%} 144 | {{ validation.not_exist }} 145 | then: 146 | - alias: Set variable for error message 147 | variables: 148 | response: 149 | error: Unable to find the video information because the channel name is invalid. 150 | - alias: Stop the script 151 | stop: Unable to find the video information because the channel name is invalid. 152 | response_variable: response 153 | - variables: 154 | response: 155 | entries: | 156 | {% for entity in entities.split(';') -%} 157 | {% set entity_id = (integration_entities('feedparser') | select('is_state_attr', 'friendly_name', entity.strip()) | first) 158 | if (integration_entities('feedparser') | select('is_state_attr', 'friendly_name', entity.strip()) | list) 159 | else (state_attr(entity_aliases, 'entities') | default([]) | selectattr('entity_id', 'match', 'sensor\.') | selectattr('aliases', 'contains', entity.strip()) | map(attribute='entity_id') | first) -%} 160 | {% for entry in state_attr(entity_id, 'entries') if (strptime(entry.published, '%Y-%m-%dT%H:%M:%S%z') > (now() - timedelta(days=period_length))) -%} 161 | {% if entry.link is not search('\/shorts\/') -%} 162 | - channel: {{ entry.author | lower }} 163 | title: {{ entry.title | lower }} 164 | published: {{ strptime(entry.published, '%Y-%m-%dT%H:%M:%S%z') | relative_time }} ago 165 | media_id: {{ entry.yt_videoid }} 166 | {% elif short_videos -%} 167 | - channel: {{ entry.author | lower }} 168 | title: {{ entry.title | lower }} 169 | published: {{ strptime(entry.published, '%Y-%m-%dT%H:%M:%S%z') | relative_time }} ago 170 | media_id: {{ entry.yt_videoid }} 171 | {% endif -%} 172 | {% endfor -%} 173 | {% endfor -%} 174 | - stop: "" 175 | response_variable: response 176 | -------------------------------------------------------------------------------- /device_location_lookup_full_llm.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Voice - Find Device 3 | author: luuquangvu 4 | description: | 5 | # Find My Device Tool used for Voice Assistant 6 | 7 | ## Blueprint Setup 8 | 9 | ### Required 10 | 11 | - Check out the guide `home_assistant_device_location_lookup_guide_en.md` for further details. 12 | - An LLM like Gemini or OpenAI. 13 | - Expose Bermuda Device Tracker or Mobile Device Tracker entities to Assist. 14 | - Only add one device tracker per physical device. 15 | - If you expose your phone or tablet to Voice Assist using Bermuda Device Tracker, make sure to rename Bermuda Device the same as Mobile Device or Mobile Device name must be part of Bermuda Device name. Because this script identifies the relationship between them to find the notification action. This allows you to locate your phone in a specific room and make it ring. 16 | - The `device_ringing_full_llm.yaml` blueprint needs to be installed. 17 | - The mentioned file(s) is/are included in the repository. 18 | - Consider adding entity aliases to make them easier to remember if needed. 19 | - A template sensor stores all information about entity aliases needs to be configured in `config/configuration.yaml`; The sensor is required for friendly-name lookup. 20 | 21 | ``` 22 | #File configuration.yaml 23 | shell_command: 24 | get_entity_alias: jq '[.data.entities[] | select(.options.conversation.should_expose == true and (.aliases | length > 0)) | {entity_id, aliases}]' ./.storage/core.entity_registry 25 | template: 26 | - triggers: 27 | - trigger: homeassistant 28 | event: start 29 | - trigger: event 30 | event_type: event_template_reloaded 31 | actions: 32 | - action: shell_command.get_entity_alias 33 | response_variable: response 34 | sensor: 35 | - name: "Assist: Entity IDs and Aliases" 36 | unique_id: entity_ids_and_aliases 37 | icon: mdi:format-list-bulleted 38 | device_class: timestamp 39 | state: "{{ now().isoformat() }}" 40 | attributes: 41 | entities: "{{ response.stdout }}" 42 | ``` 43 | 44 | ### Optional 45 | 46 | - Adjust the prompts for each field used in the script. The descriptions guide the LLM to provide the correct input. 47 | 48 | ### Note 49 | 50 | - Provide a concise and precise description for the script. This description will enable the LLM to recognize that the script is designed to determine if the device is at home or not, and the specific room where the device is located. 51 | - Make sure to expose the script to Assist after the script has been saved. 52 | - Do not alter the default script name. 53 | - Once the script is created, click the three dots in the top right corner, choose "Edit in YAML," and remove the `description: ...` line to restore the default. This step is important because it helps the LLM better understand the script's purpose. 54 | domain: script 55 | homeassistant: 56 | min_version: 2024.10.0 57 | input: 58 | entity_aliases_settings: 59 | name: Settings for Entity Aliases 60 | icon: mdi:format-list-bulleted 61 | description: You can use these settings to configure a template sensor that stores all information about entity aliases. 62 | input: 63 | entity_aliases: 64 | name: Entity Aliases 65 | selector: 66 | entity: 67 | filter: 68 | - domain: sensor 69 | integration: template 70 | prompt_settings: 71 | name: Prompt settings for the LLM 72 | icon: mdi:robot 73 | description: You can use these settings to finetune the prompts for your specific LLM (model). In most cases the defaults should be fine. 74 | collapsed: true 75 | input: 76 | entities_prompt: 77 | name: Entities Prompt 78 | description: The prompt which will be used for the LLM can provide the device's name for the query. 79 | selector: 80 | text: 81 | multiline: true 82 | default: | 83 | This argument is mandatory and must always be included. 84 | Specify the name of one or more devices whose location is actively tracked by a Home Assistant sensor (device_tracker). 85 | Use this for finding things that report their own real-time position. This tool cannot retrieve information that a user has manually saved as a memory or note. 86 | If requesting multiple devices, separate each device name with a semicolon. 87 | If the query does not specify a device, provide all relevant devices based on the context. 88 | After obtaining the result, if the device supports ringing, prompt the user to activate its ringing function. 89 | mode: parallel 90 | max_exceeded: silent 91 | description: Retrieves the real-time location of tracked electronic devices (phones, watches, tablets, BLE tags, or beacons). Only for devices currently monitored by Home Assistant, not for user-saved information. 92 | variables: 93 | version: 20251116 94 | fields: 95 | entities: 96 | name: Entities 97 | description: !input entities_prompt 98 | selector: 99 | text: 100 | required: true 101 | sequence: 102 | - variables: 103 | entity_aliases: !input entity_aliases 104 | entities: "{{ entities | default('') | trim }}" 105 | - alias: Check if variables were set correctly 106 | if: 107 | - condition: template 108 | value_template: | 109 | {% set validation = namespace(not_exist=false) -%} 110 | {% for entity in entities.split(';') -%} 111 | {% if not ((integration_entities('bermuda') | select('match','device_tracker\.') | select('is_state_attr', 'friendly_name', entity.strip()) | list) or 112 | (integration_entities('mobile_app') | select('match','device_tracker\.') | select('is_state_attr', 'friendly_name', entity.strip()) | list) or 113 | (state_attr(entity_aliases, 'entities') | default([]) | selectattr('entity_id', 'match', 'device_tracker\.') | selectattr('aliases', 'contains', entity.strip()) | list)) -%} 114 | {% set validation.not_exist = true -%} 115 | {% endif -%} 116 | {% endfor -%} 117 | {{ validation.not_exist }} 118 | then: 119 | - alias: Set variable for error message 120 | variables: 121 | response: 122 | error: Unable to find the device because the device name is invalid. 123 | - alias: Stop the script 124 | stop: Unable to find the device because the device name is invalid. 125 | response_variable: response 126 | - variables: 127 | response: 128 | devices: | 129 | {% set mobile_devices = integration_entities('mobile_app') | select('match','device_tracker\.') | map('regex_replace', 'device_tracker\.','') | list -%} 130 | {% for entity in entities.split(';') -%} 131 | {% set device = namespace(friendly_name=none, entity_id=none, notify_id=none, is_mobile=false, at_home=false) -%} 132 | {% set device.friendly_name = entity.strip() -%} 133 | {% if (integration_entities('bermuda') | select('match','device_tracker\.') | select('is_state_attr', 'friendly_name', device.friendly_name) | list) -%} 134 | {% set device.entity_id = integration_entities('bermuda') | select('match','device_tracker\.') | select('is_state_attr', 'friendly_name', device.friendly_name) | first -%} 135 | {% elif (integration_entities('mobile_app') | select('match','device_tracker\.') | select('is_state_attr', 'friendly_name', device.friendly_name) | list) -%} 136 | {% set device.entity_id = integration_entities('mobile_app') | select('match','device_tracker\.') | select('is_state_attr', 'friendly_name', device.friendly_name) | first -%} 137 | {% set device.is_mobile = true -%} 138 | {% set device.notify_id = device.entity_id.split('device_tracker.')[1] -%} 139 | {% else -%} 140 | {% set device.entity_id = state_attr(entity_aliases, 'entities') | default([]) | selectattr('entity_id', 'match', 'device_tracker\.') | selectattr('aliases', 'contains', device.friendly_name) | map(attribute='entity_id') | first -%} 141 | {% endif -%} 142 | {% if not device.is_mobile -%} 143 | {% set parts = device.entity_id.split('device_tracker.')[1].split('_') -%} 144 | {% for i in range(1, (parts | length) + 1) -%} 145 | {% if (parts[:i] | join('_')) in mobile_devices -%} 146 | {% set device.is_mobile = true -%} 147 | {% set device.notify_id = parts[:i] | join('_') -%} 148 | {% endif -%} 149 | {% endfor -%} 150 | {% endif -%} 151 | {% set device.at_home = is_state(device.entity_id, 'home') -%} 152 | - friendly_name: {{ device.friendly_name }} 153 | at_home: {{ device.at_home }} 154 | area: {{ state_attr(device.entity_id, 'area') }} 155 | can_ring: {{ device.is_mobile and device.at_home }} 156 | ring_id: {{ ('notify.mobile_app_' ~ device.notify_id) if (device.is_mobile and device.at_home) else none }} 157 | {% endfor -%} 158 | - stop: "" 159 | response_variable: response 160 | -------------------------------------------------------------------------------- /camera_snapshot_full_llm.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Voice - Capture Camera Snapshot 3 | author: luuquangvu 4 | description: | 5 | # Tool for capturing camera snapshots used for Voice Assistant 6 | 7 | ## Blueprint Setup 8 | 9 | ### Required 10 | 11 | - A template sensor stores all information about entity aliases needs to be configured in `config/configuration.yaml`; The sensor is required for friendly-name lookup. 12 | 13 | ``` 14 | #File configuration.yaml 15 | shell_command: 16 | get_entity_alias: jq '[.data.entities[] | select(.options.conversation.should_expose == true and (.aliases | length > 0)) | {entity_id, aliases}]' ./.storage/core.entity_registry 17 | template: 18 | - triggers: 19 | - trigger: homeassistant 20 | event: start 21 | - trigger: event 22 | event_type: event_template_reloaded 23 | actions: 24 | - action: shell_command.get_entity_alias 25 | response_variable: response 26 | sensor: 27 | - name: "Assist: Entity IDs and Aliases" 28 | unique_id: entity_ids_and_aliases 29 | icon: mdi:format-list-bulleted 30 | device_class: timestamp 31 | state: "{{ now().isoformat() }}" 32 | attributes: 33 | entities: "{{ response.stdout }}" 34 | ``` 35 | 36 | - Ensure the directory defined in the snapshot settings exists (default is `/media`). 37 | 38 | ### Optional 39 | 40 | - Adjust the prompts for each field used in the script. The descriptions guide the LLM to provide the correct input. 41 | 42 | ### Note 43 | 44 | - Provide a concise and precise description for the script. This will be utilized by the LLM to understand it should use this script for capturing a snapshot from a camera. 45 | - Make sure to expose camera entities to Assist. 46 | - Make sure to expose the script to Assist after the script has been saved. 47 | - Do not alter the default script name. 48 | - Once the script is created, click the three dots in the top right corner, choose "Edit in YAML," and remove the `description: ...` line to restore the default. This step is important because it helps the LLM better understand the script's purpose. 49 | domain: script 50 | homeassistant: 51 | min_version: 2024.10.0 52 | input: 53 | entity_aliases_settings: 54 | name: Settings for Entity Aliases 55 | icon: mdi:format-list-bulleted 56 | description: You can use these settings to configure a template sensor that stores all information about entity aliases. 57 | input: 58 | entity_aliases: 59 | name: Entity Aliases 60 | selector: 61 | entity: 62 | filter: 63 | - domain: sensor 64 | integration: template 65 | snapshot_settings: 66 | name: Settings for Snapshot Output 67 | icon: mdi:folder-image 68 | description: Configure where snapshots should be stored and how filenames are generated. 69 | collapsed: true 70 | input: 71 | snapshot_directory: 72 | name: Snapshot Directory 73 | description: Absolute path where the snapshot will be stored (default is `/media`). 74 | selector: 75 | text: 76 | default: /media 77 | snapshot_filename_prefix: 78 | name: Snapshot Filename Prefix 79 | description: Prefix used when generating the snapshot filename. 80 | selector: 81 | text: 82 | default: camera_snapshot_ 83 | snapshot_file_extension: 84 | name: Snapshot File Extension 85 | description: File extension to use for the snapshot (for example jpg or png). 86 | selector: 87 | text: 88 | default: jpg 89 | prompt_settings: 90 | name: Prompt settings for the LLM 91 | icon: mdi:robot 92 | description: You can use these settings to finetune the prompts for your specific LLM (model). In most cases the defaults should be fine. 93 | collapsed: true 94 | input: 95 | camera_name_prompt: 96 | name: Camera Name Prompt 97 | description: The prompt which will be used for the LLM can provide the camera name. 98 | selector: 99 | text: 100 | multiline: true 101 | default: | 102 | This argument is mandatory and must always be included. 103 | Specify the friendly name of the camera you want to capture a snapshot from (e.g., "Front Door Camera"). 104 | If the user's request does not clearly specify a camera, determine and suggest one or more relevant cameras based on context. 105 | If multiple cameras are needed, submit each camera in a separate tool call. 106 | The tool will return a local file path to the generated snapshot. 107 | mode: parallel 108 | max_exceeded: silent 109 | description: Captures a snapshot from a specified camera to visually check the scene. Returns the local file path of the image, which can be used by other tools for analysis or sent to the user. 110 | variables: 111 | version: 20251209 112 | fields: 113 | camera_name: 114 | name: Camera Name 115 | description: !input camera_name_prompt 116 | selector: 117 | text: 118 | required: true 119 | sequence: 120 | - variables: 121 | entity_aliases: !input entity_aliases 122 | camera_name: "{{ camera_name | default('') | trim }}" 123 | snapshot_directory_input: !input snapshot_directory 124 | snapshot_directory: "{{ snapshot_directory_input | default('/media', true) | trim }}" 125 | snapshot_prefix_input: !input snapshot_filename_prefix 126 | snapshot_prefix: "{{ snapshot_prefix_input | default('camera_snapshot_', true) | trim }}" 127 | snapshot_extension_input: !input snapshot_file_extension 128 | snapshot_extension: "{{ snapshot_extension_input | default('jpg', true) | trim | lower }}" 129 | - alias: Validate camera name 130 | if: 131 | - condition: template 132 | value_template: | 133 | {% set friendly_match = states.camera | selectattr('attributes.friendly_name', '==', camera_name) | list %} 134 | {% set alias_match = state_attr(entity_aliases, 'entities') | default([]) | selectattr('entity_id', 'match', 'camera\.') | selectattr('aliases', 'contains', camera_name) | list %} 135 | {{ not camera_name or (friendly_match | length == 0 and alias_match | length == 0) }} 136 | then: 137 | - alias: Set variable for error message 138 | variables: 139 | response: 140 | error: Unable to capture a snapshot because the camera name is invalid. 141 | - alias: Stop the script 142 | stop: Unable to capture a snapshot because the camera name is invalid. 143 | response_variable: response 144 | - alias: Validate snapshot settings 145 | if: 146 | - condition: template 147 | value_template: | 148 | {{ not snapshot_directory }} 149 | then: 150 | - alias: Set variable for error message 151 | variables: 152 | response: 153 | error: Unable to capture a snapshot because the snapshot directory is missing. 154 | - alias: Stop the script 155 | stop: Unable to capture a snapshot because the snapshot directory is missing. 156 | response_variable: response 157 | - variables: 158 | camera_entity_id: | 159 | {% set friendly_match = states.camera | selectattr('attributes.friendly_name', '==', camera_name) | map(attribute='entity_id') | first %} 160 | {% if friendly_match %} 161 | {{ friendly_match }} 162 | {% else %} 163 | {{ state_attr(entity_aliases, 'entities') | default([]) | selectattr('entity_id', 'match', 'camera\.') | selectattr('aliases', 'contains', camera_name) | map(attribute='entity_id') | first }} 164 | {% endif %} 165 | sanitized_prefix: | 166 | {% if snapshot_prefix %} 167 | {{ snapshot_prefix | regex_replace('[^0-9A-Za-z_-]', '_') }} 168 | {% else %} 169 | camera_snapshot_ 170 | {% endif %} 171 | sanitized_extension: | 172 | {% if snapshot_extension %} 173 | {% set ext = snapshot_extension | regex_replace('[^0-9a-z]', '') %} 174 | {{ '.' ~ (ext if ext else 'jpg') }} 175 | {% else %} 176 | .jpg 177 | {% endif %} 178 | snapshot_directory_normalized: | 179 | {% if snapshot_directory %} 180 | {{ snapshot_directory.rstrip('/\\') }} 181 | {% else %} 182 | /media 183 | {% endif %} 184 | snapshot_random_suffix: "{{ '%05d' | format(range(0, 100000) | random) }}" 185 | snapshot_filename: | 186 | {{ sanitized_prefix ~ now().strftime('%Y%m%d%H%M%S%f') ~ '_' ~ snapshot_random_suffix ~ sanitized_extension }} 187 | snapshot_full_path: | 188 | {{ snapshot_directory_normalized ~ '/' ~ snapshot_filename }} 189 | returned_image_path: | 190 | {% set prefix = '/media/' %} 191 | {% if snapshot_full_path.startswith(prefix) %} 192 | {{ 'local/' ~ snapshot_full_path[prefix | length:] }} 193 | {% else %} 194 | {{ snapshot_full_path }} 195 | {% endif %} 196 | - action: camera.snapshot 197 | target: 198 | entity_id: "{{ camera_entity_id }}" 199 | data: 200 | filename: "{{ snapshot_full_path }}" 201 | - variables: 202 | response: 203 | image_path: "{{ returned_image_path }}" 204 | - stop: "" 205 | response_variable: response 206 | -------------------------------------------------------------------------------- /devices_schedules_restart_handler.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Devices Schedules Restart Handler 3 | author: luuquangvu 4 | description: | 5 | # Devices Schedules Restart Handler 6 | 7 | - Runs automatically when Home Assistant comes back online. 8 | - Restores cached timers created by the Devices Schedules script, resuming ones still active and executing actions for any that expired while offline. 9 | 10 | ## Blueprint Setup 11 | 12 | ### Required 13 | 14 | - The Pyscript integration needs to be installed through HACS and properly configured. 15 | - The `scripts/common_utilities.py` script need to be copied into the `config/pyscript` folder. 16 | - The `devices_schedules.yaml` blueprint needs to be installed. 17 | - The mentioned file(s) is/are included in the repository. 18 | - Enable two Pyscript configuration options in `config/configuration.yaml` to permit the import of any Python package and to expose hass as a variable. 19 | 20 | ``` 21 | #File configuration.yaml 22 | pyscript: 23 | allow_all_imports: true 24 | hass_is_global: true 25 | ``` 26 | domain: automation 27 | input: 28 | required_settings: 29 | name: Required settings 30 | icon: mdi:script-text 31 | description: Select the Devices Schedules script entity to resume timers for. 32 | input: 33 | timer_script: 34 | name: Devices Schedules script entity 35 | description: Script entity created from the Devices Schedules blueprint 36 | selector: 37 | entity: 38 | filter: 39 | - domain: script 40 | advanced_settings: 41 | name: Advanced settings 42 | icon: mdi:tools 43 | description: Optional cache parameters (index key, timer prefix, TTL buffer). Only change if you customized them in Devices Schedules. 44 | collapsed: true 45 | input: 46 | registry_index_key: 47 | name: Cache index key 48 | description: Cache key that stores the full list of timer IDs 49 | default: voice_timer_index 50 | selector: 51 | text: 52 | registry_timer_prefix: 53 | name: Per-timer cache key prefix 54 | description: Prefix prepended to each individual timer key in cache 55 | default: "voice_timer:" 56 | selector: 57 | text: 58 | ttl_buffer_seconds: 59 | name: TTL buffer (seconds) 60 | description: Seconds added to each timer's remaining duration when writing to cache 61 | default: 120 62 | selector: 63 | number: 64 | min: 60 65 | max: 3600 66 | unit_of_measurement: s 67 | mode: box 68 | 69 | mode: queued 70 | max_exceeded: silent 71 | 72 | variables: 73 | version: 20251209 74 | 75 | triggers: 76 | - trigger: homeassistant 77 | event: start 78 | 79 | conditions: [] 80 | 81 | actions: 82 | - variables: 83 | _index_key: !input registry_index_key 84 | _prefix: !input registry_timer_prefix 85 | _buf: !input ttl_buffer_seconds 86 | _script_entity: !input timer_script 87 | _ids_to_remove: [] 88 | 89 | - action: pyscript.memory_cache_get 90 | data: 91 | key: "{{ _index_key }}" 92 | response_variable: _idx_res 93 | 94 | - variables: 95 | _timer_ids: | 96 | {% set raw = _idx_res.value if _idx_res.status == 'ok' else [] %} 97 | {% if raw is sequence %} 98 | {{ raw }} 99 | {% else %} 100 | {{ [] }} 101 | {% endif %} 102 | 103 | - repeat: 104 | for_each: "{{ _timer_ids if _timer_ids is sequence else [] }}" 105 | sequence: 106 | - variables: 107 | _should_remove: false 108 | 109 | - action: pyscript.memory_cache_get 110 | data: 111 | key: "{{ _prefix }}{{ repeat.item }}" 112 | response_variable: _timer_res 113 | 114 | - variables: 115 | _timer_data: "{{ _timer_res.value | default({}) if _timer_res.status == 'ok' else {} }}" 116 | _status: "{{ _timer_data.status | default('') | string | lower }}" 117 | 118 | - choose: 119 | - conditions: "{{ _timer_res.status == 'ok' and _status == 'running' }}" 120 | sequence: 121 | - variables: 122 | _end_iso: "{{ _timer_data.end | default('') }}" 123 | _end_dt: "{{ as_datetime(_end_iso, none) }}" 124 | _remaining: | 125 | {% if _end_dt is not none %} 126 | {{ (as_timestamp(_end_dt) - as_timestamp(now())) | int }} 127 | {% else %} 128 | -1 129 | {% endif %} 130 | 131 | - choose: 132 | - conditions: "{{ _remaining > 0 }}" 133 | sequence: 134 | - variables: 135 | _paused_payload: | 136 | {{ 137 | dict( 138 | _timer_data, 139 | **{ 140 | 'status': 'paused', 141 | 'paused_at': now().isoformat(), 142 | 'remaining': _remaining 143 | } 144 | ) 145 | }} 146 | _ttl_seconds: "{{ (_remaining | int) + (_buf | int) }}" 147 | 148 | - action: pyscript.memory_cache_set 149 | data: 150 | key: "{{ _prefix }}{{ repeat.item }}" 151 | value: "{{ _paused_payload }}" 152 | ttl_seconds: "{{ _ttl_seconds }}" 153 | response_variable: cache_set_timer_id 154 | 155 | - choose: 156 | - conditions: "{{ _script_entity | length > 0 }}" 157 | sequence: 158 | - service: script.turn_on 159 | target: 160 | entity_id: "{{ _script_entity }}" 161 | data: 162 | variables: 163 | mode: resume 164 | timer_id: "{{ repeat.item }}" 165 | target_entity: "{{ _timer_data.entity_id | default('') }}" 166 | 167 | default: 168 | - variables: 169 | _actions_list: | 170 | {% set raw = _timer_data.actions | default([]) %} 171 | {{ raw if raw is sequence else [] }} 172 | 173 | - choose: 174 | - conditions: "{{ _actions_list | count > 0 }}" 175 | sequence: 176 | - repeat: 177 | for_each: "{{ _actions_list }}" 178 | sequence: 179 | - choose: 180 | - conditions: "{{ repeat.item.action is defined and repeat.item.action | string | length > 0 }}" 181 | sequence: 182 | - service: "{{ repeat.item.action }}" 183 | target: "{{ repeat.item.target | default({}) }}" 184 | data: "{{ repeat.item.data | default({}) }}" 185 | continue_on_error: true 186 | 187 | - variables: 188 | _expired_payload: | 189 | {{ 190 | dict( 191 | _timer_data, 192 | **{ 193 | 'status': 'expired', 194 | 'expired_at': now().isoformat(), 195 | 'remaining': 0 196 | } 197 | ) 198 | }} 199 | 200 | - action: pyscript.memory_cache_set 201 | data: 202 | key: "{{ _prefix }}{{ repeat.item }}" 203 | value: "{{ _expired_payload }}" 204 | ttl_seconds: "{{ _buf }}" 205 | response_variable: cache_set_timer_id 206 | 207 | - variables: 208 | _should_remove: true 209 | 210 | - conditions: "{{ _timer_res.status == 'ok' and _status == 'paused' }}" 211 | sequence: [] 212 | 213 | default: 214 | - variables: 215 | _should_remove: true 216 | 217 | - choose: 218 | - conditions: "{{ _should_remove }}" 219 | sequence: 220 | - variables: 221 | _ids_to_remove: | 222 | {% set current = _ids_to_remove if _ids_to_remove is sequence else [] %} 223 | {{ current + [repeat.item] }} 224 | 225 | - action: pyscript.memory_cache_get 226 | data: 227 | key: "{{ _index_key }}" 228 | response_variable: _latest_idx_res 229 | 230 | - variables: 231 | _updated_index: | 232 | {% set fresh_index = _latest_idx_res.value if _latest_idx_res.status == 'ok' else [] %} 233 | {% set fresh_index = fresh_index if fresh_index is sequence else [] %} 234 | {% set remove = _ids_to_remove if _ids_to_remove is sequence else [] %} 235 | {{ fresh_index | reject('in', remove) | list }} 236 | 237 | - action: pyscript.memory_cache_index_update 238 | data: 239 | index_key: "{{ _index_key }}" 240 | replace: "{{ _updated_index }}" 241 | ttl_seconds: 2592000 242 | response_variable: cache_set_index 243 | -------------------------------------------------------------------------------- /zalo_bot_webhook.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Zalo Bot Webhook 3 | author: luuquangvu 4 | description: | 5 | # A Zalo bot enabling seamless two-way communication with Home Assistant. 6 | 7 | ## Blueprint Setup 8 | 9 | ### Required 10 | 11 | - Create a Zalo bot if it does not already exist using OA Zalo Bot Manager. 12 | - The Pyscript integration needs to be installed through HACS and properly configured. 13 | - The `scripts/zalo_bot_handle_tool.py` and `scripts/common_utilities.py` scripts need to be copied into the `config/pyscript` folder. 14 | - The `scripts/requirements.txt` file needs to be copied into the `config/pyscript` folder. 15 | - The mentioned file(s) is/are included in the repository. 16 | - Enable two Pyscript configuration options in `config/configuration.yaml` to permit the import of any Python package and to expose hass as a variable. 17 | - A Zalo bot token needs to be configured in `config/configuration.yaml` and `config/secrets.yaml`. 18 | 19 | ``` 20 | #File configuration.yaml 21 | pyscript: 22 | allow_all_imports: true 23 | hass_is_global: true 24 | zalo_bot_token: !secret zalo_bot_token 25 | ``` 26 | 27 | ``` 28 | #File secrets.yaml 29 | zalo_bot_token: XXXXXX # Retrieve the token from the OA Zalo Bot Manager. 30 | ``` 31 | 32 | - Use the `pyscript.get_zalo_updates` action to retrieve chat IDs then use the `pyscript.set_zalo_webhook` action to generate a Webhook ID. 33 | 34 | ### Note 35 | 36 | - The `file_content_analyzer_full_llm.yaml` blueprint must be installed to analyze and extract various types of information from media and document files. 37 | 38 | ### Optional 39 | 40 | - Use the `pyscript.get_zalo_bot_info` action to retrieve Zalo bot basic information. 41 | - Use the `pyscript.get_zalo_updates` action to retrieve chat IDs and user IDs. 42 | - Use the `pyscript.set_zalo_webhook` action to easily configure a Zalo Webhook ID automatically. 43 | - Use the `pyscript.get_zalo_webhook` action to check the current Zalo Webhook ID. 44 | - Use the `pyscript.delete_zalo_webhook` action to delete the Zalo Webhook ID when you are no longer using it. 45 | - Use the `pyscript.send_zalo_message` action to easily send a message directly to Zalo in your automation without any dependencies. 46 | - Use the `pyscript.send_zalo_photo` action to easily send a photo directly to Zalo in your automation without any dependencies. 47 | domain: automation 48 | homeassistant: 49 | min_version: 2025.8.0 50 | input: 51 | webhook_settings: 52 | name: Settings for Webhook 53 | icon: mdi:webhook 54 | description: These settings allow you to set up the Webhook ID for receiving Zalo messages. 55 | input: 56 | webhook_id: 57 | name: Webhook ID 58 | description: Enter the preferred Webhook ID. Use the `pyscript.set_zalo_webhook` action to generate one automatically. 59 | selector: 60 | text: 61 | zalo_settings: 62 | name: Settings for Zalo 63 | icon: mdi:alpha-z-circle-outline 64 | description: These settings allow you to set up the Zalo Chat IDs and optionally restrict specific user IDs within group chat IDs. 65 | input: 66 | chat_ids: 67 | name: Chat IDs 68 | description: Specified chat IDs enable communication with the Zalo bot. Use the `pyscript.get_zalo_updates` action to retrieve chat IDs. 69 | selector: 70 | text: 71 | multiple: true 72 | user_ids: 73 | name: User IDs 74 | description: You can optionally restrict specific user IDs within group chat IDs to enable communication with the Zalo bot. If left empty, by default, all users in the selected group chat IDs are allowed. 75 | selector: 76 | text: 77 | multiple: true 78 | default: 79 | agent_settings: 80 | name: Settings for Conversation Agent 81 | icon: mdi:robot-outline 82 | description: These settings allow you to set up the conversation agent responsible for managing messages. 83 | input: 84 | agent_id: 85 | name: Conversation Agent 86 | description: It should be the same conversational agent found under the Voice Assistants - Assist settings. 87 | selector: 88 | entity: 89 | filter: 90 | domain: conversation 91 | language: 92 | name: Language 93 | description: The language code used to communicate with a conversation agent follows the IETF language tag standard, such as en-US. 94 | selector: 95 | text: 96 | default: vi-VN 97 | mode: queued 98 | max: 30 99 | max_exceeded: silent 100 | variables: 101 | version: 20251209 102 | chat_ids: !input chat_ids 103 | user_ids: !input user_ids 104 | language: !input language 105 | alias: Zalo bot webhook 106 | description: "" 107 | triggers: 108 | - trigger: webhook 109 | allowed_methods: 110 | - POST 111 | local_only: false 112 | webhook_id: !input webhook_id 113 | conditions: 114 | - condition: template 115 | value_template: | 116 | {{ trigger.json is defined and trigger.json.message is defined }} 117 | - condition: template 118 | value_template: | 119 | {{ (trigger.json.message.chat.id | string) in chat_ids }} 120 | - condition: template 121 | value_template: | 122 | {% if user_ids is list and user_ids | count > 0 -%} 123 | {% if (trigger.json.message.from.id | string) in user_ids -%} 124 | {{ true }} 125 | {% else -%} 126 | {{ false }} 127 | {% endif -%} 128 | {% else -%} 129 | {{ true }} 130 | {% endif -%} 131 | actions: 132 | - variables: 133 | chat_id: | 134 | {{ ('id_' ~ trigger.json.message.chat.id ~ '_' ~ trigger.json.message.from.id) | slugify }} 135 | - action: pyscript.memory_cache_get 136 | data: 137 | key: "{{ chat_id }}" 138 | response_variable: get_chat 139 | - variables: 140 | conversation_id: | 141 | {{ get_chat.value if (get_chat.status == 'ok' and get_chat.value | default('', true) | length > 0) }} 142 | - choose: 143 | - conditions: 144 | - condition: template 145 | value_template: "{{ trigger.json.event_name == 'message.text.received' }}" 146 | sequence: 147 | - variables: 148 | agent_input: 149 | system: | 150 | Always respond in the user's language: {{ language | trim }}. 151 | prompt: | 152 | {{ trigger.json.message.text }} 153 | zalo_chat_id: "{{ trigger.json.message.chat.id }}" 154 | alias: Process text 155 | - conditions: 156 | - condition: template 157 | value_template: "{{ trigger.json.event_name == 'message.image.received' }}" 158 | sequence: 159 | - action: pyscript.get_zalo_file 160 | data: 161 | url: "{{ trigger.json.message.photo_url }}" 162 | response_variable: file 163 | - variables: 164 | agent_input: 165 | system: | 166 | Always use the *File Content Analyzer* to analyze file content and identify the user's request. 167 | If the prompt is empty, always treat any instruction or question within the file (via transcription/OCR) as the user's request and execute it directly. 168 | Only when no instruction or question exists in the file, return a concise summary of its content. 169 | Never ask the user for confirmation. Use any other tools as needed, and provide a clear, complete response. 170 | Always respond in the user's language: {{ language | trim }}. 171 | prompt: | 172 | {{ trigger.json.message.caption }} 173 | file_path: "{{ file.file_path if file.file_path is defined }}" 174 | mime_type: "{{ file.mime_type if file.mime_type is defined }}" 175 | zalo_chat_id: "{{ trigger.json.message.chat.id }}" 176 | supported: "{{ file.supported if file.supported is defined }}" 177 | alias: Process photo 178 | - choose: 179 | - conditions: 180 | - condition: template 181 | value_template: "{{ supported is defined and not bool(supported) }}" 182 | sequence: 183 | - action: pyscript.send_zalo_message 184 | data: 185 | chat_id: "{{ trigger.json.message.chat.id }}" 186 | message: This file type is not supported for analysis at the moment. The file has been stored without further processing. 187 | response_variable: zalo_response 188 | - conditions: 189 | - condition: template 190 | value_template: "{{ agent_input is defined }}" 191 | sequence: 192 | - action: pyscript.send_zalo_chat_action 193 | data: 194 | chat_id: "{{ trigger.json.message.chat.id }}" 195 | response_variable: zalo_chat_action 196 | - if: 197 | - condition: template 198 | value_template: "{{ not conversation_id }}" 199 | then: 200 | - action: conversation.process 201 | data: 202 | agent_id: !input agent_id 203 | text: "{{ agent_input }}" 204 | language: "{{ language | trim }}" 205 | response_variable: result 206 | continue_on_error: true 207 | else: 208 | - action: conversation.process 209 | data: 210 | agent_id: !input agent_id 211 | text: "{{ agent_input }}" 212 | conversation_id: "{{ conversation_id }}" 213 | language: "{{ language | trim }}" 214 | response_variable: result 215 | continue_on_error: true 216 | - variables: 217 | response: | 218 | {% if result is defined and result.response is defined and result.response.speech is defined and result.response.speech.plain is defined and (result.response.speech.plain.speech | length) > 0 %} 219 | {{ result.response.speech.plain.speech }} 220 | {% else %} 221 | I encountered an error processing your request. Please check the Home Assistant logs. 222 | {% endif %} 223 | - if: 224 | - condition: template 225 | value_template: "{{ result is defined and result.conversation_id is defined }}" 226 | then: 227 | - action: pyscript.memory_cache_set 228 | data: 229 | value: "{{ result.conversation_id }}" 230 | key: "{{ chat_id }}" 231 | response_variable: set_chat 232 | - action: pyscript.send_zalo_message 233 | data: 234 | chat_id: "{{ trigger.json.message.chat.id }}" 235 | message: "{{ response | trim }}" 236 | response_variable: zalo_response 237 | default: 238 | - action: pyscript.send_zalo_message 239 | data: 240 | chat_id: "{{ trigger.json.message.chat.id }}" 241 | message: This type of message is not supported by the current handler. 242 | response_variable: zalo_response 243 | -------------------------------------------------------------------------------- /send_to_zalo_custom_bot_full_llm.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Voice - Send to Zalo 3 | author: luuquangvu 4 | description: | 5 | # Tool for sending content to Zalo used for Voice Assistant 6 | 7 | ## Blueprint Setup 8 | 9 | ### Required 10 | 11 | - The Zalo Bot integration must be installed through HACS and properly configured. 12 | 13 | ### Optional 14 | 15 | - Adjust the prompts for each field used in the script. The descriptions guide the LLM to provide the correct input. 16 | 17 | ### Note 18 | 19 | - Provide a concise and precise description for the script. This will be utilized by the LLM to understand it should use this script for sending user requested content to Zalo. 20 | - Make sure to expose the script to Assist after the script has been saved. 21 | - Do not alter the default script name. 22 | - Once the script is created, click the three dots in the top right corner, choose "Edit in YAML," and remove the `description: ...` line to restore the default. This step is important because it helps the LLM better understand the script's purpose. 23 | domain: script 24 | homeassistant: 25 | min_version: 2024.10.0 26 | input: 27 | zalo_settings: 28 | name: Settings for Zalo 29 | icon: mdi:alpha-z-circle-outline 30 | description: You can use these settings to configure Zalo for sending the message. 31 | input: 32 | account: 33 | name: Account 34 | description: The account (phone number) to send the message. 35 | selector: 36 | text: 37 | thread_id: 38 | name: Thread ID 39 | description: The thread to send the message to. 40 | selector: 41 | text: 42 | receiver: 43 | name: Type of Receiver 44 | description: The type of receiver to send the message to. 45 | selector: 46 | select: 47 | options: 48 | - label: User 49 | value: "0" 50 | - label: Group 51 | value: "1" 52 | default: "0" 53 | ttl: 54 | name: TTL 55 | description: Message auto-recall time in milliseconds. Default is 0 (no auto-recall). 56 | selector: 57 | number: 58 | min: 0 59 | max: 86400000 60 | step: 1 61 | default: 0 62 | prompt_settings: 63 | name: Prompt settings for the LLM 64 | icon: mdi:robot 65 | description: You can use these settings to finetune the prompts for your specific LLM (model). In most cases the defaults should be fine. 66 | collapsed: true 67 | input: 68 | summary_prompt: 69 | name: Summary Prompt 70 | description: The prompt which will be used for the LLM can provide the message summary. 71 | selector: 72 | text: 73 | multiline: true 74 | default: | 75 | This argument is mandatory and must always be included. 76 | Specify a brief overview, such as the name of a person, object, or location title. 77 | Keep it short (ideally <= 10 words). No extra commentary, no markdown, emojis, or HTML. 78 | If content_type is image, write a concise caption that clearly describes the image subject. 79 | Return only the plain text value. 80 | When submitting multiple items, make sure each one must be sent separately through a separate tool call. 81 | detail_prompt: 82 | name: Detail Prompt 83 | description: The prompt which will be used for the LLM can provide the message detail. 84 | selector: 85 | text: 86 | multiline: true 87 | default: | 88 | This argument is optional. 89 | Specify clear and specific details, such as comprehensive information or a detailed description of the subject. 90 | No links, no markdown/emojis, no additional commentary, and avoid repeating the summary. 91 | If content_type is image, provide short optional supporting text to appear under the image (leave blank if not needed). 92 | Return only the plain text value. 93 | When submitting multiple items, make sure each one must be sent separately through a separate tool call. 94 | content_type_prompt: 95 | name: Content Type Prompt 96 | description: The prompt which will be used for the LLM can specify the type of content for the message. 97 | selector: 98 | text: 99 | multiline: true 100 | default: | 101 | This argument is mandatory and must always be included. 102 | Return exactly one of the following options: information, location, image. 103 | Return only the word, without quotes or extra text. 104 | Choose location for any place/address/directions/map intent. 105 | Do not choose information in these cases. 106 | Choose image when the user requests an image or photo to be sent and provides or implies a path. 107 | Otherwise choose information. 108 | When submitting multiple items, make sure each one must be sent separately through a separate tool call. 109 | location_prompt: 110 | name: Location Prompt 111 | description: The prompt which will be used for the LLM can provide the location for the place. 112 | selector: 113 | text: 114 | multiline: true 115 | default: | 116 | This argument is optional. Use only when content_type is location. 117 | Return the address of the location. Names only; no commas, prefixes or punctuation; single spaces. 118 | If content_type is not location: leave this blank. 119 | Return only the value. 120 | image_path_prompt: 121 | name: Image Path Prompt 122 | description: Prompt shown to the LLM to request the media path when sending an image. 123 | selector: 124 | text: 125 | multiline: true 126 | default: | 127 | This argument is optional. Use only when content_type is image. 128 | Specify the local or media path to the image that should be sent (for example: /media/... or local/...). 129 | The path must already exist in Home Assistant and point to an actual image file. 130 | When unknown or not specified by the user, leave this blank and do not guess. 131 | If content_type is not image: leave this blank. 132 | Return only the value. 133 | mode: queued 134 | max: 30 135 | max_exceeded: silent 136 | description: Sends a message to a specified Zalo chat. 137 | variables: 138 | version: 20251116 139 | fields: 140 | summary: 141 | name: Summary 142 | description: !input summary_prompt 143 | selector: 144 | text: 145 | required: true 146 | detail: 147 | name: Detail 148 | description: !input detail_prompt 149 | selector: 150 | text: 151 | content_type: 152 | name: Content Type 153 | description: !input content_type_prompt 154 | selector: 155 | select: 156 | options: 157 | - information 158 | - location 159 | - image 160 | required: true 161 | location: 162 | name: Location 163 | description: !input location_prompt 164 | selector: 165 | text: 166 | image_path: 167 | name: Image Path 168 | description: !input image_path_prompt 169 | selector: 170 | text: 171 | sequence: 172 | - variables: 173 | default_account: !input account 174 | default_thread_id: !input thread_id 175 | default_receiver: !input receiver 176 | summary: "{{ summary | default('') | trim }}" 177 | detail: "{{ detail | default('') | trim }}" 178 | content_type: "{{ content_type | default('') | trim }}" 179 | location: "{{ location | default('') | trim }}" 180 | image_path: "{{ image_path | default('') | trim }}" 181 | image_path_resolved: | 182 | {% set path = image_path %} 183 | {% set prefix = 'local/' %} 184 | {% if path and path.startswith(prefix) %} 185 | {{ '/media/' ~ path[prefix | length:] }} 186 | {% else %} 187 | {{ path }} 188 | {% endif %} 189 | - alias: Check if variables were set correctly 190 | if: 191 | - condition: template 192 | value_template: | 193 | {{ 194 | (not summary) 195 | or (content_type not in ['information', 'location', 'image']) 196 | or (content_type == 'location' and not location) 197 | or (content_type == 'image' and not image_path) 198 | }} 199 | then: 200 | - alias: Set variable for error message 201 | variables: 202 | response: 203 | error: | 204 | Unable to send the message to Zalo because one or more inputs are missing or invalid (summary, content_type, account, thread_id, receiver, location, or image_path). 205 | - alias: Stop the script 206 | stop: Unable to send the message to Zalo due to invalid inputs. 207 | response_variable: response 208 | - choose: 209 | - conditions: 210 | - condition: template 211 | value_template: "{{ content_type == 'information' }}" 212 | sequence: 213 | - action: zalo_bot.send_message 214 | data: 215 | account_selection: !input account 216 | thread_id: !input thread_id 217 | type: !input receiver 218 | message: | 219 | {{ summary }}{% if detail %} 220 | 221 | {{ detail }}{% endif %} 222 | 223 | {% set query = summary.split() | join('+') -%} 224 | {% set url = 'https://www.google.com/search?q=' ~ query -%} 225 | Google Search ({{ url }}) 226 | ttl: !input ttl 227 | - conditions: 228 | - condition: template 229 | value_template: "{{ content_type == 'location' }}" 230 | sequence: 231 | - action: zalo_bot.send_message 232 | data: 233 | account_selection: !input account 234 | thread_id: !input thread_id 235 | type: !input receiver 236 | message: | 237 | {{ summary }}{% if detail %} 238 | 239 | {{ detail }}{% endif %} 240 | 241 | {% set place = (summary ~ ' ' ~ location).split() | join('+') -%} 242 | {% set url = 'https://www.google.com/maps/search/?api=1&query=' ~ place -%} 243 | Google Maps ({{ url }}) 244 | ttl: !input ttl 245 | - conditions: 246 | - condition: template 247 | value_template: "{{ content_type == 'image' }}" 248 | sequence: 249 | - action: zalo_bot.send_image 250 | data: 251 | account_selection: !input account 252 | thread_id: !input thread_id 253 | type: !input receiver 254 | image_path: "{{ image_path_resolved }}" 255 | message: | 256 | {{ summary }}{% if detail %} 257 | 258 | {{ detail }}{% endif %} 259 | ttl: !input ttl 260 | - alias: Prepare success response for Assist 261 | variables: 262 | response: 263 | success: | 264 | Sent the {{ content_type }} "{{ summary }}" to Zalo (thread 265 | {{ default_thread_id | default('unspecified') }}) successfully. 266 | - stop: Finish and return response data 267 | response_variable: response 268 | --------------------------------------------------------------------------------