├── .gitignore ├── casita ├── error_320_240.png ├── idle_320_240.png ├── loading_320_240.png ├── listening_320_240.png ├── replying_320_240.png ├── thinking_320_240.png └── timer_finished_320_240.png ├── sounds └── timer_finished.wav ├── error_box_illustrations ├── error-no-ha.png └── error-no-wifi.png ├── .github ├── dependabot.yml ├── workflows │ ├── yaml-lint.yml │ ├── lock.yml │ ├── stale.yml │ ├── build-minimal.yml │ └── build.yml └── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.yml ├── README.md ├── .yamllint ├── .devcontainer └── devcontainer.json ├── m5stack-atom-echo ├── m5stack-atom-echo.factory.yaml ├── m5stack-atom-echo.minimal.factory.yaml └── m5stack-atom-echo.yaml ├── esp32-s3-box ├── esp32-s3-box.factory.yaml └── esp32-s3-box.yaml ├── esp32-s3-box-3 ├── esp32-s3-box-3.factory.yaml └── esp32-s3-box-3.yaml ├── esp32-s3-box-lite ├── esp32-s3-box-lite.factory.yaml └── esp32-s3-box-lite.yaml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .esphome/ 2 | .DS_Store 3 | secrets.yaml 4 | */.gitignore 5 | !/.gitignore 6 | venv/ 7 | -------------------------------------------------------------------------------- /casita/error_320_240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esphome/wake-word-voice-assistants/HEAD/casita/error_320_240.png -------------------------------------------------------------------------------- /casita/idle_320_240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esphome/wake-word-voice-assistants/HEAD/casita/idle_320_240.png -------------------------------------------------------------------------------- /casita/loading_320_240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esphome/wake-word-voice-assistants/HEAD/casita/loading_320_240.png -------------------------------------------------------------------------------- /sounds/timer_finished.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esphome/wake-word-voice-assistants/HEAD/sounds/timer_finished.wav -------------------------------------------------------------------------------- /casita/listening_320_240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esphome/wake-word-voice-assistants/HEAD/casita/listening_320_240.png -------------------------------------------------------------------------------- /casita/replying_320_240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esphome/wake-word-voice-assistants/HEAD/casita/replying_320_240.png -------------------------------------------------------------------------------- /casita/thinking_320_240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esphome/wake-word-voice-assistants/HEAD/casita/thinking_320_240.png -------------------------------------------------------------------------------- /casita/timer_finished_320_240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esphome/wake-word-voice-assistants/HEAD/casita/timer_finished_320_240.png -------------------------------------------------------------------------------- /error_box_illustrations/error-no-ha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esphome/wake-word-voice-assistants/HEAD/error_box_illustrations/error-no-ha.png -------------------------------------------------------------------------------- /error_box_illustrations/error-no-wifi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esphome/wake-word-voice-assistants/HEAD/error_box_illustrations/error-no-wifi.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESPHome firmwares 2 | 3 | This repo holds the source of various firmwares used for installing ESPHome onto devices with [esphome/esp-web-tools](https://github.com/esphome/esp-web-tools). 4 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | extends: default 4 | 5 | ignore-from-file: .gitignore 6 | 7 | rules: 8 | document-start: disable 9 | empty-lines: 10 | level: error 11 | max: 1 12 | max-start: 0 13 | max-end: 1 14 | indentation: 15 | level: error 16 | spaces: 2 17 | indent-sequences: true 18 | check-multi-line-strings: false 19 | line-length: disable 20 | truthy: disable 21 | -------------------------------------------------------------------------------- /.github/workflows/yaml-lint.yml: -------------------------------------------------------------------------------- 1 | name: YAML lint 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - "**.yaml" 8 | - "**.yml" 9 | pull_request: 10 | paths: 11 | - "**.yaml" 12 | - "**.yml" 13 | 14 | jobs: 15 | yamllint: 16 | name: yamllint 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Check out code from GitHub 20 | uses: actions/checkout@v6.0.1 21 | - name: Run yamllint 22 | run: yamllint --strict . 23 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ESPHome - Firmware", 3 | "image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.10", 4 | "postCreateCommand": "pip3 install esphome", 5 | "runArgs": ["--device=/dev/bus/usb", "--privileged"], 6 | "features": { 7 | "ghcr.io/devcontainers/features/github-cli": {} 8 | }, 9 | "customizations": { 10 | "vscode": { 11 | "settings": { 12 | "esphome.validator": "local" 13 | }, 14 | "extensions": ["ESPHome.esphome-vscode"] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | name: Lock 2 | 3 | on: 4 | schedule: 5 | - cron: "0 19 * * *" 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | concurrency: 13 | group: lock 14 | 15 | jobs: 16 | lock: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: dessant/lock-threads@v5.0.1 20 | with: 21 | pr-inactive-days: "1" 22 | pr-lock-reason: "" 23 | exclude-any-pr-labels: keep-open 24 | 25 | issue-inactive-days: "1" 26 | issue-lock-reason: "" 27 | exclude-any-issue-labels: keep-open 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Issue after taking control of the firmware provided here 4 | url: https://github.com/esphome/issues/issues 5 | about: |- 6 | Please search for or create an issue in the main ESPHome issue tracker 7 | if you have taken control/adpted your device in the ESPHome Builder. 8 | - name: Feature Request / Device Request 9 | url: https://github.com/esphome/wake-word-voice-assistants/discussions/categories/feature-requests 10 | about: |- 11 | Create a discussion here if you have a feature request or device request. 12 | -------------------------------------------------------------------------------- /m5stack-atom-echo/m5stack-atom-echo.factory.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | m5stack-atom-echo: !include m5stack-atom-echo.yaml 3 | 4 | esphome: 5 | project: 6 | name: m5stack.atom-echo-wake-word-voice-assistant 7 | version: dev 8 | 9 | ota: 10 | - platform: http_request 11 | id: ota_http_request 12 | 13 | update: 14 | - platform: http_request 15 | id: update_http_request 16 | name: Firmware 17 | source: https://firmware.esphome.io/wake-word-voice-assistant/m5stack-atom-echo/manifest.json 18 | 19 | http_request: 20 | 21 | dashboard_import: 22 | package_import_url: github://esphome/wake-word-voice-assistants/m5stack-atom-echo/m5stack-atom-echo.yaml@main 23 | 24 | improv_serial: 25 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues' 2 | on: 3 | schedule: 4 | - cron: '50 18 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v9.1.0 11 | with: 12 | stale-issue-message: 'As there has been no activity on this issue for 30 days, I am marking it as stale. If you think this is a mistake, please comment below and I will remove the stale label.' 13 | close-issue-message: 'This issue has been closed due to inactivity. If you think this is a mistake, please comment below.' 14 | days-before-stale: 30 15 | days-before-close: 5 16 | stale-issue-label: 'stale' 17 | exempt-issue-labels: 'not-stale' 18 | -------------------------------------------------------------------------------- /esp32-s3-box/esp32-s3-box.factory.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | packages: 3 | # This is an inline package to prefix the on_client_connected with the wait_until action 4 | # It must appear before the actual package so it becomes the orignal config and the 5 | # on_client_connected list from the package config is appends onto this one. 6 | va_connected_wait_for_ble: 7 | voice_assistant: 8 | on_client_connected: 9 | - wait_until: 10 | not: ble.enabled 11 | esp32-s3-box: !include esp32-s3-box.yaml 12 | 13 | esphome: 14 | project: 15 | name: esphome.voice-assistant 16 | version: dev 17 | 18 | ota: 19 | - platform: http_request 20 | id: ota_http_request 21 | 22 | update: 23 | - platform: http_request 24 | id: update_http_request 25 | name: Firmware 26 | source: https://firmware.esphome.io/wake-word-voice-assistant/esp32-s3-box/manifest.json 27 | 28 | http_request: 29 | 30 | dashboard_import: 31 | package_import_url: github://esphome/wake-word-voice-assistants/esp32-s3-box.yaml@main 32 | 33 | wifi: 34 | on_connect: 35 | - delay: 5s # Gives time for improv results to be transmitted 36 | - ble.disable: 37 | on_disconnect: 38 | - ble.enable: 39 | 40 | improv_serial: 41 | 42 | esp32_improv: 43 | authorizer: none 44 | -------------------------------------------------------------------------------- /esp32-s3-box-3/esp32-s3-box-3.factory.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | packages: 3 | # This is an inline package to prefix the on_client_connected with the wait_until action 4 | # It must appear before the actual package so it becomes the orignal config and the 5 | # on_client_connected list from the package config is appends onto this one. 6 | va_connected_wait_for_ble: 7 | voice_assistant: 8 | on_client_connected: 9 | - wait_until: 10 | not: ble.enabled 11 | esp32-s3-box-3: !include esp32-s3-box-3.yaml 12 | 13 | esphome: 14 | project: 15 | name: esphome.voice-assistant 16 | version: dev 17 | 18 | ota: 19 | - platform: http_request 20 | id: ota_http_request 21 | 22 | update: 23 | - platform: http_request 24 | id: update_http_request 25 | name: Firmware 26 | source: https://firmware.esphome.io/wake-word-voice-assistant/esp32-s3-box-3/manifest.json 27 | 28 | http_request: 29 | 30 | dashboard_import: 31 | package_import_url: github://esphome/wake-word-voice-assistants/esp32-s3-box-3/esp32-s3-box-3.yaml@main 32 | 33 | wifi: 34 | on_connect: 35 | - delay: 5s # Gives time for improv results to be transmitted 36 | - ble.disable: 37 | on_disconnect: 38 | - ble.enable: 39 | 40 | improv_serial: 41 | 42 | esp32_improv: 43 | authorizer: none 44 | -------------------------------------------------------------------------------- /esp32-s3-box-lite/esp32-s3-box-lite.factory.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | packages: 3 | # This is an inline package to prefix the on_client_connected with the wait_until action 4 | # It must appear before the actual package so it becomes the orignal config and the 5 | # on_client_connected list from the package config is appends onto this one. 6 | va_connected_wait_for_ble: 7 | voice_assistant: 8 | on_client_connected: 9 | - wait_until: 10 | not: ble.enabled 11 | esp32-s3-box-lite: !include esp32-s3-box-lite.yaml 12 | 13 | esphome: 14 | project: 15 | name: esphome.voice-assistant 16 | version: dev 17 | 18 | ota: 19 | - platform: http_request 20 | id: ota_http_request 21 | 22 | update: 23 | - platform: http_request 24 | id: update_http_request 25 | name: Firmware 26 | source: https://firmware.esphome.io/wake-word-voice-assistant/esp32-s3-box-lite/manifest.json 27 | 28 | http_request: 29 | 30 | dashboard_import: 31 | package_import_url: github://esphome/wake-word-voice-assistants/esp32-s3-box-lite/esp32-s3-box-lite.yaml@main 32 | 33 | wifi: 34 | on_connect: 35 | - delay: 5s # Gives time for improv results to be transmitted 36 | - ble.disable: 37 | on_disconnect: 38 | - ble.enable: 39 | 40 | improv_serial: 41 | 42 | esp32_improv: 43 | authorizer: none 44 | -------------------------------------------------------------------------------- /.github/workflows/build-minimal.yml: -------------------------------------------------------------------------------- 1 | name: Build minimal 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.ref }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | build-firmware: 13 | name: Build Firmware 14 | uses: esphome/workflows/.github/workflows/build.yml@2025.10.0 15 | with: 16 | files: | 17 | m5stack-atom-echo/m5stack-atom-echo.minimal.factory.yaml 18 | esphome-version: 2025.11.3 19 | release-summary: ${{ github.event_name == 'release' && github.event.release.body || '' }} 20 | release-url: ${{ github.event_name == 'release' && github.event.release.html_url || '' }} 21 | release-version: ${{ github.event_name == 'release' && github.event.release.tag_name || '' }} 22 | 23 | upload-to-release: 24 | name: Upload to Release 25 | runs-on: ubuntu-latest 26 | needs: 27 | - build-firmware 28 | steps: 29 | - name: Download Artifact 30 | uses: actions/download-artifact@v6.0.0 31 | with: 32 | path: files 33 | 34 | - name: Copy file to output 35 | run: |- 36 | mkdir output 37 | version="${{ github.event.release.tag_name }}" 38 | cd "files/$version" 39 | cp m5stack-atom-echo-esp32.factory.bin ../../output/m5stack-atom-echo.minimal.factory.bin 40 | md5sum m5stack-atom-echo-esp32.factory.bin | head -c 32 > ../../output/m5stack-atom-echo.minimal.factory.bin.md5 41 | 42 | - name: Upload files to release 43 | uses: softprops/action-gh-release@v2.5.0 44 | with: 45 | files: output/* 46 | tag_name: ${{ github.event.release.tag_name }} 47 | -------------------------------------------------------------------------------- /m5stack-atom-echo/m5stack-atom-echo.minimal.factory.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | m5stack-atom-echo: !include m5stack-atom-echo.yaml 3 | 4 | esphome: 5 | name: m5stack-atom-echo 6 | project: 7 | name: m5stack.atom-echo-wake-word-voice-assistant 8 | version: 25.0.0 # This firmware is a first step and devices should be OTA updated to the latest firmware on first use 9 | on_boot: 10 | - light.turn_on: 11 | id: led 12 | effect: Rainbow 13 | 14 | ota: 15 | - platform: http_request 16 | id: ota_http_request 17 | 18 | update: 19 | - platform: http_request 20 | id: update_http_request 21 | name: Firmware 22 | source: https://firmware.esphome.io/wake-word-voice-assistant/m5stack-atom-echo/manifest.json 23 | 24 | http_request: 25 | timeout: 10s 26 | watchdog_timeout: 15s 27 | 28 | dashboard_import: 29 | package_import_url: github://esphome/wake-word-voice-assistants/m5stack-atom-echo/m5stack-atom-echo.yaml@main 30 | 31 | improv_serial: 32 | 33 | esp32_improv: 34 | authorizer: none 35 | 36 | wifi: 37 | on_connect: 38 | - component.update: update_http_request 39 | - delay: 2s 40 | - ble.disable: 41 | 42 | light: 43 | - id: !extend led 44 | effects: 45 | - addressable_rainbow: 46 | name: Rainbow 47 | 48 | # The below is to make this binary smaller 49 | 50 | binary_sensor: 51 | - id: !extend echo_button 52 | on_multi_click: !remove 53 | on_press: 54 | - delay: 10s 55 | - if: 56 | condition: 57 | binary_sensor.is_on: echo_button 58 | then: 59 | - button.press: factory_reset_btn 60 | media_player: 61 | - id: !extend echo_media_player 62 | files: !remove 63 | on_announcement: !remove 64 | on_idle: !remove 65 | 66 | script: !remove 67 | 68 | micro_wake_word: !remove 69 | select: !remove 70 | switch: !remove 71 | voice_assistant: 72 | micro_wake_word: !remove 73 | on_listening: !remove 74 | on_stt_vad_end: !remove 75 | on_tts_start: !remove 76 | on_end: !remove 77 | on_error: !remove 78 | on_client_connected: !remove 79 | on_client_disconnected: !remove 80 | on_timer_finished: !remove 81 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: ESPHome Wake Word Voice Assistants Bug Report 2 | description: |- 3 | Report an issue with the ESPHome Wake Word Voice Assistant firmware provided by this repository, 4 | the web installer and the provided OTA updates. 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | This issue form is for reporting bugs with the ESPHome Wake Word Voice Assistant firmware that is compiled and released 10 | from this repository. This includes flashing the firmware from the ESPHome projects page and the OTA updates that are provided. 11 | 12 | If you have taken control of the device in the ESPHome Builder, please report the issue in the main [ESPHome issue tracker][issues]. 13 | 14 | [issues]: https://github.com/esphome/issues 15 | 16 | - type: textarea 17 | validations: 18 | required: true 19 | id: problem 20 | attributes: 21 | label: The problem 22 | description: >- 23 | Describe the issue you are experiencing here to communicate to the 24 | maintainers. Tell us what you were trying to do and what happened. 25 | 26 | Provide a clear and concise description of what the problem is. 27 | 28 | - type: dropdown 29 | validations: 30 | required: true 31 | id: device 32 | attributes: 33 | label: Which device are you using? 34 | options: 35 | - esp32-s3-box 36 | - esp32-s3-box-lite 37 | - esp32-s3-box-3 38 | - m5stack-atom-echo 39 | 40 | - type: input 41 | id: version 42 | validations: 43 | required: true 44 | attributes: 45 | label: Firmware Version 46 | description: > 47 | The version of the installed version. This is **NOT** the ESPHome version, but is in the format `25.2.1` for example. 48 | 49 | - type: textarea 50 | id: logs 51 | validations: 52 | required: true 53 | attributes: 54 | label: Logs 55 | description: Serial logs from the USB port are required 56 | render: txt 57 | 58 | - type: textarea 59 | id: additional 60 | attributes: 61 | label: Additional information 62 | description: > 63 | If you have any additional information for us, use the field below. 64 | Please note, you can attach screenshots or screen recordings here, by 65 | dragging and dropping files in the field below. 66 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | release: 7 | types: [published] 8 | schedule: 9 | - cron: '0 0 * * 0' 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | build-firmware: 17 | name: Build Firmware 18 | uses: esphome/workflows/.github/workflows/build.yml@2025.10.0 19 | with: 20 | files: | 21 | esp32-s3-box/esp32-s3-box.factory.yaml 22 | esp32-s3-box-lite/esp32-s3-box-lite.factory.yaml 23 | esp32-s3-box-3/esp32-s3-box-3.factory.yaml 24 | m5stack-atom-echo/m5stack-atom-echo.factory.yaml 25 | esphome-version: 2025.11.3 26 | release-summary: ${{ github.event_name == 'release' && github.event.release.body || '' }} 27 | release-url: ${{ github.event_name == 'release' && github.event.release.html_url || '' }} 28 | release-version: ${{ github.event_name == 'release' && github.event.release.tag_name || '' }} 29 | 30 | build-minimal-firmware: 31 | name: Build Atom Echo Minimal Firmware 32 | uses: esphome/workflows/.github/workflows/build.yml@2025.10.0 33 | if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' 34 | with: 35 | files: | 36 | m5stack-atom-echo/m5stack-atom-echo.minimal.factory.yaml 37 | combined-name: m5stack-atom-echo-minimal 38 | esphome-version: 2025.11.3 39 | release-summary: ${{ github.event_name == 'release' && github.event.release.body || '' }} 40 | release-url: ${{ github.event_name == 'release' && github.event.release.html_url || '' }} 41 | release-version: ${{ github.event_name == 'release' && github.event.release.tag_name || '' }} 42 | 43 | upload-to-r2: 44 | if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') 45 | name: Upload to R2 46 | needs: 47 | - build-firmware 48 | uses: esphome/workflows/.github/workflows/upload-to-r2.yml@2025.10.0 49 | with: 50 | directory: wake-word-voice-assistant 51 | secrets: inherit 52 | 53 | upload-to-release: 54 | name: Upload to Release 55 | if: github.event_name == 'release' 56 | uses: esphome/workflows/.github/workflows/upload-to-gh-release.yml@2025.8.1 57 | needs: 58 | - build-firmware 59 | with: 60 | version: ${{ github.event.release.tag_name }} 61 | 62 | promote-beta: 63 | name: Promote to Beta 64 | if: github.event_name == 'release' 65 | uses: esphome/workflows/.github/workflows/promote-r2.yml@2025.8.1 66 | needs: 67 | - upload-to-r2 68 | with: 69 | version: ${{ github.event.release.tag_name }} 70 | directory: wake-word-voice-assistant 71 | channel: beta 72 | manifest-filename: manifest-beta.json 73 | secrets: inherit 74 | 75 | promote-prod: 76 | name: Promote to Production 77 | if: github.event_name == 'release' && github.event.release.prerelease == false 78 | uses: esphome/workflows/.github/workflows/promote-r2.yml@2025.8.1 79 | needs: 80 | - upload-to-r2 81 | with: 82 | version: ${{ github.event.release.tag_name }} 83 | directory: wake-word-voice-assistant 84 | channel: production 85 | manifest-filename: manifest.json 86 | secrets: inherit 87 | -------------------------------------------------------------------------------- /m5stack-atom-echo/m5stack-atom-echo.yaml: -------------------------------------------------------------------------------- 1 | substitutions: 2 | name: m5stack-atom-echo 3 | friendly_name: M5Stack Atom Echo 4 | 5 | esphome: 6 | name: ${name} 7 | name_add_mac_suffix: true 8 | friendly_name: ${friendly_name} 9 | min_version: 2025.5.0 10 | 11 | esp32: 12 | board: m5stack-atom 13 | cpu_frequency: 240MHz 14 | framework: 15 | type: esp-idf 16 | 17 | logger: 18 | api: 19 | 20 | ota: 21 | - platform: esphome 22 | id: ota_esphome 23 | 24 | wifi: 25 | ap: 26 | 27 | captive_portal: 28 | 29 | button: 30 | - platform: factory_reset 31 | id: factory_reset_btn 32 | name: Factory reset 33 | 34 | i2s_audio: 35 | - id: i2s_audio_bus 36 | i2s_lrclk_pin: GPIO33 37 | i2s_bclk_pin: GPIO19 38 | 39 | microphone: 40 | - platform: i2s_audio 41 | id: echo_microphone 42 | i2s_din_pin: GPIO23 43 | adc_type: external 44 | pdm: true 45 | sample_rate: 16000 46 | correct_dc_offset: true 47 | 48 | speaker: 49 | - platform: i2s_audio 50 | id: echo_speaker 51 | i2s_dout_pin: GPIO22 52 | dac_type: external 53 | bits_per_sample: 16bit 54 | sample_rate: 16000 55 | channel: stereo # The Echo has poor playback audio quality when using mon audio 56 | buffer_duration: 60ms 57 | 58 | media_player: 59 | - platform: speaker 60 | name: None 61 | id: echo_media_player 62 | announcement_pipeline: 63 | speaker: echo_speaker 64 | format: WAV 65 | codec_support_enabled: false 66 | buffer_size: 6000 67 | volume_min: 0.4 68 | files: 69 | - id: timer_finished_wave_file 70 | file: https://github.com/esphome/wake-word-voice-assistants/raw/main/sounds/timer_finished.wav 71 | on_announcement: 72 | - if: 73 | condition: 74 | - microphone.is_capturing: 75 | then: 76 | - script.execute: stop_wake_word 77 | - light.turn_on: 78 | id: led 79 | blue: 100% 80 | red: 0% 81 | green: 0% 82 | brightness: 100% 83 | effect: none 84 | on_idle: 85 | - script.execute: start_wake_word 86 | - script.execute: reset_led 87 | 88 | voice_assistant: 89 | id: va 90 | micro_wake_word: 91 | microphone: 92 | microphone: echo_microphone 93 | channels: 0 94 | gain_factor: 4 95 | media_player: echo_media_player 96 | noise_suppression_level: 2 97 | auto_gain: 31dBFS 98 | on_listening: 99 | - light.turn_on: 100 | id: led 101 | blue: 100% 102 | red: 0% 103 | green: 0% 104 | effect: "Slow Pulse" 105 | on_stt_vad_end: 106 | - light.turn_on: 107 | id: led 108 | blue: 100% 109 | red: 0% 110 | green: 0% 111 | effect: "Fast Pulse" 112 | on_tts_start: 113 | - light.turn_on: 114 | id: led 115 | blue: 100% 116 | red: 0% 117 | green: 0% 118 | brightness: 100% 119 | effect: none 120 | on_end: 121 | # Handle the "nevermind" case where there is no announcement 122 | - wait_until: 123 | condition: 124 | - media_player.is_announcing: 125 | timeout: 0.5s 126 | # Restart only mWW if enabled; streaming wake words automatically restart 127 | - if: 128 | condition: 129 | - lambda: |- 130 | return strcmp(id(wake_word_engine_location).current_option(), "On device") == 0; 131 | then: 132 | - wait_until: 133 | - and: 134 | - not: 135 | voice_assistant.is_running: 136 | - not: 137 | speaker.is_playing: 138 | - lambda: id(va).set_use_wake_word(false); 139 | - micro_wake_word.start: 140 | - script.execute: reset_led 141 | on_error: 142 | - light.turn_on: 143 | id: led 144 | red: 100% 145 | green: 0% 146 | blue: 0% 147 | brightness: 100% 148 | effect: none 149 | - delay: 2s 150 | - script.execute: reset_led 151 | on_client_connected: 152 | - delay: 2s # Give the api server time to settle 153 | - script.execute: start_wake_word 154 | on_client_disconnected: 155 | - script.execute: stop_wake_word 156 | on_timer_finished: 157 | - script.execute: stop_wake_word 158 | - wait_until: 159 | not: 160 | microphone.is_capturing: 161 | - switch.turn_on: timer_ringing 162 | - light.turn_on: 163 | id: led 164 | red: 0% 165 | green: 100% 166 | blue: 0% 167 | brightness: 100% 168 | effect: "Fast Pulse" 169 | - wait_until: 170 | - switch.is_off: timer_ringing 171 | - light.turn_off: led 172 | - switch.turn_off: timer_ringing 173 | 174 | binary_sensor: 175 | # button does the following: 176 | # short click - stop a timer 177 | # if no timer then restart either microwakeword or voice assistant continuous 178 | - platform: gpio 179 | pin: 180 | number: GPIO39 181 | inverted: true 182 | name: Button 183 | disabled_by_default: true 184 | entity_category: diagnostic 185 | id: echo_button 186 | on_multi_click: 187 | - timing: 188 | - ON for at least 50ms 189 | - OFF for at least 50ms 190 | then: 191 | - if: 192 | condition: 193 | switch.is_on: timer_ringing 194 | then: 195 | - switch.turn_off: timer_ringing 196 | else: 197 | - script.execute: start_wake_word 198 | - timing: 199 | - ON for at least 10s 200 | then: 201 | - button.press: factory_reset_btn 202 | 203 | light: 204 | - platform: esp32_rmt_led_strip 205 | id: led 206 | name: None 207 | disabled_by_default: true 208 | entity_category: config 209 | pin: GPIO27 210 | default_transition_length: 0s 211 | chipset: SK6812 212 | num_leds: 1 213 | rgb_order: grb 214 | effects: 215 | - pulse: 216 | name: "Slow Pulse" 217 | transition_length: 250ms 218 | update_interval: 250ms 219 | min_brightness: 50% 220 | max_brightness: 100% 221 | - pulse: 222 | name: "Fast Pulse" 223 | transition_length: 100ms 224 | update_interval: 100ms 225 | min_brightness: 50% 226 | max_brightness: 100% 227 | 228 | script: 229 | - id: reset_led 230 | then: 231 | - if: 232 | condition: 233 | - lambda: |- 234 | return strcmp(id(wake_word_engine_location).current_option(), "On device") == 0; 235 | - switch.is_on: use_listen_light 236 | then: 237 | - light.turn_on: 238 | id: led 239 | red: 100% 240 | green: 89% 241 | blue: 71% 242 | brightness: 60% 243 | effect: none 244 | else: 245 | - if: 246 | condition: 247 | - lambda: |- 248 | return strcmp(id(wake_word_engine_location).current_option(), "On device") == 0; 249 | - switch.is_on: use_listen_light 250 | then: 251 | - light.turn_on: 252 | id: led 253 | red: 0% 254 | green: 100% 255 | blue: 100% 256 | brightness: 60% 257 | effect: none 258 | else: 259 | - light.turn_off: led 260 | - id: start_wake_word 261 | then: 262 | - if: 263 | condition: 264 | and: 265 | - not: 266 | - voice_assistant.is_running: 267 | - lambda: |- 268 | return strcmp(id(wake_word_engine_location).current_option(), "On device") == 0; 269 | then: 270 | - lambda: id(va).set_use_wake_word(false); 271 | - micro_wake_word.start: 272 | - if: 273 | condition: 274 | and: 275 | - not: 276 | - voice_assistant.is_running: 277 | - lambda: |- 278 | return strcmp(id(wake_word_engine_location).current_option(), "In Home Assistant") == 0; 279 | then: 280 | - lambda: id(va).set_use_wake_word(true); 281 | - voice_assistant.start_continuous: 282 | - id: stop_wake_word 283 | then: 284 | - if: 285 | condition: 286 | lambda: |- 287 | return strcmp(id(wake_word_engine_location).current_option(), "In Home Assistant") == 0; 288 | then: 289 | - lambda: id(va).set_use_wake_word(false); 290 | - voice_assistant.stop: 291 | - if: 292 | condition: 293 | lambda: |- 294 | return strcmp(id(wake_word_engine_location).current_option(), "On device") == 0; 295 | then: 296 | - micro_wake_word.stop: 297 | 298 | switch: 299 | - platform: template 300 | name: Use listen light 301 | id: use_listen_light 302 | optimistic: true 303 | restore_mode: RESTORE_DEFAULT_ON 304 | entity_category: config 305 | on_turn_on: 306 | - script.execute: reset_led 307 | on_turn_off: 308 | - script.execute: reset_led 309 | - platform: template 310 | id: timer_ringing 311 | optimistic: true 312 | restore_mode: ALWAYS_OFF 313 | on_turn_off: 314 | # Turn off the repeat mode and disable the pause between playlist items 315 | - lambda: |- 316 | id(echo_media_player) 317 | ->make_call() 318 | .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_OFF) 319 | .set_announcement(true) 320 | .perform(); 321 | id(echo_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 0); 322 | # Stop playing the alarm 323 | - media_player.stop: 324 | announcement: true 325 | on_turn_on: 326 | # Turn on the repeat mode and pause for 1000 ms between playlist items/repeats 327 | - lambda: |- 328 | id(echo_media_player) 329 | ->make_call() 330 | .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ONE) 331 | .set_announcement(true) 332 | .perform(); 333 | id(echo_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 1000); 334 | - media_player.speaker.play_on_device_media_file: 335 | media_file: timer_finished_wave_file 336 | announcement: true 337 | - delay: 15min 338 | - switch.turn_off: timer_ringing 339 | 340 | select: 341 | - platform: template 342 | entity_category: config 343 | name: Wake word engine location 344 | id: wake_word_engine_location 345 | optimistic: true 346 | restore_value: true 347 | options: 348 | - In Home Assistant 349 | - On device 350 | initial_option: On device 351 | on_value: 352 | - if: 353 | condition: 354 | lambda: return x == "In Home Assistant"; 355 | then: 356 | - micro_wake_word.stop: 357 | - delay: 500ms 358 | - lambda: id(va).set_use_wake_word(true); 359 | - voice_assistant.start_continuous: 360 | - if: 361 | condition: 362 | lambda: return x == "On device"; 363 | then: 364 | - lambda: id(va).set_use_wake_word(false); 365 | - voice_assistant.stop: 366 | - delay: 500ms 367 | - micro_wake_word.start: 368 | 369 | micro_wake_word: 370 | on_wake_word_detected: 371 | - voice_assistant.start: 372 | wake_word: !lambda return wake_word; 373 | vad: 374 | models: 375 | - model: okay_nabu 376 | - model: hey_mycroft 377 | - model: hey_jarvis 378 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /esp32-s3-box/esp32-s3-box.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | substitutions: 3 | name: esp32-s3-box 4 | friendly_name: ESP32 S3 Box 5 | loading_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/loading_320_240.png 6 | idle_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/idle_320_240.png 7 | listening_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/listening_320_240.png 8 | thinking_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/thinking_320_240.png 9 | replying_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/replying_320_240.png 10 | error_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/error_320_240.png 11 | timer_finished_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/timer_finished_320_240.png 12 | 13 | loading_illustration_background_color: "000000" 14 | idle_illustration_background_color: "000000" 15 | listening_illustration_background_color: "FFFFFF" 16 | thinking_illustration_background_color: "FFFFFF" 17 | replying_illustration_background_color: "FFFFFF" 18 | error_illustration_background_color: "000000" 19 | 20 | voice_assist_idle_phase_id: "1" 21 | voice_assist_listening_phase_id: "2" 22 | voice_assist_thinking_phase_id: "3" 23 | voice_assist_replying_phase_id: "4" 24 | voice_assist_not_ready_phase_id: "10" 25 | voice_assist_error_phase_id: "11" 26 | voice_assist_muted_phase_id: "12" 27 | voice_assist_timer_finished_phase_id: "20" 28 | 29 | # These unique characters have been extracted from every test file of every language available on https://github.com/home-assistant/intents (14 March 2024) 30 | # However, the Figtree font only contains Latin characters, so there is no point using this... unlessyou change the font configuration accordingly. 31 | allowed_characters: " !#%'()+,-./0123456789:;<>?@ABCDEFGHIJKLMNOPQRSTUVWYZ[]_abcdefghijklmnopqrstuvwxyz{|}°²³µ¿ÁÂÄÅÉÖÚßàáâãäåæçèéêëìíîðñòóôõöøùúûüýþāăąćčďĐđēėęěğĮįıļľŁłńňőřśšťũūůűųźŻżŽžơưșțΆΈΌΐΑΒΓΔΕΖΗΘΚΜΝΠΡΣΤΥΦάέήίαβγδεζηθικλμνξοπρςστυφχψωϊόύώАБВГДЕЖЗИКЛМНОПРСТУХЦЧШЪЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяёђєіїјљњћאבגדהוזחטיכלםמןנסעפץצקרשת،ءآأإئابةتجحخدذرزسشصضطظعغفقكلمنهوىيٹپچڈکگںھہیےংকচতধনফবযরলশষস়ািু্చయలిెొ్ംഅആഇഈഉഎഓകഗങചജഞടഡണതദധനപഫബഭമയരറലളവശസഹാിീുൂെേൈ്ൺൻർൽൾაბგდევზთილმნოპრსტუფქყშჩცძჭხạảấầẩậắặẹẽếềểệỉịọỏốồổỗộớờởợụủứừửữựỳ—、一上不个中为主乾了些亮人任低佔何作供依侧係個側偵充光入全关冇冷几切到制前動區卧厅厨及口另右吊后吗启吸呀咗哪唔問啟嗎嘅嘛器圍在场執場外多大始安定客室家密寵对將小少左已帘常幫幾库度庫廊廚廳开式後恆感態成我戲戶户房所扇手打执把拔换掉控插摄整斯新明是景暗更最會有未本模機檯櫃欄次正氏水沒没洗活派温測源溫漏潮激濕灯為無煙照熱燈燥物狀玄现現瓦用發的盞目着睡私空窗立笛管節簾籬紅線红罐置聚聲脚腦腳臥色节著行衣解設調請謝警设调走路車车运連遊運過道邊部都量鎖锁門閂閉開關门闭除隱離電震霧面音頂題顏颜風风食餅餵가간감갔강개거게겨결경고공과관그금급기길깥꺼껐꼽나난내네놀누는능니다닫담대더데도동됐되된됨둡드든등디때떤뜨라래러렇렌려로료른를리림링마많명몇모무문물뭐바밝방배변보부불블빨뽑사산상색서설성세센션소쇼수스습시신실싱아안않알았애야어얼업없었에여연열옆오온완외왼요운움워원위으은을음의이인일임입있작잠장재전절정제져조족종주줄중줘지직진짐쪽차창천최추출충치침커컴켜켰쿠크키탁탄태탬터텔통트튼티파팬퍼폰표퓨플핑한함해했행혀현화활후휴힘,?" 32 | 33 | # Add support for non-unicode characters by using better glyphset 34 | font_glyphsets: "GF_Latin_Core" 35 | # for Greek use "Noto Sans" for other languages use a compatible font family 36 | font_family: Figtree 37 | 38 | esphome: 39 | name: ${name} 40 | friendly_name: ${friendly_name} 41 | min_version: 2025.5.0 42 | name_add_mac_suffix: true 43 | on_boot: 44 | priority: 600 45 | then: 46 | - script.execute: draw_display 47 | - delay: 30s 48 | - if: 49 | condition: 50 | lambda: return id(init_in_progress); 51 | then: 52 | - lambda: id(init_in_progress) = false; 53 | - script.execute: draw_display 54 | 55 | esp32: 56 | board: esp32s3box 57 | flash_size: 16MB 58 | cpu_frequency: 240MHz 59 | framework: 60 | type: esp-idf 61 | sdkconfig_options: 62 | CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y" 63 | CONFIG_ESP32S3_DATA_CACHE_64KB: "y" 64 | CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y" 65 | 66 | psram: 67 | mode: octal 68 | speed: 80MHz 69 | 70 | api: 71 | on_client_connected: 72 | - script.execute: draw_display 73 | on_client_disconnected: 74 | - script.execute: draw_display 75 | 76 | ota: 77 | - platform: esphome 78 | id: ota_esphome 79 | 80 | logger: 81 | hardware_uart: USB_SERIAL_JTAG 82 | 83 | wifi: 84 | ap: 85 | on_connect: 86 | - script.execute: draw_display 87 | on_disconnect: 88 | - script.execute: draw_display 89 | 90 | captive_portal: 91 | 92 | button: 93 | - platform: factory_reset 94 | id: factory_reset_btn 95 | internal: true 96 | 97 | binary_sensor: 98 | - platform: gpio 99 | pin: 100 | number: GPIO0 101 | mode: INPUT_PULLUP 102 | inverted: true 103 | id: left_top_button 104 | internal: true 105 | on_multi_click: 106 | - timing: 107 | - ON for at least 50ms 108 | - OFF for at least 50ms 109 | then: 110 | - switch.turn_off: timer_ringing 111 | - timing: 112 | - ON for at least 10s 113 | then: 114 | - button.press: factory_reset_btn 115 | 116 | output: 117 | - platform: ledc 118 | pin: GPIO45 119 | id: backlight_output 120 | 121 | light: 122 | - platform: monochromatic 123 | id: led 124 | name: Screen 125 | icon: "mdi:television" 126 | entity_category: config 127 | output: backlight_output 128 | restore_mode: RESTORE_DEFAULT_ON 129 | default_transition_length: 250ms 130 | 131 | i2c: 132 | scl: GPIO18 133 | sda: GPIO8 134 | 135 | i2s_audio: 136 | - id: i2s_audio_bus 137 | i2s_lrclk_pin: GPIO47 138 | i2s_bclk_pin: GPIO17 139 | i2s_mclk_pin: GPIO2 140 | 141 | audio_adc: 142 | - platform: es7210 143 | id: es7210_adc 144 | bits_per_sample: 16bit 145 | sample_rate: 16000 146 | 147 | audio_dac: 148 | - platform: es8311 149 | id: es8311_dac 150 | bits_per_sample: 16bit 151 | sample_rate: 48000 152 | 153 | microphone: 154 | - platform: i2s_audio 155 | id: box_mic 156 | sample_rate: 16000 157 | i2s_din_pin: GPIO16 158 | bits_per_sample: 16bit 159 | adc_type: external 160 | 161 | speaker: 162 | - platform: i2s_audio 163 | id: box_speaker 164 | i2s_dout_pin: GPIO15 165 | dac_type: external 166 | sample_rate: 48000 167 | bits_per_sample: 16bit 168 | channel: left 169 | audio_dac: es8311_dac 170 | buffer_duration: 100ms 171 | 172 | media_player: 173 | - platform: speaker 174 | name: None 175 | id: speaker_media_player 176 | volume_min: 0.5 177 | volume_max: 0.8 178 | announcement_pipeline: 179 | speaker: box_speaker 180 | format: FLAC 181 | sample_rate: 48000 182 | num_channels: 1 # S3 Box only has one output channel 183 | files: 184 | - id: timer_finished_sound 185 | file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/timer_finished.flac 186 | on_announcement: 187 | # Stop the wake word (mWW or VA) if the mic is capturing 188 | - if: 189 | condition: 190 | - microphone.is_capturing: 191 | then: 192 | - script.execute: stop_wake_word 193 | # Ensure VA stops before moving on 194 | - if: 195 | condition: 196 | - lambda: return id(wake_word_engine_location).state == "In Home Assistant"; 197 | then: 198 | - wait_until: 199 | - not: 200 | voice_assistant.is_running: 201 | # Since VA isn't running, this is user-intiated media playback. Draw the mute display 202 | - if: 203 | condition: 204 | not: 205 | voice_assistant.is_running: 206 | then: 207 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 208 | - script.execute: draw_display 209 | on_idle: 210 | # Since VA isn't running, this is the end of user-intiated media playback. Restart the wake word. 211 | - if: 212 | condition: 213 | not: 214 | voice_assistant.is_running: 215 | then: 216 | - script.execute: start_wake_word 217 | - script.execute: set_idle_or_mute_phase 218 | - script.execute: draw_display 219 | 220 | micro_wake_word: 221 | id: mww 222 | models: 223 | - okay_nabu 224 | - hey_mycroft 225 | - hey_jarvis 226 | on_wake_word_detected: 227 | - voice_assistant.start: 228 | wake_word: !lambda return wake_word; 229 | 230 | voice_assistant: 231 | id: va 232 | microphone: box_mic 233 | media_player: speaker_media_player 234 | micro_wake_word: mww 235 | noise_suppression_level: 2 236 | auto_gain: 31dBFS 237 | volume_multiplier: 2.0 238 | on_listening: 239 | - lambda: id(voice_assistant_phase) = ${voice_assist_listening_phase_id}; 240 | - text_sensor.template.publish: 241 | id: text_request 242 | state: "..." 243 | - text_sensor.template.publish: 244 | id: text_response 245 | state: "..." 246 | - script.execute: draw_display 247 | on_stt_vad_end: 248 | - lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id}; 249 | - script.execute: draw_display 250 | on_stt_end: 251 | - text_sensor.template.publish: 252 | id: text_request 253 | state: !lambda return x; 254 | - script.execute: draw_display 255 | on_tts_start: 256 | - text_sensor.template.publish: 257 | id: text_response 258 | state: !lambda return x; 259 | - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; 260 | - script.execute: draw_display 261 | on_end: 262 | # Wait a short amount of time to see if an announcement starts 263 | - wait_until: 264 | condition: 265 | - media_player.is_announcing: 266 | timeout: 0.5s 267 | # Announcement is finished and the I2S bus is free 268 | - wait_until: 269 | - and: 270 | - not: 271 | media_player.is_announcing: 272 | - not: 273 | speaker.is_playing: 274 | # Restart only mWW if enabled; streaming wake words automatically restart 275 | - if: 276 | condition: 277 | - lambda: return id(wake_word_engine_location).state == "On device"; 278 | then: 279 | - lambda: id(va).set_use_wake_word(false); 280 | - micro_wake_word.start: 281 | - script.execute: set_idle_or_mute_phase 282 | - script.execute: draw_display 283 | # Clear text sensors 284 | - text_sensor.template.publish: 285 | id: text_request 286 | state: "" 287 | - text_sensor.template.publish: 288 | id: text_response 289 | state: "" 290 | on_error: 291 | - if: 292 | condition: 293 | lambda: return !id(init_in_progress); 294 | then: 295 | - lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id}; 296 | - script.execute: draw_display 297 | - delay: 1s 298 | - if: 299 | condition: 300 | switch.is_off: mute 301 | then: 302 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 303 | else: 304 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 305 | - script.execute: draw_display 306 | on_client_connected: 307 | - lambda: id(init_in_progress) = false; 308 | - script.execute: start_wake_word 309 | - script.execute: set_idle_or_mute_phase 310 | - script.execute: draw_display 311 | on_client_disconnected: 312 | - script.execute: stop_wake_word 313 | - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id}; 314 | - script.execute: draw_display 315 | on_timer_started: 316 | - script.execute: draw_display 317 | on_timer_cancelled: 318 | - script.execute: draw_display 319 | on_timer_updated: 320 | - script.execute: draw_display 321 | on_timer_tick: 322 | - script.execute: draw_display 323 | on_timer_finished: 324 | - switch.turn_on: timer_ringing 325 | - wait_until: 326 | media_player.is_announcing: 327 | - lambda: id(voice_assistant_phase) = ${voice_assist_timer_finished_phase_id}; 328 | - script.execute: draw_display 329 | 330 | script: 331 | - id: draw_display 332 | then: 333 | - if: 334 | condition: 335 | lambda: return !id(init_in_progress); 336 | then: 337 | - if: 338 | condition: 339 | wifi.connected: 340 | then: 341 | - if: 342 | condition: 343 | api.connected: 344 | then: 345 | - lambda: | 346 | switch(id(voice_assistant_phase)) { 347 | case ${voice_assist_listening_phase_id}: 348 | id(s3_box_lcd).show_page(listening_page); 349 | id(s3_box_lcd).update(); 350 | break; 351 | case ${voice_assist_thinking_phase_id}: 352 | id(s3_box_lcd).show_page(thinking_page); 353 | id(s3_box_lcd).update(); 354 | break; 355 | case ${voice_assist_replying_phase_id}: 356 | id(s3_box_lcd).show_page(replying_page); 357 | id(s3_box_lcd).update(); 358 | break; 359 | case ${voice_assist_error_phase_id}: 360 | id(s3_box_lcd).show_page(error_page); 361 | id(s3_box_lcd).update(); 362 | break; 363 | case ${voice_assist_muted_phase_id}: 364 | id(s3_box_lcd).show_page(muted_page); 365 | id(s3_box_lcd).update(); 366 | break; 367 | case ${voice_assist_not_ready_phase_id}: 368 | id(s3_box_lcd).show_page(no_ha_page); 369 | id(s3_box_lcd).update(); 370 | break; 371 | case ${voice_assist_timer_finished_phase_id}: 372 | id(s3_box_lcd).show_page(timer_finished_page); 373 | id(s3_box_lcd).update(); 374 | break; 375 | default: 376 | id(s3_box_lcd).show_page(idle_page); 377 | id(s3_box_lcd).update(); 378 | } 379 | else: 380 | - display.page.show: no_ha_page 381 | - component.update: s3_box_lcd 382 | else: 383 | - display.page.show: no_wifi_page 384 | - component.update: s3_box_lcd 385 | else: 386 | - display.page.show: initializing_page 387 | - component.update: s3_box_lcd 388 | 389 | - id: fetch_first_active_timer 390 | then: 391 | - lambda: | 392 | const auto timers = id(va).get_timers(); 393 | auto output_timer = timers.begin()->second; 394 | for (auto &iterable_timer : timers) { 395 | if (iterable_timer.second.is_active && iterable_timer.second.seconds_left <= output_timer.seconds_left) { 396 | output_timer = iterable_timer.second; 397 | } 398 | } 399 | id(global_first_active_timer) = output_timer; 400 | - id: check_if_timers_active 401 | then: 402 | - lambda: | 403 | const auto timers = id(va).get_timers(); 404 | bool output = false; 405 | if (timers.size() > 0) { 406 | for (auto &iterable_timer : timers) { 407 | if(iterable_timer.second.is_active) { 408 | output = true; 409 | } 410 | } 411 | } 412 | id(global_is_timer_active) = output; 413 | - id: fetch_first_timer 414 | then: 415 | - lambda: | 416 | const auto timers = id(va).get_timers(); 417 | auto output_timer = timers.begin()->second; 418 | for (auto &iterable_timer : timers) { 419 | if (iterable_timer.second.seconds_left <= output_timer.seconds_left) { 420 | output_timer = iterable_timer.second; 421 | } 422 | } 423 | id(global_first_timer) = output_timer; 424 | - id: check_if_timers 425 | then: 426 | - lambda: | 427 | const auto timers = id(va).get_timers(); 428 | bool output = false; 429 | if (timers.size() > 0) { 430 | output = true; 431 | } 432 | id(global_is_timer) = output; 433 | 434 | - id: draw_timer_timeline 435 | then: 436 | - lambda: | 437 | id(check_if_timers_active).execute(); 438 | id(check_if_timers).execute(); 439 | if (id(global_is_timer_active)){ 440 | id(fetch_first_active_timer).execute(); 441 | int active_pixels = round( 320 * id(global_first_active_timer).seconds_left / max(id(global_first_active_timer).total_seconds , static_cast(1)) ); 442 | if (active_pixels > 0){ 443 | id(s3_box_lcd).filled_rectangle(0 , 225 , 320 , 15 , Color::WHITE ); 444 | id(s3_box_lcd).filled_rectangle(0 , 226 , active_pixels , 13 , id(active_timer_color) ); 445 | } 446 | } else if (id(global_is_timer)){ 447 | id(fetch_first_timer).execute(); 448 | int active_pixels = round( 320 * id(global_first_timer).seconds_left / max(id(global_first_timer).total_seconds , static_cast(1))); 449 | if (active_pixels > 0){ 450 | id(s3_box_lcd).filled_rectangle(0 , 225 , 320 , 15 , Color::WHITE ); 451 | id(s3_box_lcd).filled_rectangle(0 , 226 , active_pixels , 13 , id(paused_timer_color) ); 452 | } 453 | } 454 | - id: draw_active_timer_widget 455 | then: 456 | - lambda: | 457 | id(check_if_timers_active).execute(); 458 | if (id(global_is_timer_active)){ 459 | id(s3_box_lcd).filled_rectangle(80 , 40 , 160 , 50 , Color::WHITE ); 460 | id(s3_box_lcd).rectangle(80 , 40 , 160 , 50 , Color::BLACK ); 461 | 462 | id(fetch_first_active_timer).execute(); 463 | int hours_left = floor(id(global_first_active_timer).seconds_left / 3600); 464 | int minutes_left = floor((id(global_first_active_timer).seconds_left - hours_left * 3600) / 60); 465 | int seconds_left = id(global_first_active_timer).seconds_left - hours_left * 3600 - minutes_left * 60 ; 466 | auto display_hours = (hours_left < 10 ? "0" : "") + std::to_string(hours_left); 467 | auto display_minute = (minutes_left < 10 ? "0" : "") + std::to_string(minutes_left); 468 | auto display_seconds = (seconds_left < 10 ? "0" : "") + std::to_string(seconds_left) ; 469 | 470 | std::string display_string = ""; 471 | if (hours_left > 0) { 472 | display_string = display_hours + ":" + display_minute; 473 | } else { 474 | display_string = display_minute + ":" + display_seconds; 475 | } 476 | id(s3_box_lcd).printf(120, 47, id(font_timer), Color::BLACK, "%s", display_string.c_str()); 477 | } 478 | # Starts either mWW or the streaming wake word, depending on the configured location 479 | - id: start_wake_word 480 | then: 481 | - if: 482 | condition: 483 | and: 484 | - not: 485 | - voice_assistant.is_running: 486 | - lambda: return id(wake_word_engine_location).state == "On device"; 487 | then: 488 | - lambda: id(va).set_use_wake_word(false); 489 | - micro_wake_word.start: 490 | - if: 491 | condition: 492 | and: 493 | - not: 494 | - voice_assistant.is_running: 495 | - lambda: return id(wake_word_engine_location).state == "In Home Assistant"; 496 | then: 497 | - lambda: id(va).set_use_wake_word(true); 498 | - voice_assistant.start_continuous: 499 | # Stops either mWW or the streaming wake word, depending on the configured location 500 | - id: stop_wake_word 501 | then: 502 | - if: 503 | condition: 504 | lambda: return id(wake_word_engine_location).state == "In Home Assistant"; 505 | then: 506 | - lambda: id(va).set_use_wake_word(false); 507 | - voice_assistant.stop: 508 | - if: 509 | condition: 510 | lambda: return id(wake_word_engine_location).state == "On device"; 511 | then: 512 | - micro_wake_word.stop: 513 | # Set the voice assistant phase to idle or muted, depending on if the software mute switch is activated 514 | - id: set_idle_or_mute_phase 515 | then: 516 | - if: 517 | condition: 518 | switch.is_off: mute 519 | then: 520 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 521 | else: 522 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 523 | 524 | switch: 525 | - platform: gpio 526 | name: Speaker Enable 527 | pin: GPIO46 528 | restore_mode: RESTORE_DEFAULT_ON 529 | entity_category: config 530 | disabled_by_default: true 531 | - platform: template 532 | name: Mute 533 | id: mute 534 | icon: "mdi:microphone-off" 535 | optimistic: true 536 | restore_mode: RESTORE_DEFAULT_OFF 537 | entity_category: config 538 | on_turn_off: 539 | - microphone.unmute: 540 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 541 | - script.execute: draw_display 542 | on_turn_on: 543 | - microphone.mute: 544 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 545 | - script.execute: draw_display 546 | - platform: template 547 | id: timer_ringing 548 | optimistic: true 549 | internal: true 550 | restore_mode: ALWAYS_OFF 551 | on_turn_off: 552 | # Turn off the repeat mode and disable the pause between playlist items 553 | - lambda: |- 554 | id(speaker_media_player) 555 | ->make_call() 556 | .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_OFF) 557 | .set_announcement(true) 558 | .perform(); 559 | id(speaker_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 0); 560 | # Stop playing the alarm 561 | - media_player.stop: 562 | announcement: true 563 | on_turn_on: 564 | # Turn on the repeat mode and pause for 1000 ms between playlist items/repeats 565 | - lambda: |- 566 | id(speaker_media_player) 567 | ->make_call() 568 | .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ONE) 569 | .set_announcement(true) 570 | .perform(); 571 | id(speaker_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 1000); 572 | - media_player.speaker.play_on_device_media_file: 573 | media_file: timer_finished_sound 574 | announcement: true 575 | - delay: 15min 576 | - switch.turn_off: timer_ringing 577 | 578 | select: 579 | - platform: template 580 | entity_category: config 581 | name: Wake word engine location 582 | id: wake_word_engine_location 583 | icon: "mdi:account-voice" 584 | optimistic: true 585 | restore_value: true 586 | options: 587 | - In Home Assistant 588 | - On device 589 | initial_option: On device 590 | on_value: 591 | - if: 592 | condition: 593 | lambda: return !id(init_in_progress); 594 | then: 595 | - wait_until: 596 | lambda: return id(voice_assistant_phase) == ${voice_assist_muted_phase_id} || id(voice_assistant_phase) == ${voice_assist_idle_phase_id}; 597 | - if: 598 | condition: 599 | lambda: return x == "In Home Assistant"; 600 | then: 601 | - micro_wake_word.stop 602 | - delay: 500ms 603 | - if: 604 | condition: 605 | switch.is_off: mute 606 | then: 607 | - lambda: id(va).set_use_wake_word(true); 608 | - voice_assistant.start_continuous: 609 | - if: 610 | condition: 611 | lambda: return x == "On device"; 612 | then: 613 | - lambda: id(va).set_use_wake_word(false); 614 | - voice_assistant.stop 615 | - delay: 500ms 616 | - if: 617 | condition: 618 | switch.is_off: mute 619 | then: 620 | - micro_wake_word.start 621 | 622 | globals: 623 | - id: init_in_progress 624 | type: bool 625 | restore_value: false 626 | initial_value: "true" 627 | - id: voice_assistant_phase 628 | type: int 629 | restore_value: false 630 | initial_value: ${voice_assist_not_ready_phase_id} 631 | - id: global_first_active_timer 632 | type: voice_assistant::Timer 633 | restore_value: false 634 | - id: global_is_timer_active 635 | type: bool 636 | restore_value: false 637 | - id: global_first_timer 638 | type: voice_assistant::Timer 639 | restore_value: false 640 | - id: global_is_timer 641 | type: bool 642 | restore_value: false 643 | 644 | image: 645 | - file: ${error_illustration_file} 646 | id: casita_error 647 | resize: 320x240 648 | type: RGB 649 | transparency: alpha_channel 650 | - file: ${idle_illustration_file} 651 | id: casita_idle 652 | resize: 320x240 653 | type: RGB 654 | transparency: alpha_channel 655 | - file: ${listening_illustration_file} 656 | id: casita_listening 657 | resize: 320x240 658 | type: RGB 659 | transparency: alpha_channel 660 | - file: ${thinking_illustration_file} 661 | id: casita_thinking 662 | resize: 320x240 663 | type: RGB 664 | transparency: alpha_channel 665 | - file: ${replying_illustration_file} 666 | id: casita_replying 667 | resize: 320x240 668 | type: RGB 669 | transparency: alpha_channel 670 | - file: ${timer_finished_illustration_file} 671 | id: casita_timer_finished 672 | resize: 320x240 673 | type: RGB 674 | transparency: alpha_channel 675 | - file: ${loading_illustration_file} 676 | id: casita_initializing 677 | resize: 320x240 678 | type: RGB 679 | transparency: alpha_channel 680 | - file: https://github.com/esphome/wake-word-voice-assistants/raw/main/error_box_illustrations/error-no-wifi.png 681 | id: error_no_wifi 682 | resize: 320x240 683 | type: RGB 684 | transparency: alpha_channel 685 | - file: https://github.com/esphome/wake-word-voice-assistants/raw/main/error_box_illustrations/error-no-ha.png 686 | id: error_no_ha 687 | resize: 320x240 688 | type: RGB 689 | transparency: alpha_channel 690 | 691 | font: 692 | - file: 693 | type: gfonts 694 | family: ${font_family} 695 | weight: 300 696 | italic: true 697 | id: font_request 698 | size: 15 699 | glyphsets: 700 | - ${font_glyphsets} 701 | - file: 702 | type: gfonts 703 | family: ${font_family} 704 | weight: 300 705 | id: font_response 706 | size: 15 707 | glyphsets: 708 | - ${font_glyphsets} 709 | - file: 710 | type: gfonts 711 | family: ${font_family} 712 | weight: 300 713 | id: font_timer 714 | size: 30 715 | glyphsets: 716 | - ${font_glyphsets} 717 | 718 | text_sensor: 719 | - id: text_request 720 | platform: template 721 | on_value: 722 | lambda: |- 723 | if(id(text_request).state.length()>32) { 724 | std::string name = id(text_request).state.c_str(); 725 | std::string truncated = esphome::str_truncate(name.c_str(),31); 726 | id(text_request).state = (truncated+"...").c_str(); 727 | } 728 | 729 | - id: text_response 730 | platform: template 731 | on_value: 732 | lambda: |- 733 | if(id(text_response).state.length()>32) { 734 | std::string name = id(text_response).state.c_str(); 735 | std::string truncated = esphome::str_truncate(name.c_str(),31); 736 | id(text_response).state = (truncated+"...").c_str(); 737 | } 738 | 739 | color: 740 | - id: idle_color 741 | hex: ${idle_illustration_background_color} 742 | - id: listening_color 743 | hex: ${listening_illustration_background_color} 744 | - id: thinking_color 745 | hex: ${thinking_illustration_background_color} 746 | - id: replying_color 747 | hex: ${replying_illustration_background_color} 748 | - id: loading_color 749 | hex: ${loading_illustration_background_color} 750 | - id: error_color 751 | hex: ${error_illustration_background_color} 752 | - id: active_timer_color 753 | hex: "26ed3a" 754 | - id: paused_timer_color 755 | hex: "3b89e3" 756 | 757 | spi: 758 | - id: spi_bus 759 | clk_pin: 7 760 | mosi_pin: 6 761 | 762 | display: 763 | - platform: ili9xxx 764 | id: s3_box_lcd 765 | model: S3BOX 766 | invert_colors: false 767 | data_rate: 40MHz 768 | cs_pin: 5 769 | dc_pin: 4 770 | reset_pin: 48 771 | update_interval: never 772 | pages: 773 | - id: idle_page 774 | lambda: |- 775 | it.fill(id(idle_color)); 776 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_idle), ImageAlign::CENTER); 777 | id(draw_timer_timeline).execute(); 778 | id(draw_active_timer_widget).execute(); 779 | - id: listening_page 780 | lambda: |- 781 | it.fill(id(listening_color)); 782 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_listening), ImageAlign::CENTER); 783 | id(draw_timer_timeline).execute(); 784 | - id: thinking_page 785 | lambda: |- 786 | it.fill(id(thinking_color)); 787 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_thinking), ImageAlign::CENTER); 788 | it.filled_rectangle(20 , 20 , 280 , 30 , Color::WHITE ); 789 | it.rectangle(20 , 20 , 280 , 30 , Color::BLACK ); 790 | it.printf(30, 25, id(font_request), Color::BLACK, "%s", id(text_request).state.c_str()); 791 | id(draw_timer_timeline).execute(); 792 | - id: replying_page 793 | lambda: |- 794 | it.fill(id(replying_color)); 795 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_replying), ImageAlign::CENTER); 796 | it.filled_rectangle(20 , 20 , 280 , 30 , Color::WHITE ); 797 | it.rectangle(20 , 20 , 280 , 30 , Color::BLACK ); 798 | it.filled_rectangle(20 , 190 , 280 , 30 , Color::WHITE ); 799 | it.rectangle(20 , 190 , 280 , 30 , Color::BLACK ); 800 | it.printf(30, 25, id(font_request), Color::BLACK, "%s", id(text_request).state.c_str()); 801 | it.printf(30, 195, id(font_response), Color::BLACK, "%s", id(text_response).state.c_str()); 802 | id(draw_timer_timeline).execute(); 803 | - id: timer_finished_page 804 | lambda: |- 805 | it.fill(id(idle_color)); 806 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_timer_finished), ImageAlign::CENTER); 807 | - id: error_page 808 | lambda: |- 809 | it.fill(id(error_color)); 810 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_error), ImageAlign::CENTER); 811 | - id: no_ha_page 812 | lambda: |- 813 | it.image((it.get_width() / 2), (it.get_height() / 2), id(error_no_ha), ImageAlign::CENTER); 814 | - id: no_wifi_page 815 | lambda: |- 816 | it.image((it.get_width() / 2), (it.get_height() / 2), id(error_no_wifi), ImageAlign::CENTER); 817 | - id: initializing_page 818 | lambda: |- 819 | it.fill(id(loading_color)); 820 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_initializing), ImageAlign::CENTER); 821 | - id: muted_page 822 | lambda: |- 823 | it.fill(Color::BLACK); 824 | id(draw_timer_timeline).execute(); 825 | id(draw_active_timer_widget).execute(); 826 | -------------------------------------------------------------------------------- /esp32-s3-box-3/esp32-s3-box-3.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | substitutions: 3 | name: esp32-s3-box-3 4 | friendly_name: ESP32 S3 Box 3 5 | loading_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/loading_320_240.png 6 | idle_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/idle_320_240.png 7 | listening_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/listening_320_240.png 8 | thinking_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/thinking_320_240.png 9 | replying_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/replying_320_240.png 10 | error_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/error_320_240.png 11 | timer_finished_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/timer_finished_320_240.png 12 | 13 | loading_illustration_background_color: "000000" 14 | idle_illustration_background_color: "000000" 15 | listening_illustration_background_color: "FFFFFF" 16 | thinking_illustration_background_color: "FFFFFF" 17 | replying_illustration_background_color: "FFFFFF" 18 | error_illustration_background_color: "000000" 19 | 20 | voice_assist_idle_phase_id: "1" 21 | voice_assist_listening_phase_id: "2" 22 | voice_assist_thinking_phase_id: "3" 23 | voice_assist_replying_phase_id: "4" 24 | voice_assist_not_ready_phase_id: "10" 25 | voice_assist_error_phase_id: "11" 26 | voice_assist_muted_phase_id: "12" 27 | voice_assist_timer_finished_phase_id: "20" 28 | 29 | # These unique characters have been extracted from every test file of every language available on https://github.com/home-assistant/intents (14 March 2024) 30 | # However, the Figtree font only contains Latin characters, so there is no point using this... unlessyou change the font configuration accordingly. 31 | allowed_characters: " !#%'()+,-./0123456789:;<>?@ABCDEFGHIJKLMNOPQRSTUVWYZ[]_abcdefghijklmnopqrstuvwxyz{|}°²³µ¿ÁÂÄÅÉÖÚßàáâãäåæçèéêëìíîðñòóôõöøùúûüýþāăąćčďĐđēėęěğĮįıļľŁłńňőřśšťũūůűųźŻżŽžơưșțΆΈΌΐΑΒΓΔΕΖΗΘΚΜΝΠΡΣΤΥΦάέήίαβγδεζηθικλμνξοπρςστυφχψωϊόύώАБВГДЕЖЗИКЛМНОПРСТУХЦЧШЪЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяёђєіїјљњћאבגדהוזחטיכלםמןנסעפץצקרשת،ءآأإئابةتجحخدذرزسشصضطظعغفقكلمنهوىيٹپچڈکگںھہیےংকচতধনফবযরলশষস়ািু্చయలిెొ్ംഅആഇഈഉഎഓകഗങചജഞടഡണതദധനപഫബഭമയരറലളവശസഹാിീുൂെേൈ്ൺൻർൽൾაბგდევზთილმნოპრსტუფქყშჩცძჭხạảấầẩậắặẹẽếềểệỉịọỏốồổỗộớờởợụủứừửữựỳ—、一上不个中为主乾了些亮人任低佔何作供依侧係個側偵充光入全关冇冷几切到制前動區卧厅厨及口另右吊后吗启吸呀咗哪唔問啟嗎嘅嘛器圍在场執場外多大始安定客室家密寵对將小少左已帘常幫幾库度庫廊廚廳开式後恆感態成我戲戶户房所扇手打执把拔换掉控插摄整斯新明是景暗更最會有未本模機檯櫃欄次正氏水沒没洗活派温測源溫漏潮激濕灯為無煙照熱燈燥物狀玄现現瓦用發的盞目着睡私空窗立笛管節簾籬紅線红罐置聚聲脚腦腳臥色节著行衣解設調請謝警设调走路車车运連遊運過道邊部都量鎖锁門閂閉開關门闭除隱離電震霧面音頂題顏颜風风食餅餵가간감갔강개거게겨결경고공과관그금급기길깥꺼껐꼽나난내네놀누는능니다닫담대더데도동됐되된됨둡드든등디때떤뜨라래러렇렌려로료른를리림링마많명몇모무문물뭐바밝방배변보부불블빨뽑사산상색서설성세센션소쇼수스습시신실싱아안않알았애야어얼업없었에여연열옆오온완외왼요운움워원위으은을음의이인일임입있작잠장재전절정제져조족종주줄중줘지직진짐쪽차창천최추출충치침커컴켜켰쿠크키탁탄태탬터텔통트튼티파팬퍼폰표퓨플핑한함해했행혀현화활후휴힘,?" 32 | 33 | # Add support for non-unicode characters by using better glyphset 34 | font_glyphsets: "GF_Latin_Core" 35 | # for Greek use "Noto Sans" for other languages use a compatible font family 36 | font_family: Figtree 37 | 38 | esphome: 39 | name: ${name} 40 | friendly_name: ${friendly_name} 41 | min_version: 2025.5.0 42 | name_add_mac_suffix: true 43 | on_boot: 44 | priority: 600 45 | then: 46 | - script.execute: draw_display 47 | - delay: 30s 48 | - if: 49 | condition: 50 | lambda: return id(init_in_progress); 51 | then: 52 | - lambda: id(init_in_progress) = false; 53 | - script.execute: draw_display 54 | 55 | esp32: 56 | board: esp32s3box 57 | flash_size: 16MB 58 | cpu_frequency: 240MHz 59 | framework: 60 | type: esp-idf 61 | sdkconfig_options: 62 | CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y" 63 | CONFIG_ESP32S3_DATA_CACHE_64KB: "y" 64 | CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y" 65 | 66 | psram: 67 | mode: octal 68 | speed: 80MHz 69 | 70 | api: 71 | on_client_connected: 72 | - script.execute: draw_display 73 | on_client_disconnected: 74 | - script.execute: draw_display 75 | 76 | ota: 77 | - platform: esphome 78 | id: ota_esphome 79 | 80 | logger: 81 | hardware_uart: USB_SERIAL_JTAG 82 | 83 | wifi: 84 | ap: 85 | on_connect: 86 | - script.execute: draw_display 87 | on_disconnect: 88 | - script.execute: draw_display 89 | 90 | captive_portal: 91 | 92 | button: 93 | - platform: factory_reset 94 | id: factory_reset_btn 95 | internal: true 96 | 97 | binary_sensor: 98 | - platform: gpio 99 | pin: 100 | number: GPIO0 101 | mode: INPUT_PULLUP 102 | inverted: true 103 | id: left_top_button 104 | internal: true 105 | on_multi_click: 106 | - timing: 107 | - ON for at least 50ms 108 | - OFF for at least 50ms 109 | then: 110 | - switch.turn_off: timer_ringing 111 | - timing: 112 | - ON for at least 10s 113 | then: 114 | - button.press: factory_reset_btn 115 | 116 | output: 117 | - platform: ledc 118 | pin: GPIO47 119 | id: backlight_output 120 | 121 | light: 122 | - platform: monochromatic 123 | id: led 124 | name: Screen 125 | icon: "mdi:television" 126 | entity_category: config 127 | output: backlight_output 128 | restore_mode: RESTORE_DEFAULT_ON 129 | default_transition_length: 250ms 130 | 131 | i2c: 132 | scl: GPIO18 133 | sda: GPIO8 134 | 135 | i2s_audio: 136 | - id: i2s_audio_bus 137 | i2s_lrclk_pin: GPIO45 138 | i2s_bclk_pin: GPIO17 139 | i2s_mclk_pin: GPIO2 140 | 141 | audio_adc: 142 | - platform: es7210 143 | id: es7210_adc 144 | bits_per_sample: 16bit 145 | sample_rate: 16000 146 | 147 | audio_dac: 148 | - platform: es8311 149 | id: es8311_dac 150 | bits_per_sample: 16bit 151 | sample_rate: 48000 152 | 153 | microphone: 154 | - platform: i2s_audio 155 | id: box_mic 156 | sample_rate: 16000 157 | i2s_din_pin: GPIO16 158 | bits_per_sample: 16bit 159 | adc_type: external 160 | 161 | speaker: 162 | - platform: i2s_audio 163 | id: box_speaker 164 | i2s_dout_pin: GPIO15 165 | dac_type: external 166 | sample_rate: 48000 167 | bits_per_sample: 16bit 168 | channel: left 169 | audio_dac: es8311_dac 170 | buffer_duration: 100ms 171 | 172 | media_player: 173 | - platform: speaker 174 | name: None 175 | id: speaker_media_player 176 | volume_min: 0.5 177 | volume_max: 0.8 178 | announcement_pipeline: 179 | speaker: box_speaker 180 | format: FLAC 181 | sample_rate: 48000 182 | num_channels: 1 # S3 Box only has one output channel 183 | files: 184 | - id: timer_finished_sound 185 | file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/timer_finished.flac 186 | on_announcement: 187 | # Stop the wake word (mWW or VA) if the mic is capturing 188 | - if: 189 | condition: 190 | - microphone.is_capturing: 191 | then: 192 | - script.execute: stop_wake_word 193 | # Ensure VA stops before moving on 194 | - if: 195 | condition: 196 | - lambda: return id(wake_word_engine_location).state == "In Home Assistant"; 197 | then: 198 | - wait_until: 199 | - not: 200 | voice_assistant.is_running: 201 | # Since VA isn't running, this is user-intiated media playback. Draw the mute display 202 | - if: 203 | condition: 204 | not: 205 | voice_assistant.is_running: 206 | then: 207 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 208 | - script.execute: draw_display 209 | on_idle: 210 | # Since VA isn't running, this is the end of user-intiated media playback. Restart the wake word. 211 | - if: 212 | condition: 213 | not: 214 | voice_assistant.is_running: 215 | then: 216 | - script.execute: start_wake_word 217 | - script.execute: set_idle_or_mute_phase 218 | - script.execute: draw_display 219 | 220 | micro_wake_word: 221 | id: mww 222 | models: 223 | - okay_nabu 224 | - hey_mycroft 225 | - hey_jarvis 226 | on_wake_word_detected: 227 | - voice_assistant.start: 228 | wake_word: !lambda return wake_word; 229 | 230 | voice_assistant: 231 | id: va 232 | microphone: box_mic 233 | media_player: speaker_media_player 234 | micro_wake_word: mww 235 | noise_suppression_level: 2 236 | auto_gain: 31dBFS 237 | volume_multiplier: 2.0 238 | on_listening: 239 | - lambda: id(voice_assistant_phase) = ${voice_assist_listening_phase_id}; 240 | - text_sensor.template.publish: 241 | id: text_request 242 | state: "..." 243 | - text_sensor.template.publish: 244 | id: text_response 245 | state: "..." 246 | - script.execute: draw_display 247 | on_stt_vad_end: 248 | - lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id}; 249 | - script.execute: draw_display 250 | on_stt_end: 251 | - text_sensor.template.publish: 252 | id: text_request 253 | state: !lambda return x; 254 | - script.execute: draw_display 255 | on_tts_start: 256 | - text_sensor.template.publish: 257 | id: text_response 258 | state: !lambda return x; 259 | - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; 260 | - script.execute: draw_display 261 | on_end: 262 | # Wait a short amount of time to see if an announcement starts 263 | - wait_until: 264 | condition: 265 | - media_player.is_announcing: 266 | timeout: 0.5s 267 | # Announcement is finished and the I2S bus is free 268 | - wait_until: 269 | - and: 270 | - not: 271 | media_player.is_announcing: 272 | - not: 273 | speaker.is_playing: 274 | # Restart only mWW if enabled; streaming wake words automatically restart 275 | - if: 276 | condition: 277 | - lambda: return id(wake_word_engine_location).state == "On device"; 278 | then: 279 | - lambda: id(va).set_use_wake_word(false); 280 | - micro_wake_word.start: 281 | - script.execute: set_idle_or_mute_phase 282 | - script.execute: draw_display 283 | # Clear text sensors 284 | - text_sensor.template.publish: 285 | id: text_request 286 | state: "" 287 | - text_sensor.template.publish: 288 | id: text_response 289 | state: "" 290 | on_error: 291 | - if: 292 | condition: 293 | lambda: return !id(init_in_progress); 294 | then: 295 | - lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id}; 296 | - script.execute: draw_display 297 | - delay: 1s 298 | - if: 299 | condition: 300 | switch.is_off: mute 301 | then: 302 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 303 | else: 304 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 305 | - script.execute: draw_display 306 | on_client_connected: 307 | - lambda: id(init_in_progress) = false; 308 | - script.execute: start_wake_word 309 | - script.execute: set_idle_or_mute_phase 310 | - script.execute: draw_display 311 | on_client_disconnected: 312 | - script.execute: stop_wake_word 313 | - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id}; 314 | - script.execute: draw_display 315 | on_timer_started: 316 | - script.execute: draw_display 317 | on_timer_cancelled: 318 | - script.execute: draw_display 319 | on_timer_updated: 320 | - script.execute: draw_display 321 | on_timer_tick: 322 | - script.execute: draw_display 323 | on_timer_finished: 324 | - switch.turn_on: timer_ringing 325 | - wait_until: 326 | media_player.is_announcing: 327 | - lambda: id(voice_assistant_phase) = ${voice_assist_timer_finished_phase_id}; 328 | - script.execute: draw_display 329 | 330 | script: 331 | - id: draw_display 332 | then: 333 | - if: 334 | condition: 335 | lambda: return !id(init_in_progress); 336 | then: 337 | - if: 338 | condition: 339 | wifi.connected: 340 | then: 341 | - if: 342 | condition: 343 | api.connected: 344 | then: 345 | - lambda: | 346 | switch(id(voice_assistant_phase)) { 347 | case ${voice_assist_listening_phase_id}: 348 | id(s3_box_lcd).show_page(listening_page); 349 | id(s3_box_lcd).update(); 350 | break; 351 | case ${voice_assist_thinking_phase_id}: 352 | id(s3_box_lcd).show_page(thinking_page); 353 | id(s3_box_lcd).update(); 354 | break; 355 | case ${voice_assist_replying_phase_id}: 356 | id(s3_box_lcd).show_page(replying_page); 357 | id(s3_box_lcd).update(); 358 | break; 359 | case ${voice_assist_error_phase_id}: 360 | id(s3_box_lcd).show_page(error_page); 361 | id(s3_box_lcd).update(); 362 | break; 363 | case ${voice_assist_muted_phase_id}: 364 | id(s3_box_lcd).show_page(muted_page); 365 | id(s3_box_lcd).update(); 366 | break; 367 | case ${voice_assist_not_ready_phase_id}: 368 | id(s3_box_lcd).show_page(no_ha_page); 369 | id(s3_box_lcd).update(); 370 | break; 371 | case ${voice_assist_timer_finished_phase_id}: 372 | id(s3_box_lcd).show_page(timer_finished_page); 373 | id(s3_box_lcd).update(); 374 | break; 375 | default: 376 | id(s3_box_lcd).show_page(idle_page); 377 | id(s3_box_lcd).update(); 378 | } 379 | else: 380 | - display.page.show: no_ha_page 381 | - component.update: s3_box_lcd 382 | else: 383 | - display.page.show: no_wifi_page 384 | - component.update: s3_box_lcd 385 | else: 386 | - display.page.show: initializing_page 387 | - component.update: s3_box_lcd 388 | 389 | - id: fetch_first_active_timer 390 | then: 391 | - lambda: | 392 | const auto timers = id(va).get_timers(); 393 | auto output_timer = timers.begin()->second; 394 | for (auto &iterable_timer : timers) { 395 | if (iterable_timer.second.is_active && iterable_timer.second.seconds_left <= output_timer.seconds_left) { 396 | output_timer = iterable_timer.second; 397 | } 398 | } 399 | id(global_first_active_timer) = output_timer; 400 | - id: check_if_timers_active 401 | then: 402 | - lambda: | 403 | const auto timers = id(va).get_timers(); 404 | bool output = false; 405 | if (timers.size() > 0) { 406 | for (auto &iterable_timer : timers) { 407 | if(iterable_timer.second.is_active) { 408 | output = true; 409 | } 410 | } 411 | } 412 | id(global_is_timer_active) = output; 413 | - id: fetch_first_timer 414 | then: 415 | - lambda: | 416 | const auto timers = id(va).get_timers(); 417 | auto output_timer = timers.begin()->second; 418 | for (auto &iterable_timer : timers) { 419 | if (iterable_timer.second.seconds_left <= output_timer.seconds_left) { 420 | output_timer = iterable_timer.second; 421 | } 422 | } 423 | id(global_first_timer) = output_timer; 424 | - id: check_if_timers 425 | then: 426 | - lambda: | 427 | const auto timers = id(va).get_timers(); 428 | bool output = false; 429 | if (timers.size() > 0) { 430 | output = true; 431 | } 432 | id(global_is_timer) = output; 433 | 434 | - id: draw_timer_timeline 435 | then: 436 | - lambda: | 437 | id(check_if_timers_active).execute(); 438 | id(check_if_timers).execute(); 439 | if (id(global_is_timer_active)){ 440 | id(fetch_first_active_timer).execute(); 441 | int active_pixels = round( 320 * id(global_first_active_timer).seconds_left / max(id(global_first_active_timer).total_seconds , static_cast(1)) ); 442 | if (active_pixels > 0){ 443 | id(s3_box_lcd).filled_rectangle(0 , 225 , 320 , 15 , Color::WHITE ); 444 | id(s3_box_lcd).filled_rectangle(0 , 226 , active_pixels , 13 , id(active_timer_color) ); 445 | } 446 | } else if (id(global_is_timer)){ 447 | id(fetch_first_timer).execute(); 448 | int active_pixels = round( 320 * id(global_first_timer).seconds_left / max(id(global_first_timer).total_seconds , static_cast(1))); 449 | if (active_pixels > 0){ 450 | id(s3_box_lcd).filled_rectangle(0 , 225 , 320 , 15 , Color::WHITE ); 451 | id(s3_box_lcd).filled_rectangle(0 , 226 , active_pixels , 13 , id(paused_timer_color) ); 452 | } 453 | } 454 | - id: draw_active_timer_widget 455 | then: 456 | - lambda: | 457 | id(check_if_timers_active).execute(); 458 | if (id(global_is_timer_active)){ 459 | id(s3_box_lcd).filled_rectangle(80 , 40 , 160 , 50 , Color::WHITE ); 460 | id(s3_box_lcd).rectangle(80 , 40 , 160 , 50 , Color::BLACK ); 461 | 462 | id(fetch_first_active_timer).execute(); 463 | int hours_left = floor(id(global_first_active_timer).seconds_left / 3600); 464 | int minutes_left = floor((id(global_first_active_timer).seconds_left - hours_left * 3600) / 60); 465 | int seconds_left = id(global_first_active_timer).seconds_left - hours_left * 3600 - minutes_left * 60 ; 466 | auto display_hours = (hours_left < 10 ? "0" : "") + std::to_string(hours_left); 467 | auto display_minute = (minutes_left < 10 ? "0" : "") + std::to_string(minutes_left); 468 | auto display_seconds = (seconds_left < 10 ? "0" : "") + std::to_string(seconds_left) ; 469 | 470 | std::string display_string = ""; 471 | if (hours_left > 0) { 472 | display_string = display_hours + ":" + display_minute; 473 | } else { 474 | display_string = display_minute + ":" + display_seconds; 475 | } 476 | id(s3_box_lcd).printf(120, 47, id(font_timer), Color::BLACK, "%s", display_string.c_str()); 477 | } 478 | # Starts either mWW or the streaming wake word, depending on the configured location 479 | - id: start_wake_word 480 | then: 481 | - if: 482 | condition: 483 | and: 484 | - not: 485 | - voice_assistant.is_running: 486 | - lambda: return id(wake_word_engine_location).state == "On device"; 487 | then: 488 | - lambda: id(va).set_use_wake_word(false); 489 | - micro_wake_word.start: 490 | - if: 491 | condition: 492 | and: 493 | - not: 494 | - voice_assistant.is_running: 495 | - lambda: return id(wake_word_engine_location).state == "In Home Assistant"; 496 | then: 497 | - lambda: id(va).set_use_wake_word(true); 498 | - voice_assistant.start_continuous: 499 | # Stops either mWW or the streaming wake word, depending on the configured location 500 | - id: stop_wake_word 501 | then: 502 | - if: 503 | condition: 504 | lambda: return id(wake_word_engine_location).state == "In Home Assistant"; 505 | then: 506 | - lambda: id(va).set_use_wake_word(false); 507 | - voice_assistant.stop: 508 | - if: 509 | condition: 510 | lambda: return id(wake_word_engine_location).state == "On device"; 511 | then: 512 | - micro_wake_word.stop: 513 | # Set the voice assistant phase to idle or muted, depending on if the software mute switch is activated 514 | - id: set_idle_or_mute_phase 515 | then: 516 | - if: 517 | condition: 518 | switch.is_off: mute 519 | then: 520 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 521 | else: 522 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 523 | 524 | switch: 525 | - platform: gpio 526 | name: Speaker Enable 527 | pin: GPIO46 528 | restore_mode: RESTORE_DEFAULT_ON 529 | entity_category: config 530 | disabled_by_default: true 531 | - platform: template 532 | name: Mute 533 | id: mute 534 | icon: "mdi:microphone-off" 535 | optimistic: true 536 | restore_mode: RESTORE_DEFAULT_OFF 537 | entity_category: config 538 | on_turn_off: 539 | - microphone.unmute: 540 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 541 | - script.execute: draw_display 542 | on_turn_on: 543 | - microphone.mute: 544 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 545 | - script.execute: draw_display 546 | - platform: template 547 | id: timer_ringing 548 | optimistic: true 549 | internal: true 550 | restore_mode: ALWAYS_OFF 551 | on_turn_off: 552 | # Turn off the repeat mode and disable the pause between playlist items 553 | - lambda: |- 554 | id(speaker_media_player) 555 | ->make_call() 556 | .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_OFF) 557 | .set_announcement(true) 558 | .perform(); 559 | id(speaker_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 0); 560 | # Stop playing the alarm 561 | - media_player.stop: 562 | announcement: true 563 | on_turn_on: 564 | # Turn on the repeat mode and pause for 1000 ms between playlist items/repeats 565 | - lambda: |- 566 | id(speaker_media_player) 567 | ->make_call() 568 | .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ONE) 569 | .set_announcement(true) 570 | .perform(); 571 | id(speaker_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 1000); 572 | - media_player.speaker.play_on_device_media_file: 573 | media_file: timer_finished_sound 574 | announcement: true 575 | - delay: 15min 576 | - switch.turn_off: timer_ringing 577 | 578 | select: 579 | - platform: template 580 | entity_category: config 581 | name: Wake word engine location 582 | id: wake_word_engine_location 583 | icon: "mdi:account-voice" 584 | optimistic: true 585 | restore_value: true 586 | options: 587 | - In Home Assistant 588 | - On device 589 | initial_option: On device 590 | on_value: 591 | - if: 592 | condition: 593 | lambda: return !id(init_in_progress); 594 | then: 595 | - wait_until: 596 | lambda: return id(voice_assistant_phase) == ${voice_assist_muted_phase_id} || id(voice_assistant_phase) == ${voice_assist_idle_phase_id}; 597 | - if: 598 | condition: 599 | lambda: return x == "In Home Assistant"; 600 | then: 601 | - micro_wake_word.stop 602 | - delay: 500ms 603 | - if: 604 | condition: 605 | switch.is_off: mute 606 | then: 607 | - lambda: id(va).set_use_wake_word(true); 608 | - voice_assistant.start_continuous: 609 | - if: 610 | condition: 611 | lambda: return x == "On device"; 612 | then: 613 | - lambda: id(va).set_use_wake_word(false); 614 | - voice_assistant.stop 615 | - delay: 500ms 616 | - if: 617 | condition: 618 | switch.is_off: mute 619 | then: 620 | - micro_wake_word.start 621 | 622 | globals: 623 | - id: init_in_progress 624 | type: bool 625 | restore_value: false 626 | initial_value: "true" 627 | - id: voice_assistant_phase 628 | type: int 629 | restore_value: false 630 | initial_value: ${voice_assist_not_ready_phase_id} 631 | - id: global_first_active_timer 632 | type: voice_assistant::Timer 633 | restore_value: false 634 | - id: global_is_timer_active 635 | type: bool 636 | restore_value: false 637 | - id: global_first_timer 638 | type: voice_assistant::Timer 639 | restore_value: false 640 | - id: global_is_timer 641 | type: bool 642 | restore_value: false 643 | 644 | image: 645 | - file: ${error_illustration_file} 646 | id: casita_error 647 | resize: 320x240 648 | type: RGB 649 | transparency: alpha_channel 650 | - file: ${idle_illustration_file} 651 | id: casita_idle 652 | resize: 320x240 653 | type: RGB 654 | transparency: alpha_channel 655 | - file: ${listening_illustration_file} 656 | id: casita_listening 657 | resize: 320x240 658 | type: RGB 659 | transparency: alpha_channel 660 | - file: ${thinking_illustration_file} 661 | id: casita_thinking 662 | resize: 320x240 663 | type: RGB 664 | transparency: alpha_channel 665 | - file: ${replying_illustration_file} 666 | id: casita_replying 667 | resize: 320x240 668 | type: RGB 669 | transparency: alpha_channel 670 | - file: ${timer_finished_illustration_file} 671 | id: casita_timer_finished 672 | resize: 320x240 673 | type: RGB 674 | transparency: alpha_channel 675 | - file: ${loading_illustration_file} 676 | id: casita_initializing 677 | resize: 320x240 678 | type: RGB 679 | transparency: alpha_channel 680 | - file: https://github.com/esphome/wake-word-voice-assistants/raw/main/error_box_illustrations/error-no-wifi.png 681 | id: error_no_wifi 682 | resize: 320x240 683 | type: RGB 684 | transparency: alpha_channel 685 | - file: https://github.com/esphome/wake-word-voice-assistants/raw/main/error_box_illustrations/error-no-ha.png 686 | id: error_no_ha 687 | resize: 320x240 688 | type: RGB 689 | transparency: alpha_channel 690 | 691 | font: 692 | - file: 693 | type: gfonts 694 | family: ${font_family} 695 | weight: 300 696 | italic: true 697 | id: font_request 698 | size: 15 699 | glyphsets: 700 | - ${font_glyphsets} 701 | - file: 702 | type: gfonts 703 | family: ${font_family} 704 | weight: 300 705 | id: font_response 706 | size: 15 707 | glyphsets: 708 | - ${font_glyphsets} 709 | - file: 710 | type: gfonts 711 | family: ${font_family} 712 | weight: 300 713 | id: font_timer 714 | size: 30 715 | glyphsets: 716 | - ${font_glyphsets} 717 | 718 | text_sensor: 719 | - id: text_request 720 | platform: template 721 | on_value: 722 | lambda: |- 723 | if(id(text_request).state.length()>32) { 724 | std::string name = id(text_request).state.c_str(); 725 | std::string truncated = esphome::str_truncate(name.c_str(),31); 726 | id(text_request).state = (truncated+"...").c_str(); 727 | } 728 | 729 | - id: text_response 730 | platform: template 731 | on_value: 732 | lambda: |- 733 | if(id(text_response).state.length()>32) { 734 | std::string name = id(text_response).state.c_str(); 735 | std::string truncated = esphome::str_truncate(name.c_str(),31); 736 | id(text_response).state = (truncated+"...").c_str(); 737 | } 738 | 739 | color: 740 | - id: idle_color 741 | hex: ${idle_illustration_background_color} 742 | - id: listening_color 743 | hex: ${listening_illustration_background_color} 744 | - id: thinking_color 745 | hex: ${thinking_illustration_background_color} 746 | - id: replying_color 747 | hex: ${replying_illustration_background_color} 748 | - id: loading_color 749 | hex: ${loading_illustration_background_color} 750 | - id: error_color 751 | hex: ${error_illustration_background_color} 752 | - id: active_timer_color 753 | hex: "26ed3a" 754 | - id: paused_timer_color 755 | hex: "3b89e3" 756 | 757 | spi: 758 | - id: spi_bus 759 | clk_pin: 7 760 | mosi_pin: 6 761 | 762 | display: 763 | - platform: ili9xxx 764 | id: s3_box_lcd 765 | model: S3BOX 766 | invert_colors: false 767 | data_rate: 40MHz 768 | cs_pin: 5 769 | dc_pin: 4 770 | reset_pin: 771 | number: 48 772 | inverted: true 773 | update_interval: never 774 | pages: 775 | - id: idle_page 776 | lambda: |- 777 | it.fill(id(idle_color)); 778 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_idle), ImageAlign::CENTER); 779 | id(draw_timer_timeline).execute(); 780 | id(draw_active_timer_widget).execute(); 781 | - id: listening_page 782 | lambda: |- 783 | it.fill(id(listening_color)); 784 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_listening), ImageAlign::CENTER); 785 | id(draw_timer_timeline).execute(); 786 | - id: thinking_page 787 | lambda: |- 788 | it.fill(id(thinking_color)); 789 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_thinking), ImageAlign::CENTER); 790 | it.filled_rectangle(20 , 20 , 280 , 30 , Color::WHITE ); 791 | it.rectangle(20 , 20 , 280 , 30 , Color::BLACK ); 792 | it.printf(30, 25, id(font_request), Color::BLACK, "%s", id(text_request).state.c_str()); 793 | id(draw_timer_timeline).execute(); 794 | - id: replying_page 795 | lambda: |- 796 | it.fill(id(replying_color)); 797 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_replying), ImageAlign::CENTER); 798 | it.filled_rectangle(20 , 20 , 280 , 30 , Color::WHITE ); 799 | it.rectangle(20 , 20 , 280 , 30 , Color::BLACK ); 800 | it.filled_rectangle(20 , 190 , 280 , 30 , Color::WHITE ); 801 | it.rectangle(20 , 190 , 280 , 30 , Color::BLACK ); 802 | it.printf(30, 25, id(font_request), Color::BLACK, "%s", id(text_request).state.c_str()); 803 | it.printf(30, 195, id(font_response), Color::BLACK, "%s", id(text_response).state.c_str()); 804 | id(draw_timer_timeline).execute(); 805 | - id: timer_finished_page 806 | lambda: |- 807 | it.fill(id(idle_color)); 808 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_timer_finished), ImageAlign::CENTER); 809 | - id: error_page 810 | lambda: |- 811 | it.fill(id(error_color)); 812 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_error), ImageAlign::CENTER); 813 | - id: no_ha_page 814 | lambda: |- 815 | it.image((it.get_width() / 2), (it.get_height() / 2), id(error_no_ha), ImageAlign::CENTER); 816 | - id: no_wifi_page 817 | lambda: |- 818 | it.image((it.get_width() / 2), (it.get_height() / 2), id(error_no_wifi), ImageAlign::CENTER); 819 | - id: initializing_page 820 | lambda: |- 821 | it.fill(id(loading_color)); 822 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_initializing), ImageAlign::CENTER); 823 | - id: muted_page 824 | lambda: |- 825 | it.fill(Color::BLACK); 826 | id(draw_timer_timeline).execute(); 827 | id(draw_active_timer_widget).execute(); 828 | -------------------------------------------------------------------------------- /esp32-s3-box-lite/esp32-s3-box-lite.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | substitutions: 3 | name: esp32-s3-box-lite 4 | friendly_name: ESP32 S3 Box Lite 5 | loading_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/loading_320_240.png 6 | idle_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/idle_320_240.png 7 | listening_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/listening_320_240.png 8 | thinking_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/thinking_320_240.png 9 | replying_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/replying_320_240.png 10 | error_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/error_320_240.png 11 | timer_finished_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/timer_finished_320_240.png 12 | 13 | loading_illustration_background_color: "000000" 14 | idle_illustration_background_color: "000000" 15 | listening_illustration_background_color: "FFFFFF" 16 | thinking_illustration_background_color: "FFFFFF" 17 | replying_illustration_background_color: "FFFFFF" 18 | error_illustration_background_color: "000000" 19 | 20 | voice_assist_idle_phase_id: "1" 21 | voice_assist_listening_phase_id: "2" 22 | voice_assist_thinking_phase_id: "3" 23 | voice_assist_replying_phase_id: "4" 24 | voice_assist_not_ready_phase_id: "10" 25 | voice_assist_error_phase_id: "11" 26 | voice_assist_muted_phase_id: "12" 27 | voice_assist_timer_finished_phase_id: "20" 28 | 29 | # These unique characters have been extracted from every test file of every language available on https://github.com/home-assistant/intents (14 March 2024) 30 | # However, the Figtree font only contains Latin characters, so there is no point using this... unlessyou change the font configuration accordingly. 31 | allowed_characters: " !#%'()+,-./0123456789:;<>?@ABCDEFGHIJKLMNOPQRSTUVWYZ[]_abcdefghijklmnopqrstuvwxyz{|}°²³µ¿ÁÂÄÅÉÖÚßàáâãäåæçèéêëìíîðñòóôõöøùúûüýþāăąćčďĐđēėęěğĮįıļľŁłńňőřśšťũūůűųźŻżŽžơưșțΆΈΌΐΑΒΓΔΕΖΗΘΚΜΝΠΡΣΤΥΦάέήίαβγδεζηθικλμνξοπρςστυφχψωϊόύώАБВГДЕЖЗИКЛМНОПРСТУХЦЧШЪЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяёђєіїјљњћאבגדהוזחטיכלםמןנסעפץצקרשת،ءآأإئابةتجحخدذرزسشصضطظعغفقكلمنهوىيٹپچڈکگںھہیےংকচতধনফবযরলশষস়ািু্చయలిెొ్ംഅആഇഈഉഎഓകഗങചജഞടഡണതദധനപഫബഭമയരറലളവശസഹാിീുൂെേൈ്ൺൻർൽൾაბგდევზთილმნოპრსტუფქყშჩცძჭხạảấầẩậắặẹẽếềểệỉịọỏốồổỗộớờởợụủứừửữựỳ—、一上不个中为主乾了些亮人任低佔何作供依侧係個側偵充光入全关冇冷几切到制前動區卧厅厨及口另右吊后吗启吸呀咗哪唔問啟嗎嘅嘛器圍在场執場外多大始安定客室家密寵对將小少左已帘常幫幾库度庫廊廚廳开式後恆感態成我戲戶户房所扇手打执把拔换掉控插摄整斯新明是景暗更最會有未本模機檯櫃欄次正氏水沒没洗活派温測源溫漏潮激濕灯為無煙照熱燈燥物狀玄现現瓦用發的盞目着睡私空窗立笛管節簾籬紅線红罐置聚聲脚腦腳臥色节著行衣解設調請謝警设调走路車车运連遊運過道邊部都量鎖锁門閂閉開關门闭除隱離電震霧面音頂題顏颜風风食餅餵가간감갔강개거게겨결경고공과관그금급기길깥꺼껐꼽나난내네놀누는능니다닫담대더데도동됐되된됨둡드든등디때떤뜨라래러렇렌려로료른를리림링마많명몇모무문물뭐바밝방배변보부불블빨뽑사산상색서설성세센션소쇼수스습시신실싱아안않알았애야어얼업없었에여연열옆오온완외왼요운움워원위으은을음의이인일임입있작잠장재전절정제져조족종주줄중줘지직진짐쪽차창천최추출충치침커컴켜켰쿠크키탁탄태탬터텔통트튼티파팬퍼폰표퓨플핑한함해했행혀현화활후휴힘,?" 32 | 33 | # Add support for non-unicode characters by using better glyphset 34 | font_glyphsets: "GF_Latin_Core" 35 | # for Greek use "Noto Sans" for other languages use a compatible font family 36 | font_family: Figtree 37 | 38 | esphome: 39 | name: ${name} 40 | friendly_name: ${friendly_name} 41 | min_version: 2025.5.0 42 | name_add_mac_suffix: true 43 | on_boot: 44 | priority: 600 45 | then: 46 | - script.execute: draw_display 47 | - delay: 30s 48 | - if: 49 | condition: 50 | lambda: return id(init_in_progress); 51 | then: 52 | - lambda: id(init_in_progress) = false; 53 | - script.execute: draw_display 54 | 55 | esp32: 56 | board: esp32s3box 57 | flash_size: 16MB 58 | cpu_frequency: 240MHz 59 | framework: 60 | type: esp-idf 61 | sdkconfig_options: 62 | CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y" 63 | CONFIG_ESP32S3_DATA_CACHE_64KB: "y" 64 | CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y" 65 | 66 | psram: 67 | mode: octal 68 | speed: 80MHz 69 | 70 | api: 71 | on_client_connected: 72 | - script.execute: draw_display 73 | on_client_disconnected: 74 | - script.execute: draw_display 75 | 76 | ota: 77 | - platform: esphome 78 | id: ota_esphome 79 | 80 | logger: 81 | hardware_uart: USB_SERIAL_JTAG 82 | 83 | wifi: 84 | ap: 85 | on_connect: 86 | - script.execute: draw_display 87 | on_disconnect: 88 | - script.execute: draw_display 89 | 90 | captive_portal: 91 | 92 | button: 93 | - platform: factory_reset 94 | id: factory_reset_btn 95 | internal: true 96 | 97 | sensor: 98 | - platform: adc 99 | pin: GPIO1 100 | id: front_buttons 101 | internal: true 102 | update_interval: 16ms 103 | attenuation: 11db 104 | on_value: 105 | - lambda: |- 106 | // none: 3.121 107 | // left: 2.392 108 | // middle: 1.965 109 | // left+middle: 1.600 110 | // right: 0.794 111 | // left+right: 0.726 112 | // middle+right: 0.682 113 | // all: 0.632 114 | if (x > 3) { 115 | id(left).publish_state(false); 116 | id(middle).publish_state(false); 117 | id(right).publish_state(false); 118 | } else if (x > 2.3) { 119 | id(left).publish_state(true); 120 | id(middle).publish_state(false); 121 | id(right).publish_state(false); 122 | } else if (x > 1.9) { 123 | id(left).publish_state(false); 124 | id(middle).publish_state(true); 125 | id(right).publish_state(false); 126 | } else if (x > 1) { 127 | id(left).publish_state(true); 128 | id(middle).publish_state(true); 129 | id(right).publish_state(false); 130 | } else if (x > 0.73) { 131 | id(left).publish_state(false); 132 | id(middle).publish_state(false); 133 | id(right).publish_state(true); 134 | } else if (x > 0.7) { 135 | id(left).publish_state(true); 136 | id(middle).publish_state(false); 137 | id(right).publish_state(true); 138 | } else if (x > 0.65) { 139 | id(left).publish_state(false); 140 | id(middle).publish_state(true); 141 | id(right).publish_state(true); 142 | } else { 143 | id(left).publish_state(true); 144 | id(middle).publish_state(true); 145 | id(right).publish_state(true); 146 | } 147 | 148 | binary_sensor: 149 | - platform: template 150 | id: left 151 | internal: true 152 | on_press: 153 | - switch.turn_off: timer_ringing 154 | - platform: template 155 | id: middle 156 | internal: true 157 | on_press: 158 | - switch.turn_off: timer_ringing 159 | - platform: template 160 | id: right 161 | internal: true 162 | on_press: 163 | - switch.turn_off: timer_ringing 164 | 165 | - platform: gpio 166 | pin: 167 | number: GPIO0 168 | mode: INPUT_PULLUP 169 | inverted: true 170 | id: left_top_button 171 | internal: true 172 | on_multi_click: 173 | - timing: 174 | - ON for at least 50ms 175 | - OFF for at least 50ms 176 | then: 177 | - switch.turn_off: timer_ringing 178 | - timing: 179 | - ON for at least 10s 180 | then: 181 | - button.press: factory_reset_btn 182 | 183 | output: 184 | - platform: ledc 185 | pin: GPIO45 186 | inverted: true 187 | id: backlight_output 188 | 189 | light: 190 | - platform: monochromatic 191 | id: led 192 | name: Screen 193 | icon: "mdi:television" 194 | entity_category: config 195 | output: backlight_output 196 | restore_mode: RESTORE_DEFAULT_ON 197 | default_transition_length: 250ms 198 | 199 | i2c: 200 | scl: GPIO18 201 | sda: GPIO8 202 | 203 | i2s_audio: 204 | - id: i2s_audio_bus 205 | i2s_lrclk_pin: GPIO47 206 | i2s_bclk_pin: GPIO17 207 | i2s_mclk_pin: GPIO2 208 | 209 | audio_adc: 210 | - platform: es7243e 211 | id: es7243e_adc 212 | 213 | audio_dac: 214 | - platform: es8156 215 | id: es8156_dac 216 | 217 | microphone: 218 | - platform: i2s_audio 219 | id: box_mic 220 | sample_rate: 16000 221 | i2s_din_pin: GPIO16 222 | bits_per_sample: 16bit 223 | adc_type: external 224 | 225 | speaker: 226 | - platform: i2s_audio 227 | id: box_speaker 228 | i2s_dout_pin: GPIO15 229 | dac_type: external 230 | sample_rate: 48000 231 | bits_per_sample: 16bit 232 | channel: left 233 | audio_dac: es8156_dac 234 | buffer_duration: 100ms 235 | 236 | media_player: 237 | - platform: speaker 238 | name: None 239 | id: speaker_media_player 240 | volume_min: 0.5 241 | volume_max: 0.8 242 | announcement_pipeline: 243 | speaker: box_speaker 244 | format: FLAC 245 | sample_rate: 48000 246 | num_channels: 1 # S3 Box only has one output channel 247 | files: 248 | - id: timer_finished_sound 249 | file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/timer_finished.flac 250 | on_announcement: 251 | # Stop the wake word (mWW or VA) if the mic is capturing 252 | - if: 253 | condition: 254 | - microphone.is_capturing: 255 | then: 256 | - script.execute: stop_wake_word 257 | # Ensure VA stops before moving on 258 | - if: 259 | condition: 260 | - lambda: return id(wake_word_engine_location).state == "In Home Assistant"; 261 | then: 262 | - wait_until: 263 | - not: 264 | voice_assistant.is_running: 265 | # Since VA isn't running, this is user-intiated media playback. Draw the mute display 266 | - if: 267 | condition: 268 | not: 269 | voice_assistant.is_running: 270 | then: 271 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 272 | - script.execute: draw_display 273 | on_idle: 274 | # Since VA isn't running, this is the end of user-intiated media playback. Restart the wake word. 275 | - if: 276 | condition: 277 | not: 278 | voice_assistant.is_running: 279 | then: 280 | - script.execute: start_wake_word 281 | - script.execute: set_idle_or_mute_phase 282 | - script.execute: draw_display 283 | 284 | micro_wake_word: 285 | id: mww 286 | models: 287 | - okay_nabu 288 | - hey_mycroft 289 | - hey_jarvis 290 | on_wake_word_detected: 291 | - voice_assistant.start: 292 | wake_word: !lambda return wake_word; 293 | 294 | voice_assistant: 295 | id: va 296 | microphone: box_mic 297 | media_player: speaker_media_player 298 | micro_wake_word: mww 299 | noise_suppression_level: 2 300 | auto_gain: 31dBFS 301 | volume_multiplier: 2.0 302 | on_listening: 303 | - lambda: id(voice_assistant_phase) = ${voice_assist_listening_phase_id}; 304 | - text_sensor.template.publish: 305 | id: text_request 306 | state: "..." 307 | - text_sensor.template.publish: 308 | id: text_response 309 | state: "..." 310 | - script.execute: draw_display 311 | on_stt_vad_end: 312 | - lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id}; 313 | - script.execute: draw_display 314 | on_stt_end: 315 | - text_sensor.template.publish: 316 | id: text_request 317 | state: !lambda return x; 318 | - script.execute: draw_display 319 | on_tts_start: 320 | - text_sensor.template.publish: 321 | id: text_response 322 | state: !lambda return x; 323 | - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; 324 | - script.execute: draw_display 325 | on_end: 326 | # Wait a short amount of time to see if an announcement starts 327 | - wait_until: 328 | condition: 329 | - media_player.is_announcing: 330 | timeout: 0.5s 331 | # Announcement is finished and the I2S bus is free 332 | - wait_until: 333 | - and: 334 | - not: 335 | media_player.is_announcing: 336 | - not: 337 | speaker.is_playing: 338 | # Restart only mWW if enabled; streaming wake words automatically restart 339 | - if: 340 | condition: 341 | - lambda: return id(wake_word_engine_location).state == "On device"; 342 | then: 343 | - lambda: id(va).set_use_wake_word(false); 344 | - micro_wake_word.start: 345 | - script.execute: set_idle_or_mute_phase 346 | - script.execute: draw_display 347 | # Clear text sensors 348 | - text_sensor.template.publish: 349 | id: text_request 350 | state: "" 351 | - text_sensor.template.publish: 352 | id: text_response 353 | state: "" 354 | on_error: 355 | - if: 356 | condition: 357 | lambda: return !id(init_in_progress); 358 | then: 359 | - lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id}; 360 | - script.execute: draw_display 361 | - delay: 1s 362 | - if: 363 | condition: 364 | switch.is_off: mute 365 | then: 366 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 367 | else: 368 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 369 | - script.execute: draw_display 370 | on_client_connected: 371 | - lambda: id(init_in_progress) = false; 372 | - script.execute: start_wake_word 373 | - script.execute: set_idle_or_mute_phase 374 | - script.execute: draw_display 375 | on_client_disconnected: 376 | - script.execute: stop_wake_word 377 | - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id}; 378 | - script.execute: draw_display 379 | on_timer_started: 380 | - script.execute: draw_display 381 | on_timer_cancelled: 382 | - script.execute: draw_display 383 | on_timer_updated: 384 | - script.execute: draw_display 385 | on_timer_tick: 386 | - script.execute: draw_display 387 | on_timer_finished: 388 | - switch.turn_on: timer_ringing 389 | - wait_until: 390 | media_player.is_announcing: 391 | - lambda: id(voice_assistant_phase) = ${voice_assist_timer_finished_phase_id}; 392 | - script.execute: draw_display 393 | 394 | script: 395 | - id: draw_display 396 | then: 397 | - if: 398 | condition: 399 | lambda: return !id(init_in_progress); 400 | then: 401 | - if: 402 | condition: 403 | wifi.connected: 404 | then: 405 | - if: 406 | condition: 407 | api.connected: 408 | then: 409 | - lambda: | 410 | switch(id(voice_assistant_phase)) { 411 | case ${voice_assist_listening_phase_id}: 412 | id(s3_box_lcd).show_page(listening_page); 413 | id(s3_box_lcd).update(); 414 | break; 415 | case ${voice_assist_thinking_phase_id}: 416 | id(s3_box_lcd).show_page(thinking_page); 417 | id(s3_box_lcd).update(); 418 | break; 419 | case ${voice_assist_replying_phase_id}: 420 | id(s3_box_lcd).show_page(replying_page); 421 | id(s3_box_lcd).update(); 422 | break; 423 | case ${voice_assist_error_phase_id}: 424 | id(s3_box_lcd).show_page(error_page); 425 | id(s3_box_lcd).update(); 426 | break; 427 | case ${voice_assist_muted_phase_id}: 428 | id(s3_box_lcd).show_page(muted_page); 429 | id(s3_box_lcd).update(); 430 | break; 431 | case ${voice_assist_not_ready_phase_id}: 432 | id(s3_box_lcd).show_page(no_ha_page); 433 | id(s3_box_lcd).update(); 434 | break; 435 | case ${voice_assist_timer_finished_phase_id}: 436 | id(s3_box_lcd).show_page(timer_finished_page); 437 | id(s3_box_lcd).update(); 438 | break; 439 | default: 440 | id(s3_box_lcd).show_page(idle_page); 441 | id(s3_box_lcd).update(); 442 | } 443 | else: 444 | - display.page.show: no_ha_page 445 | - component.update: s3_box_lcd 446 | else: 447 | - display.page.show: no_wifi_page 448 | - component.update: s3_box_lcd 449 | else: 450 | - display.page.show: initializing_page 451 | - component.update: s3_box_lcd 452 | 453 | - id: fetch_first_active_timer 454 | then: 455 | - lambda: | 456 | const auto timers = id(va).get_timers(); 457 | auto output_timer = timers.begin()->second; 458 | for (auto &iterable_timer : timers) { 459 | if (iterable_timer.second.is_active && iterable_timer.second.seconds_left <= output_timer.seconds_left) { 460 | output_timer = iterable_timer.second; 461 | } 462 | } 463 | id(global_first_active_timer) = output_timer; 464 | - id: check_if_timers_active 465 | then: 466 | - lambda: | 467 | const auto timers = id(va).get_timers(); 468 | bool output = false; 469 | if (timers.size() > 0) { 470 | for (auto &iterable_timer : timers) { 471 | if(iterable_timer.second.is_active) { 472 | output = true; 473 | } 474 | } 475 | } 476 | id(global_is_timer_active) = output; 477 | - id: fetch_first_timer 478 | then: 479 | - lambda: | 480 | const auto timers = id(va).get_timers(); 481 | auto output_timer = timers.begin()->second; 482 | for (auto &iterable_timer : timers) { 483 | if (iterable_timer.second.seconds_left <= output_timer.seconds_left) { 484 | output_timer = iterable_timer.second; 485 | } 486 | } 487 | id(global_first_timer) = output_timer; 488 | - id: check_if_timers 489 | then: 490 | - lambda: | 491 | const auto timers = id(va).get_timers(); 492 | bool output = false; 493 | if (timers.size() > 0) { 494 | output = true; 495 | } 496 | id(global_is_timer) = output; 497 | 498 | - id: draw_timer_timeline 499 | then: 500 | - lambda: | 501 | id(check_if_timers_active).execute(); 502 | id(check_if_timers).execute(); 503 | if (id(global_is_timer_active)){ 504 | id(fetch_first_active_timer).execute(); 505 | int active_pixels = round( 320 * id(global_first_active_timer).seconds_left / max(id(global_first_active_timer).total_seconds , static_cast(1)) ); 506 | if (active_pixels > 0){ 507 | id(s3_box_lcd).filled_rectangle(0 , 225 , 320 , 15 , Color::WHITE ); 508 | id(s3_box_lcd).filled_rectangle(0 , 226 , active_pixels , 13 , id(active_timer_color) ); 509 | } 510 | } else if (id(global_is_timer)){ 511 | id(fetch_first_timer).execute(); 512 | int active_pixels = round( 320 * id(global_first_timer).seconds_left / max(id(global_first_timer).total_seconds , static_cast(1))); 513 | if (active_pixels > 0){ 514 | id(s3_box_lcd).filled_rectangle(0 , 225 , 320 , 15 , Color::WHITE ); 515 | id(s3_box_lcd).filled_rectangle(0 , 226 , active_pixels , 13 , id(paused_timer_color) ); 516 | } 517 | } 518 | - id: draw_active_timer_widget 519 | then: 520 | - lambda: | 521 | id(check_if_timers_active).execute(); 522 | if (id(global_is_timer_active)){ 523 | id(s3_box_lcd).filled_rectangle(80 , 40 , 160 , 50 , Color::WHITE ); 524 | id(s3_box_lcd).rectangle(80 , 40 , 160 , 50 , Color::BLACK ); 525 | 526 | id(fetch_first_active_timer).execute(); 527 | int hours_left = floor(id(global_first_active_timer).seconds_left / 3600); 528 | int minutes_left = floor((id(global_first_active_timer).seconds_left - hours_left * 3600) / 60); 529 | int seconds_left = id(global_first_active_timer).seconds_left - hours_left * 3600 - minutes_left * 60 ; 530 | auto display_hours = (hours_left < 10 ? "0" : "") + std::to_string(hours_left); 531 | auto display_minute = (minutes_left < 10 ? "0" : "") + std::to_string(minutes_left); 532 | auto display_seconds = (seconds_left < 10 ? "0" : "") + std::to_string(seconds_left) ; 533 | 534 | std::string display_string = ""; 535 | if (hours_left > 0) { 536 | display_string = display_hours + ":" + display_minute; 537 | } else { 538 | display_string = display_minute + ":" + display_seconds; 539 | } 540 | id(s3_box_lcd).printf(120, 47, id(font_timer), Color::BLACK, "%s", display_string.c_str()); 541 | } 542 | # Starts either mWW or the streaming wake word, depending on the configured location 543 | - id: start_wake_word 544 | then: 545 | - if: 546 | condition: 547 | and: 548 | - not: 549 | - voice_assistant.is_running: 550 | - lambda: return id(wake_word_engine_location).state == "On device"; 551 | then: 552 | - lambda: id(va).set_use_wake_word(false); 553 | - micro_wake_word.start: 554 | - if: 555 | condition: 556 | and: 557 | - not: 558 | - voice_assistant.is_running: 559 | - lambda: return id(wake_word_engine_location).state == "In Home Assistant"; 560 | then: 561 | - lambda: id(va).set_use_wake_word(true); 562 | - voice_assistant.start_continuous: 563 | # Stops either mWW or the streaming wake word, depending on the configured location 564 | - id: stop_wake_word 565 | then: 566 | - if: 567 | condition: 568 | lambda: return id(wake_word_engine_location).state == "In Home Assistant"; 569 | then: 570 | - lambda: id(va).set_use_wake_word(false); 571 | - voice_assistant.stop: 572 | - if: 573 | condition: 574 | lambda: return id(wake_word_engine_location).state == "On device"; 575 | then: 576 | - micro_wake_word.stop: 577 | # Set the voice assistant phase to idle or muted, depending on if the software mute switch is activated 578 | - id: set_idle_or_mute_phase 579 | then: 580 | - if: 581 | condition: 582 | switch.is_off: mute 583 | then: 584 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 585 | else: 586 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 587 | 588 | switch: 589 | - platform: gpio 590 | name: Speaker Enable 591 | pin: GPIO46 592 | restore_mode: RESTORE_DEFAULT_ON 593 | entity_category: config 594 | disabled_by_default: true 595 | - platform: template 596 | name: Mute 597 | id: mute 598 | icon: "mdi:microphone-off" 599 | optimistic: true 600 | restore_mode: RESTORE_DEFAULT_OFF 601 | entity_category: config 602 | on_turn_off: 603 | - microphone.unmute: 604 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 605 | - script.execute: draw_display 606 | on_turn_on: 607 | - microphone.mute: 608 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 609 | - script.execute: draw_display 610 | - platform: template 611 | id: timer_ringing 612 | optimistic: true 613 | internal: true 614 | restore_mode: ALWAYS_OFF 615 | on_turn_off: 616 | # Turn off the repeat mode and disable the pause between playlist items 617 | - lambda: |- 618 | id(speaker_media_player) 619 | ->make_call() 620 | .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_OFF) 621 | .set_announcement(true) 622 | .perform(); 623 | id(speaker_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 0); 624 | # Stop playing the alarm 625 | - media_player.stop: 626 | announcement: true 627 | on_turn_on: 628 | # Turn on the repeat mode and pause for 1000 ms between playlist items/repeats 629 | - lambda: |- 630 | id(speaker_media_player) 631 | ->make_call() 632 | .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ONE) 633 | .set_announcement(true) 634 | .perform(); 635 | id(speaker_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 1000); 636 | - media_player.speaker.play_on_device_media_file: 637 | media_file: timer_finished_sound 638 | announcement: true 639 | - delay: 15min 640 | - switch.turn_off: timer_ringing 641 | 642 | select: 643 | - platform: template 644 | entity_category: config 645 | name: Wake word engine location 646 | id: wake_word_engine_location 647 | icon: "mdi:account-voice" 648 | optimistic: true 649 | restore_value: true 650 | options: 651 | - In Home Assistant 652 | - On device 653 | initial_option: On device 654 | on_value: 655 | - if: 656 | condition: 657 | lambda: return !id(init_in_progress); 658 | then: 659 | - wait_until: 660 | lambda: return id(voice_assistant_phase) == ${voice_assist_muted_phase_id} || id(voice_assistant_phase) == ${voice_assist_idle_phase_id}; 661 | - if: 662 | condition: 663 | lambda: return x == "In Home Assistant"; 664 | then: 665 | - micro_wake_word.stop 666 | - delay: 500ms 667 | - if: 668 | condition: 669 | switch.is_off: mute 670 | then: 671 | - lambda: id(va).set_use_wake_word(true); 672 | - voice_assistant.start_continuous: 673 | - if: 674 | condition: 675 | lambda: return x == "On device"; 676 | then: 677 | - lambda: id(va).set_use_wake_word(false); 678 | - voice_assistant.stop 679 | - delay: 500ms 680 | - if: 681 | condition: 682 | switch.is_off: mute 683 | then: 684 | - micro_wake_word.start 685 | 686 | globals: 687 | - id: init_in_progress 688 | type: bool 689 | restore_value: false 690 | initial_value: "true" 691 | - id: voice_assistant_phase 692 | type: int 693 | restore_value: false 694 | initial_value: ${voice_assist_not_ready_phase_id} 695 | - id: global_first_active_timer 696 | type: voice_assistant::Timer 697 | restore_value: false 698 | - id: global_is_timer_active 699 | type: bool 700 | restore_value: false 701 | - id: global_first_timer 702 | type: voice_assistant::Timer 703 | restore_value: false 704 | - id: global_is_timer 705 | type: bool 706 | restore_value: false 707 | 708 | image: 709 | - file: ${error_illustration_file} 710 | id: casita_error 711 | resize: 320x240 712 | type: RGB 713 | transparency: alpha_channel 714 | - file: ${idle_illustration_file} 715 | id: casita_idle 716 | resize: 320x240 717 | type: RGB 718 | transparency: alpha_channel 719 | - file: ${listening_illustration_file} 720 | id: casita_listening 721 | resize: 320x240 722 | type: RGB 723 | transparency: alpha_channel 724 | - file: ${thinking_illustration_file} 725 | id: casita_thinking 726 | resize: 320x240 727 | type: RGB 728 | transparency: alpha_channel 729 | - file: ${replying_illustration_file} 730 | id: casita_replying 731 | resize: 320x240 732 | type: RGB 733 | transparency: alpha_channel 734 | - file: ${timer_finished_illustration_file} 735 | id: casita_timer_finished 736 | resize: 320x240 737 | type: RGB 738 | transparency: alpha_channel 739 | - file: ${loading_illustration_file} 740 | id: casita_initializing 741 | resize: 320x240 742 | type: RGB 743 | transparency: alpha_channel 744 | - file: https://github.com/esphome/wake-word-voice-assistants/raw/main/error_box_illustrations/error-no-wifi.png 745 | id: error_no_wifi 746 | resize: 320x240 747 | type: RGB 748 | transparency: alpha_channel 749 | - file: https://github.com/esphome/wake-word-voice-assistants/raw/main/error_box_illustrations/error-no-ha.png 750 | id: error_no_ha 751 | resize: 320x240 752 | type: RGB 753 | transparency: alpha_channel 754 | 755 | font: 756 | - file: 757 | type: gfonts 758 | family: ${font_family} 759 | weight: 300 760 | italic: true 761 | id: font_request 762 | size: 15 763 | glyphsets: 764 | - ${font_glyphsets} 765 | - file: 766 | type: gfonts 767 | family: ${font_family} 768 | weight: 300 769 | id: font_response 770 | size: 15 771 | glyphsets: 772 | - ${font_glyphsets} 773 | - file: 774 | type: gfonts 775 | family: ${font_family} 776 | weight: 300 777 | id: font_timer 778 | size: 30 779 | glyphsets: 780 | - ${font_glyphsets} 781 | 782 | text_sensor: 783 | - id: text_request 784 | platform: template 785 | on_value: 786 | lambda: |- 787 | if(id(text_request).state.length()>32) { 788 | std::string name = id(text_request).state.c_str(); 789 | std::string truncated = esphome::str_truncate(name.c_str(),31); 790 | id(text_request).state = (truncated+"...").c_str(); 791 | } 792 | 793 | - id: text_response 794 | platform: template 795 | on_value: 796 | lambda: |- 797 | if(id(text_response).state.length()>32) { 798 | std::string name = id(text_response).state.c_str(); 799 | std::string truncated = esphome::str_truncate(name.c_str(),31); 800 | id(text_response).state = (truncated+"...").c_str(); 801 | } 802 | 803 | color: 804 | - id: idle_color 805 | hex: ${idle_illustration_background_color} 806 | - id: listening_color 807 | hex: ${listening_illustration_background_color} 808 | - id: thinking_color 809 | hex: ${thinking_illustration_background_color} 810 | - id: replying_color 811 | hex: ${replying_illustration_background_color} 812 | - id: loading_color 813 | hex: ${loading_illustration_background_color} 814 | - id: error_color 815 | hex: ${error_illustration_background_color} 816 | - id: active_timer_color 817 | hex: "26ed3a" 818 | - id: paused_timer_color 819 | hex: "3b89e3" 820 | 821 | spi: 822 | - id: spi_bus 823 | clk_pin: 7 824 | mosi_pin: 6 825 | 826 | display: 827 | - platform: ili9xxx 828 | id: s3_box_lcd 829 | model: S3BOX_LITE 830 | invert_colors: true 831 | data_rate: 40MHz 832 | cs_pin: 5 833 | dc_pin: 4 834 | reset_pin: 48 835 | update_interval: never 836 | pages: 837 | - id: idle_page 838 | lambda: |- 839 | it.fill(id(idle_color)); 840 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_idle), ImageAlign::CENTER); 841 | id(draw_timer_timeline).execute(); 842 | id(draw_active_timer_widget).execute(); 843 | - id: listening_page 844 | lambda: |- 845 | it.fill(id(listening_color)); 846 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_listening), ImageAlign::CENTER); 847 | id(draw_timer_timeline).execute(); 848 | - id: thinking_page 849 | lambda: |- 850 | it.fill(id(thinking_color)); 851 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_thinking), ImageAlign::CENTER); 852 | it.filled_rectangle(20 , 20 , 280 , 30 , Color::WHITE ); 853 | it.rectangle(20 , 20 , 280 , 30 , Color::BLACK ); 854 | it.printf(30, 25, id(font_request), Color::BLACK, "%s", id(text_request).state.c_str()); 855 | id(draw_timer_timeline).execute(); 856 | - id: replying_page 857 | lambda: |- 858 | it.fill(id(replying_color)); 859 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_replying), ImageAlign::CENTER); 860 | it.filled_rectangle(20 , 20 , 280 , 30 , Color::WHITE ); 861 | it.rectangle(20 , 20 , 280 , 30 , Color::BLACK ); 862 | it.filled_rectangle(20 , 190 , 280 , 30 , Color::WHITE ); 863 | it.rectangle(20 , 190 , 280 , 30 , Color::BLACK ); 864 | it.printf(30, 25, id(font_request), Color::BLACK, "%s", id(text_request).state.c_str()); 865 | it.printf(30, 195, id(font_response), Color::BLACK, "%s", id(text_response).state.c_str()); 866 | id(draw_timer_timeline).execute(); 867 | - id: timer_finished_page 868 | lambda: |- 869 | it.fill(id(idle_color)); 870 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_timer_finished), ImageAlign::CENTER); 871 | - id: error_page 872 | lambda: |- 873 | it.fill(id(error_color)); 874 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_error), ImageAlign::CENTER); 875 | - id: no_ha_page 876 | lambda: |- 877 | it.image((it.get_width() / 2), (it.get_height() / 2), id(error_no_ha), ImageAlign::CENTER); 878 | - id: no_wifi_page 879 | lambda: |- 880 | it.image((it.get_width() / 2), (it.get_height() / 2), id(error_no_wifi), ImageAlign::CENTER); 881 | - id: initializing_page 882 | lambda: |- 883 | it.fill(id(loading_color)); 884 | it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_initializing), ImageAlign::CENTER); 885 | - id: muted_page 886 | lambda: |- 887 | it.fill(Color::BLACK); 888 | id(draw_timer_timeline).execute(); 889 | id(draw_active_timer_widget).execute(); 890 | --------------------------------------------------------------------------------