├── ESP Assistant v2.yaml ├── ESP Assistant v3.yaml ├── ESP Assistant.yaml ├── README.md ├── hardware ├── VoicePuck-body.stl ├── VoicePuck-cover.stl ├── VoicePuck_V2-body.stl ├── VoicePuck_V2-lid.stl └── Voicepuck-body.png ├── media ├── HA-device.png ├── VoicePuck-GoogleHome.jpg ├── VoicePuck-bottom.jpg ├── VoicePuck-double.jpg ├── VoicePuck-top.jpg ├── VoicePuck-white.jpg └── schematic.png └── sounds └── timer_finished.mp3 /ESP Assistant v2.yaml: -------------------------------------------------------------------------------- 1 | # ESP Voice Assistant 2 | # 18 Aug 2024 (v 2.0.0) 3 | # - Based on the ESP32-S3_BOX version 4 | # addapted to work without a screen but with I2s amp and mic 5 | # Added some LED's for interacting 6 | # Request and response sensors in HA 7 | # 8 | # by A.A. van Zoelen 9 | # Based on the work of Giants 10 | 11 | substitutions: 12 | name: "esp-assistant" 13 | friendly_name: ESP Assistant 14 | 15 | micro_wake_word_model: okay_nabu # Other options are: hey_jarvis or alexa 16 | timer_alarm_sound: "sounds/timer_finished.wav" # in the esphome/sounds folder 17 | 18 | voice_assist_idle_phase_id: "1" 19 | voice_assist_listening_phase_id: "2" 20 | voice_assist_thinking_phase_id: "3" 21 | voice_assist_replying_phase_id: "4" 22 | voice_assist_not_ready_phase_id: "10" 23 | voice_assist_error_phase_id: "11" 24 | voice_assist_muted_phase_id: "12" 25 | voice_assist_timer_finished_phase_id: "20" 26 | 27 | # These unique characters have been extracted from every test file of every language available on https://github.com/home-assistant/intents (14 March 2024) 28 | allowed_characters: " !#%'()+,-./0123456789:;<>?@ABCDEFGHIJKLMNOPQRSTUVWYZ[]_abcdefghijklmnopqrstuvwxyz{|}°²³µ¿ÁÂÄÅÉÖÚßàáâãäåæçèéêëìíîðñòóôõöøùúûüýþāăąćčďĐđēėęěğĮįıļľŁłńňőřśšťũūůűųźŻżŽžơưșțΆΈΌΐΑΒΓΔΕΖΗΘΚΜΝΠΡΣΤΥΦάέήίαβγδεζηθικλμνξοπρςστυφχψωϊόύώАБВГДЕЖЗИКЛМНОПРСТУХЦЧШЪЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяёђєіїјљњћאבגדהוזחטיכלםמןנסעפץצקרשת،ءآأإئابةتجحخدذرزسشصضطظعغفقكلمنهوىيٹپچڈکگںھہیےংকচতধনফবযরলশষস়ািু্చయలిెొ్ംഅആഇഈഉഎഓകഗങചജഞടഡണതദധനപഫബഭമയരറലളവശസഹാിീുൂെേൈ്ൺൻർൽൾაბგდევზთილმნოპრსტუფქყშჩცძჭხạảấầẩậắặẹẽếềểệỉịọỏốồổỗộớờởợụủứừửữựỳ—、一上不个中为主乾了些亮人任低佔何作供依侧係個側偵充光入全关冇冷几切到制前動區卧厅厨及口另右吊后吗启吸呀咗哪唔問啟嗎嘅嘛器圍在场執場外多大始安定客室家密寵对將小少左已帘常幫幾库度庫廊廚廳开式後恆感態成我戲戶户房所扇手打执把拔换掉控插摄整斯新明是景暗更最會有未本模機檯櫃欄次正氏水沒没洗活派温測源溫漏潮激濕灯為無煙照熱燈燥物狀玄现現瓦用發的盞目着睡私空窗立笛管節簾籬紅線红罐置聚聲脚腦腳臥色节著行衣解設調請謝警设调走路車车运連遊運過道邊部都量鎖锁門閂閉開關门闭除隱離電震霧面音頂題顏颜風风食餅餵가간감갔강개거게겨결경고공과관그금급기길깥꺼껐꼽나난내네놀누는능니다닫담대더데도동됐되된됨둡드든등디때떤뜨라래러렇렌려로료른를리림링마많명몇모무문물뭐바밝방배변보부불블빨뽑사산상색서설성세센션소쇼수스습시신실싱아안않알았애야어얼업없었에여연열옆오온완외왼요운움워원위으은을음의이인일임입있작잠장재전절정제져조족종주줄중줘지직진짐쪽차창천최추출충치침커컴켜켰쿠크키탁탄태탬터텔통트튼티파팬퍼폰표퓨플핑한함해했행혀현화활후휴힘,?" 29 | 30 | 31 | esphome: 32 | name: ${name} 33 | friendly_name: ${friendly_name} 34 | min_version: 2024.7.0 35 | compile_process_limit: 1 # Helps when compiling on light weight systems 36 | platformio_options: 37 | board_build.flash_mode: dio 38 | project: 39 | name: AA_van_Zoelen.VoicePuck 40 | version: '2.0.0' 41 | on_boot: 42 | priority: 600 43 | then: 44 | - script.execute: control_led 45 | - delay: 30s 46 | - if: 47 | condition: 48 | lambda: return id(init_in_progress); 49 | then: 50 | - lambda: id(init_in_progress) = false; 51 | - script.execute: control_led 52 | 53 | esp32: 54 | board: esp32-s3-devkitc-1 55 | variant: esp32s3 56 | flash_size: 16MB 57 | framework: 58 | type: esp-idf 59 | version: recommended 60 | components: 61 | - name: esphome_board 62 | source: github://jesserockz/esphome-esp-adf-board@main 63 | refresh: 0s 64 | sdkconfig_options: 65 | CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y" 66 | CONFIG_ESP32S3_DATA_CACHE_64KB: "y" 67 | CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y" 68 | CONFIG_AUDIO_BOARD_CUSTOM: "y" 69 | 70 | psram: 71 | mode: octal 72 | speed: 80MHz 73 | 74 | external_components: 75 | - source: github://pr#5230 76 | components: esp_adf 77 | refresh: 0s 78 | - source: github://jesserockz/esphome-components 79 | components: [file] 80 | refresh: 0s 81 | 82 | api: 83 | on_client_connected: 84 | - script.execute: control_led 85 | on_client_disconnected: 86 | - script.execute: control_led 87 | 88 | 89 | ota: 90 | - platform: esphome 91 | id: ota_esphome 92 | 93 | logger: 94 | # hardware_uart: USB_SERIAL_JTAG 95 | 96 | wifi: 97 | ssid: !secret wifi_ssid 98 | password: !secret wifi_password 99 | output_power: 8.5dB 100 | 101 | # If the device connects, or disconnects, to the Wifi: Run the script to refresh the LED status 102 | on_connect: 103 | - script.execute: control_led 104 | on_disconnect: 105 | - script.execute: control_led 106 | 107 | # Enable fallback hotspot (captive portal) in case wifi connection fails 108 | ap: 109 | ssid: !secret fallback_ssid 110 | password: !secret fallback_password 111 | 112 | 113 | button: 114 | - platform: restart 115 | id: "restart_device" 116 | name: "Restart" 117 | entity_category: "diagnostic" 118 | - platform: safe_mode 119 | id: "restart_device_safe_mode" 120 | name: "Restart (Safe Mode)" 121 | entity_category: "diagnostic" 122 | 123 | 124 | light: 125 | - platform: esp32_rmt_led_strip 126 | rgb_order: GRB 127 | pin: GPIO17 #GPIO48 # On board light 128 | num_leds: 3 129 | rmt_channel: 0 130 | chipset: WS2812 131 | name: "Status LED" 132 | id: led_strip 133 | disabled_by_default: True 134 | entity_category: diagnostic 135 | icon: mdi:led-on 136 | default_transition_length: 0s 137 | effects: 138 | - pulse: 139 | name: "Slow Pulse" 140 | transition_length: 250ms 141 | update_interval: 250ms 142 | min_brightness: 60% 143 | max_brightness: 80% 144 | - pulse: 145 | name: "Fast Pulse" 146 | transition_length: 100ms 147 | update_interval: 100ms 148 | min_brightness: 60% 149 | max_brightness: 80% 150 | - addressable_scan: 151 | name: "Scanning" 152 | move_interval: 70ms 153 | scan_width: 1 154 | - pulse: 155 | name: "Waiting for wake word" 156 | min_brightness: 15% 157 | max_brightness: 35% 158 | transition_length: 3s # defaults to 1s 159 | update_interval: 3s 160 | 161 | 162 | esp_adf: 163 | 164 | micro_wake_word: 165 | models: 166 | - ${micro_wake_word_model} 167 | on_wake_word_detected: 168 | - voice_assistant.start: 169 | wake_word: !lambda return wake_word; 170 | 171 | 172 | # This is our two i2s buses with the correct pins. 173 | # You can refer to the wiring diagram of our voice assistant for more details 174 | i2s_audio: 175 | - id: i2s_in 176 | i2s_lrclk_pin: GPIO18 #WS 177 | i2s_bclk_pin: GPIO2 #SCK 178 | - id: i2s_out 179 | i2s_lrclk_pin: GPIO6 #LRC 180 | i2s_bclk_pin: GPIO7 #BLCK 181 | 182 | # This is the declaration of our microphone. 183 | # It includes the data pin (You can refer to the wiring diagram of our voice assistant for more details) 184 | # It references the correct i2s bus declared above. 185 | microphone: 186 | - platform: i2s_audio 187 | id: box_mic 188 | adc_type: external 189 | i2s_audio_id: i2s_in 190 | i2s_din_pin: GPIO4 191 | pdm: false 192 | channel: left 193 | bits_per_sample: 32 bit 194 | 195 | # This is the declaration of our speaker. 196 | # It includes the data pin (You can refer to the wiring diagram of our voice assistant for more details) 197 | # It references the correct i2s bus declared above. 198 | speaker: 199 | platform: i2s_audio 200 | id: box_speaker 201 | dac_type: external 202 | i2s_audio_id: i2s_out 203 | i2s_dout_pin: GPIO8 204 | mode: mono 205 | 206 | 207 | voice_assistant: 208 | id: va 209 | microphone: box_mic 210 | speaker: box_speaker 211 | noise_suppression_level: 2 212 | auto_gain: 31dBFS 213 | volume_multiplier: 2.0 214 | vad_threshold: 3 215 | on_listening: 216 | - lambda: id(voice_assistant_phase) = ${voice_assist_listening_phase_id}; 217 | - text_sensor.template.publish: 218 | id: text_request 219 | state: "..." 220 | - text_sensor.template.publish: 221 | id: text_response 222 | state: "..." 223 | - if: 224 | condition: 225 | switch.is_on: wake_up_bell 226 | then: 227 | - lambda: id(box_speaker).play(id(timer_finished_wave_file), sizeof(id(timer_finished_wave_file))); 228 | - script.execute: control_led 229 | on_stt_vad_end: 230 | - lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id}; 231 | - script.execute: control_led 232 | on_stt_end: 233 | - text_sensor.template.publish: 234 | id: text_request 235 | state: !lambda return x; 236 | - script.execute: control_led 237 | on_tts_start: 238 | - text_sensor.template.publish: 239 | id: text_response 240 | state: !lambda return x; 241 | on_tts_stream_start: 242 | - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; 243 | - script.execute: control_led 244 | on_tts_stream_end: 245 | - if: 246 | condition: 247 | switch.is_off: mute 248 | then: 249 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 250 | else: 251 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 252 | - script.execute: control_led 253 | on_end: 254 | - wait_until: 255 | not: 256 | voice_assistant.is_running: 257 | - if: 258 | condition: 259 | and: 260 | - switch.is_off: mute 261 | - lambda: return id(wake_word_engine_location).state == "On device"; 262 | then: 263 | - micro_wake_word.start: 264 | on_error: 265 | - if: 266 | condition: 267 | lambda: return !id(init_in_progress); 268 | then: 269 | - lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id}; 270 | - script.execute: control_led 271 | - delay: 1s 272 | - if: 273 | condition: 274 | switch.is_off: mute 275 | then: 276 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 277 | else: 278 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 279 | - script.execute: control_led 280 | on_client_connected: 281 | - lambda: id(init_in_progress) = false; 282 | - script.execute: start_voice_assistant 283 | - script.execute: control_led 284 | on_client_disconnected: 285 | - script.execute: stop_voice_assistant 286 | - script.execute: control_led 287 | on_timer_started: 288 | - script.execute: control_led 289 | on_timer_cancelled: 290 | - script.execute: control_led 291 | on_timer_updated: 292 | - script.execute: control_led 293 | on_timer_tick: 294 | - script.execute: control_led 295 | on_timer_finished: 296 | - script.execute: stop_voice_assistant 297 | - lambda: id(voice_assistant_phase) = ${voice_assist_timer_finished_phase_id}; 298 | - switch.turn_on: timer_ringing 299 | - script.execute: control_led 300 | - wait_until: 301 | not: 302 | microphone.is_capturing: 303 | - while: 304 | condition: 305 | switch.is_on: timer_ringing 306 | then: 307 | - lambda: id(box_speaker).play(id(timer_finished_wave_file), sizeof(id(timer_finished_wave_file))); 308 | - delay: 1s 309 | - wait_until: 310 | not: 311 | speaker.is_playing: 312 | - switch.turn_off: timer_ringing 313 | - script.execute: start_voice_assistant 314 | - script.execute: control_led 315 | 316 | 317 | script: 318 | - id: control_led 319 | then: 320 | - if: 321 | condition: 322 | lambda: return !id(init_in_progress); 323 | then: 324 | - if: 325 | condition: 326 | wifi.connected: 327 | then: 328 | - if: 329 | condition: 330 | api.connected: 331 | then: 332 | - lambda: | 333 | switch(id(voice_assistant_phase)) { 334 | case ${voice_assist_listening_phase_id}: 335 | id(control_led_voice_assist_listening_phase).execute(); 336 | break; 337 | case ${voice_assist_thinking_phase_id}: 338 | id(control_led_voice_assist_thinking_phase).execute(); 339 | break; 340 | case ${voice_assist_replying_phase_id}: 341 | id(control_led_voice_assist_replying_phase).execute(); 342 | break; 343 | case ${voice_assist_error_phase_id}: 344 | id(control_led_voice_assist_error_phase).execute(); 345 | break; 346 | case ${voice_assist_muted_phase_id}: 347 | id(control_led_voice_assist_muted_phase).execute(); 348 | break; 349 | case ${voice_assist_not_ready_phase_id}: 350 | id(control_led_voice_assist_not_ready_phase).execute(); 351 | break; 352 | case ${voice_assist_timer_finished_phase_id}: 353 | id(control_led_voice_assist_timer_finished_phase).execute(); 354 | break; 355 | default: 356 | id(control_led_voice_assist_idle_phase).execute(); 357 | break; 358 | } 359 | else: 360 | - script.execute: control_led_no_ha_connection_state 361 | else: 362 | - script.execute: control_led_no_ha_connection_state 363 | else: 364 | - script.execute: control_led_init_state 365 | 366 | # Script executed during initialisation: In this example: Turn the LED in green with a slow pulse 🟢 367 | - id: control_led_init_state 368 | then: 369 | - light.turn_on: 370 | id: led_strip 371 | blue: 0% 372 | red: 0% 373 | green: 100% 374 | brightness: 50% 375 | effect: "Fast Pulse" 376 | 377 | 378 | # Script executed when the device has no connection to Home Assistant: In this example: Turn off the LED 379 | - id: control_led_no_ha_connection_state 380 | then: 381 | - light.turn_off: 382 | id: led_strip 383 | 384 | 385 | # Script executed when the voice assistant is idle (waiting for a wake word): In this example: Turn the LED in white with 20% of brightness ⚪ 386 | - id: control_led_voice_assist_idle_phase 387 | then: 388 | - light.turn_on: 389 | id: led_strip 390 | blue: 10% 391 | red: 10% 392 | green: 100% 393 | effect: "Waiting for wake word" 394 | 395 | 396 | # Script executed when the voice assistant is listening to a command: In this example: Turn the LED in blue with a slow pulse 🔵 397 | - id: control_led_voice_assist_listening_phase 398 | then: 399 | - light.turn_on: 400 | id: led_strip 401 | blue: 100% 402 | red: 0% 403 | green: 0% 404 | brightness: 80% 405 | effect: "Scanning" 406 | 407 | 408 | # Script executed when the voice assistant is processing the command: In this example: Turn the LED in blue with a fast pulse 🔵 409 | - id: control_led_voice_assist_thinking_phase 410 | then: 411 | - light.turn_on: 412 | id: led_strip 413 | blue: 100% 414 | red: 30% 415 | green: 30% 416 | brightness: 80% 417 | effect: "Fast Pulse" 418 | 419 | 420 | # Script executed when the voice assistant is replying to a command: In this example: Turn the LED in blue, solid (no pulse) 🔵 421 | - id: control_led_voice_assist_replying_phase 422 | then: 423 | - light.turn_on: 424 | id: led_strip 425 | blue: 100% 426 | red: 0% 427 | green: 0% 428 | brightness: 99% 429 | effect: "none" 430 | 431 | 432 | # Script executed when the voice assistant encounters an error: In this example: Turn the LED in red, solid (no pulse) 🔴 433 | - id: control_led_voice_assist_error_phase 434 | then: 435 | - light.turn_on: 436 | id: led_strip 437 | blue: 0% 438 | red: 100% 439 | green: 0% 440 | brightness: 98% 441 | effect: "none" 442 | 443 | 444 | # Script executed when the voice assistant is muted: In this example: Turn off the LED 445 | - id: control_led_voice_assist_muted_phase 446 | then: 447 | - light.turn_off: 448 | id: led_strip 449 | 450 | 451 | # Script executed when the voice assistant is not ready: In this example: Turn off the LED 452 | - id: control_led_voice_assist_not_ready_phase 453 | then: 454 | - light.turn_off: 455 | id: led_strip 456 | 457 | # TIMER scripts 458 | - id: control_led_voice_assist_timer_finished_phase 459 | then: 460 | - light.turn_on: 461 | id: led_strip 462 | blue: 50% 463 | red: 50% 464 | green: 50% 465 | brightness: 76% 466 | effect: "Fast Pulse" 467 | 468 | 469 | - id: check_if_timers_active 470 | then: 471 | - lambda: | 472 | const auto timers = id(va).get_timers(); 473 | bool output = false; 474 | if (timers.size() > 0) { 475 | for (auto &iterable_timer : timers) { 476 | if(iterable_timer.second.is_active) { 477 | output = true; 478 | } 479 | } 480 | } 481 | id(global_is_timer_active) = output; 482 | 483 | 484 | - id: check_if_timers 485 | then: 486 | - lambda: | 487 | const auto timers = id(va).get_timers(); 488 | bool output = false; 489 | if (timers.size() > 0) { 490 | output = true; 491 | } 492 | id(global_is_timer) = output; 493 | 494 | 495 | - id: timer_by_index 496 | then: 497 | - lambda: | 498 | const auto timers = id(va).get_timers(); 499 | auto timer_id = 0; 500 | if (timers.size() > 0) { 501 | for (auto &iterable_timer : timers) { 502 | if(iterable_timer.second.is_active) { 503 | if(timer_id == id(timer_list).active_index() ) { 504 | id(global_first_active_timer) = iterable_timer.second; 505 | } 506 | } 507 | timer_id++; 508 | } 509 | } 510 | 511 | 512 | - id: active_timer_widget 513 | then: 514 | - lambda: | 515 | id(check_if_timers_active).execute(); 516 | if (id(global_is_timer_active)){ 517 | 518 | id(timer_by_index).execute(); 519 | int hours_left = floor(id(global_first_active_timer).seconds_left / 3600); 520 | int minutes_left = floor((id(global_first_active_timer).seconds_left - hours_left * 3600) / 60); 521 | int seconds_left = id(global_first_active_timer).seconds_left - hours_left * 3600 - minutes_left * 60 ; 522 | auto display_hours = (hours_left < 10 ? "0" : "") + std::to_string(hours_left); 523 | auto display_minute = (minutes_left < 10 ? "0" : "") + std::to_string(minutes_left); 524 | auto display_seconds = (seconds_left < 10 ? "0" : "") + std::to_string(seconds_left) ; 525 | 526 | std::string display_string = ""; 527 | if (hours_left > 0) { 528 | display_string = display_hours + ":" + display_minute; 529 | } else { 530 | display_string = display_minute + ":" + display_seconds; 531 | } 532 | id(text_timer).publish_state(display_string.c_str()); 533 | } 534 | else { 535 | id(text_timer).publish_state("--:--"); 536 | } 537 | 538 | 539 | - id: start_voice_assistant 540 | then: 541 | - if: 542 | condition: 543 | switch.is_off: mute 544 | then: 545 | - if: 546 | condition: 547 | lambda: return id(wake_word_engine_location).state == "In Home Assistant"; 548 | then: 549 | - lambda: id(va).set_use_wake_word(true); 550 | - voice_assistant.start_continuous: 551 | - if: 552 | condition: 553 | lambda: return id(wake_word_engine_location).state == "On device"; 554 | then: 555 | - lambda: id(va).set_use_wake_word(false); 556 | - micro_wake_word.start 557 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 558 | else: 559 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 560 | 561 | 562 | - id: stop_voice_assistant 563 | then: 564 | - if: 565 | condition: 566 | lambda: return id(wake_word_engine_location).state == "In Home Assistant"; 567 | then: 568 | - lambda: id(va).set_use_wake_word(false); 569 | - voice_assistant.stop: 570 | - if: 571 | condition: 572 | lambda: return id(wake_word_engine_location).state == "On device"; 573 | then: 574 | - voice_assistant.stop: 575 | - micro_wake_word.stop: 576 | - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id}; 577 | 578 | 579 | switch: 580 | - platform: template 581 | name: Mute 582 | id: mute 583 | icon: "mdi:microphone-off" 584 | optimistic: true 585 | restore_mode: RESTORE_DEFAULT_OFF 586 | entity_category: config 587 | on_turn_off: 588 | - if: 589 | condition: 590 | lambda: return !id(init_in_progress); 591 | then: 592 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 593 | - if: 594 | condition: 595 | not: 596 | - voice_assistant.is_running 597 | then: 598 | - if: 599 | condition: 600 | lambda: return id(wake_word_engine_location).state == "In Home Assistant"; 601 | then: 602 | - lambda: id(va).set_use_wake_word(true); 603 | - voice_assistant.start_continuous 604 | - if: 605 | condition: 606 | lambda: return id(wake_word_engine_location).state == "On device"; 607 | then: 608 | - lambda: id(va).set_use_wake_word(false); 609 | - micro_wake_word.start 610 | - script.execute: control_led 611 | on_turn_on: 612 | - if: 613 | condition: 614 | lambda: return !id(init_in_progress); 615 | then: 616 | - lambda: id(va).set_use_wake_word(false); 617 | - voice_assistant.stop 618 | - micro_wake_word.stop 619 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 620 | - script.execute: control_led 621 | 622 | - platform: template 623 | id: timer_ringing 624 | optimistic: true 625 | internal: true 626 | restore_mode: ALWAYS_OFF 627 | on_turn_on: 628 | - delay: 2min 629 | - switch.turn_off: timer_ringing 630 | - platform: template 631 | name: Wake up bel 632 | id: wake_up_bell 633 | optimistic: true 634 | restore_mode: RESTORE_DEFAULT_ON 635 | icon: mdi:bell 636 | 637 | 638 | select: 639 | - platform: template 640 | entity_category: config 641 | name: Wake word engine location 642 | id: wake_word_engine_location 643 | icon: "mdi:account-voice" 644 | optimistic: true 645 | restore_value: true 646 | options: 647 | - In Home Assistant 648 | - On device 649 | initial_option: On device 650 | on_value: 651 | - if: 652 | condition: 653 | lambda: return !id(init_in_progress); 654 | then: 655 | - wait_until: 656 | lambda: return id(voice_assistant_phase) == ${voice_assist_muted_phase_id} || id(voice_assistant_phase) == ${voice_assist_idle_phase_id}; 657 | - if: 658 | condition: 659 | lambda: return x == "In Home Assistant"; 660 | then: 661 | - micro_wake_word.stop 662 | - delay: 500ms 663 | - if: 664 | condition: 665 | switch.is_off: mute 666 | then: 667 | - lambda: id(va).set_use_wake_word(true); 668 | - voice_assistant.start_continuous: 669 | - if: 670 | condition: 671 | lambda: return x == "On device"; 672 | then: 673 | - lambda: id(va).set_use_wake_word(false); 674 | - voice_assistant.stop 675 | - delay: 500ms 676 | - if: 677 | condition: 678 | switch.is_off: mute 679 | then: 680 | - micro_wake_word.start 681 | - platform: template 682 | name: Show timer 683 | id: timer_list 684 | icon: "mdi:account-voice" 685 | optimistic: true 686 | restore_value: true 687 | options: 688 | - "Timer 1" 689 | - "Timer 2" 690 | - "Timer 3" 691 | - "Timer 4" 692 | initial_option: "Timer 1" 693 | on_value: 694 | - logger.log: 695 | format: "# of timer: %d - Index: %d" 696 | args: ["id(va).get_timers().size()", "i"] 697 | 698 | 699 | globals: 700 | - id: init_in_progress 701 | type: bool 702 | restore_value: false 703 | initial_value: "true" 704 | - id: voice_assistant_phase 705 | type: int 706 | restore_value: false 707 | initial_value: ${voice_assist_not_ready_phase_id} 708 | - id: global_first_active_timer 709 | type: voice_assistant::Timer 710 | restore_value: false 711 | - id: global_is_timer_active 712 | type: bool 713 | restore_value: false 714 | - id: global_first_timer 715 | type: voice_assistant::Timer 716 | restore_value: false 717 | - id: global_is_timer 718 | type: bool 719 | restore_value: false 720 | # - id: timer_id 721 | # type: int 722 | # restore_value: false 723 | 724 | 725 | # Request and response text sensors so you can see what was understood 726 | text_sensor: 727 | - id: text_request 728 | name: Request 729 | platform: template 730 | icon: mdi:ear-hearing 731 | on_value: 732 | lambda: |- 733 | if(id(text_request).state.length()>32) { 734 | std::string name = id(text_request).state.c_str(); 735 | std::string truncated = esphome::str_truncate(name.c_str(),31); 736 | id(text_request).state = (truncated+"...").c_str(); 737 | } 738 | - id: text_response 739 | name: Response 740 | platform: template 741 | icon: mdi:react 742 | on_value: 743 | lambda: |- 744 | if(id(text_response).state.length()>32) { 745 | std::string name = id(text_response).state.c_str(); 746 | std::string truncated = esphome::str_truncate(name.c_str(),31); 747 | id(text_response).state = (truncated+"...").c_str(); 748 | } 749 | - id: text_timer 750 | name: Timer 751 | platform: template 752 | icon: mdi:camera-timer 753 | # Expose WiFi information as sensors. 754 | - platform: wifi_info 755 | ip_address: 756 | name: IP 757 | ssid: 758 | name: SSID 759 | bssid: 760 | name: BSSID 761 | 762 | 763 | sensor: 764 | - platform: wifi_signal # Reports the WiFi signal strength/RSSI in dB 765 | name: "WiFi Signal dB" 766 | id: wifi_signal_db 767 | update_interval: 60s 768 | entity_category: "diagnostic" 769 | 770 | - platform: copy # Reports the WiFi signal strength in % 771 | source_id: wifi_signal_db 772 | name: "WiFi Signal Percent" 773 | filters: 774 | - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0); 775 | unit_of_measurement: "Signal %" 776 | entity_category: "diagnostic" 777 | device_class: "" 778 | 779 | - platform: uptime 780 | name: Uptime Sensor 781 | 782 | 783 | 784 | file: 785 | - id: timer_finished_wave_file 786 | file: ${timer_alarm_sound} 787 | -------------------------------------------------------------------------------- /ESP Assistant v3.yaml: -------------------------------------------------------------------------------- 1 | # ESP Voice Assistant 2 | # 07 Oct 2024 (v 3.0.0) 3 | # - Based on the ESP32-S3_BOX version 4 | # adapted to work without a screen but with I2s amp and mic 5 | # Added some LED's for interacting 6 | # Request and response sensors in HA 7 | # Made to work in contineous mode with adjustable timeout 8 | # 9 | # by A.A. van Zoelen 10 | # Based on the work of Giants 11 | 12 | 13 | substitutions: 14 | name: "esp-assistant-vp" 15 | friendly_name: ESP Assistant VP 16 | 17 | micro_wake_word_model: okay_nabu # Other options are: hey_jarvis or alexa 18 | timer_alarm_sound: "sounds/timer_finished.wav" # in the esphome/sounds folder 19 | voice_assist_idle_phase_id: "1" 20 | voice_assist_listening_phase_id: "2" 21 | voice_assist_thinking_phase_id: "3" 22 | voice_assist_replying_phase_id: "4" 23 | voice_assist_not_ready_phase_id: "10" 24 | voice_assist_error_phase_id: "11" 25 | voice_assist_muted_phase_id: "12" 26 | voice_assist_timer_finished_phase_id: "20" 27 | 28 | # These unique characters have been extracted from every test file of every language available on https://github.com/home-assistant/intents (14 March 2024) 29 | allowed_characters: " !#%'()+,-./0123456789:;<>?@ABCDEFGHIJKLMNOPQRSTUVWYZ[]_abcdefghijklmnopqrstuvwxyz{|}°²³µ¿ÁÂÄÅÉÖÚßàáâãäåæçèéêëìíîðñòóôõöøùúûüýþāăąćčďĐđēėęěğĮįıļľŁłńňőřśšťũūůűųźŻżŽžơưșțΆΈΌΐΑΒΓΔΕΖΗΘΚΜΝΠΡΣΤΥΦάέήίαβγδεζηθικλμνξοπρςστυφχψωϊόύώАБВГДЕЖЗИКЛМНОПРСТУХЦЧШЪЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяёђєіїјљњћאבגדהוזחטיכלםמןנסעפץצקרשת،ءآأإئابةتجحخدذرزسشصضطظعغفقكلمنهوىيٹپچڈکگںھہیےংকচতধনফবযরলশষস়ািু্చయలిెొ్ംഅആഇഈഉഎഓകഗങചജഞടഡണതദധനപഫബഭമയരറലളവശസഹാിീുൂെേൈ്ൺൻർൽൾაბგდევზთილმნოპრსტუფქყშჩცძჭხạảấầẩậắặẹẽếềểệỉịọỏốồổỗộớờởợụủứừửữựỳ—、一上不个中为主乾了些亮人任低佔何作供依侧係個側偵充光入全关冇冷几切到制前動區卧厅厨及口另右吊后吗启吸呀咗哪唔問啟嗎嘅嘛器圍在场執場外多大始安定客室家密寵对將小少左已帘常幫幾库度庫廊廚廳开式後恆感態成我戲戶户房所扇手打执把拔换掉控插摄整斯新明是景暗更最會有未本模機檯櫃欄次正氏水沒没洗活派温測源溫漏潮激濕灯為無煙照熱燈燥物狀玄现現瓦用發的盞目着睡私空窗立笛管節簾籬紅線红罐置聚聲脚腦腳臥色节著行衣解設調請謝警设调走路車车运連遊運過道邊部都量鎖锁門閂閉開關门闭除隱離電震霧面音頂題顏颜風风食餅餵가간감갔강개거게겨결경고공과관그금급기길깥꺼껐꼽나난내네놀누는능니다닫담대더데도동됐되된됨둡드든등디때떤뜨라래러렇렌려로료른를리림링마많명몇모무문물뭐바밝방배변보부불블빨뽑사산상색서설성세센션소쇼수스습시신실싱아안않알았애야어얼업없었에여연열옆오온완외왼요운움워원위으은을음의이인일임입있작잠장재전절정제져조족종주줄중줘지직진짐쪽차창천최추출충치침커컴켜켰쿠크키탁탄태탬터텔통트튼티파팬퍼폰표퓨플핑한함해했행혀현화활후휴힘,?" 30 | 31 | 32 | esphome: 33 | name: ${name} 34 | friendly_name: ${friendly_name} 35 | min_version: 2024.7.0 36 | compile_process_limit: 1 # Helps when compiling on light weight systems 37 | platformio_options: 38 | board_build.flash_mode: dio 39 | project: 40 | name: AA_van_Zoelen.VoicePuck 41 | version: '3.0.0' 42 | on_boot: 43 | priority: 600 44 | then: 45 | - script.execute: control_led 46 | - delay: 30s 47 | - if: 48 | condition: 49 | lambda: return id(init_in_progress); 50 | then: 51 | - lambda: id(init_in_progress) = false; 52 | - script.execute: control_led 53 | 54 | esp32: 55 | board: esp32-s3-devkitc-1 56 | variant: esp32s3 57 | flash_size: 16MB 58 | framework: 59 | type: esp-idf 60 | version: recommended 61 | components: 62 | - name: esphome_board 63 | source: github://jesserockz/esphome-esp-adf-board@main 64 | refresh: 0s 65 | sdkconfig_options: 66 | CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y" 67 | CONFIG_ESP32S3_DATA_CACHE_64KB: "y" 68 | CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y" 69 | CONFIG_AUDIO_BOARD_CUSTOM: "y" 70 | 71 | psram: 72 | mode: octal 73 | speed: 80MHz 74 | 75 | external_components: 76 | - source: github://pr#5230 77 | components: esp_adf 78 | refresh: 0s 79 | - source: github://jesserockz/esphome-components 80 | components: [file] 81 | refresh: 0s 82 | 83 | api: 84 | encryption: 85 | key: !secret api_key # "0RpjwjYTywc+fIkRoV78gPaRJ6E/wDzCAJ3X8a/HZJE=" 86 | on_client_connected: 87 | - script.execute: control_led 88 | on_client_disconnected: 89 | - script.execute: control_led 90 | 91 | ota: 92 | - platform: esphome 93 | id: ota_esphome 94 | 95 | logger: 96 | # hardware_uart: USB_SERIAL_JTAG 97 | 98 | wifi: 99 | ssid: !secret wifi_ssid 100 | password: !secret wifi_password 101 | output_power: 8.5dB 102 | 103 | # If the device connects, or disconnects, to the Wifi: Run the script to refresh the LED status 104 | on_connect: 105 | - script.execute: control_led 106 | on_disconnect: 107 | - script.execute: control_led 108 | 109 | # Enable fallback hotspot (captive portal) in case wifi connection fails 110 | ap: 111 | ssid: !secret fallback_ssid 112 | password: !secret fallback_password 113 | 114 | 115 | button: # Diagnotiscs 116 | - platform: restart 117 | id: "restart_device" 118 | name: "Restart" 119 | entity_category: "diagnostic" 120 | 121 | 122 | light: 123 | - platform: esp32_rmt_led_strip 124 | rgb_order: GRB 125 | pin: GPIO17 #GPIO48 # On board light 126 | num_leds: 3 127 | rmt_channel: 0 128 | chipset: WS2812 129 | name: "Status LED" 130 | id: led_strip 131 | disabled_by_default: True 132 | entity_category: config 133 | icon: mdi:led-on 134 | default_transition_length: 0s 135 | effects: 136 | - pulse: 137 | name: "Slow Pulse" 138 | transition_length: 250ms 139 | update_interval: 250ms 140 | min_brightness: 60% 141 | max_brightness: 80% 142 | - pulse: 143 | name: "Fast Pulse" 144 | transition_length: 100ms 145 | update_interval: 100ms 146 | min_brightness: 60% 147 | max_brightness: 80% 148 | - addressable_scan: 149 | name: "Scanning" 150 | move_interval: 70ms 151 | scan_width: 1 152 | - pulse: 153 | name: "Waiting for wake word" 154 | min_brightness: 15% 155 | max_brightness: 35% 156 | transition_length: 3s # defaults to 1s 157 | update_interval: 3s 158 | 159 | 160 | esp_adf: 161 | 162 | micro_wake_word: 163 | models: 164 | - ${micro_wake_word_model} 165 | on_wake_word_detected: 166 | - if: 167 | condition: 168 | lambda: return id(wake_up_bell_active) == true; 169 | then: 170 | switch.turn_on: wake_up_bell 171 | - if: 172 | condition: 173 | switch.is_on: continued_conversation 174 | then: 175 | - logger.log: "MICRO WAKE WORD - CONTINUOUS" 176 | - lambda: id(va).set_use_wake_word(false); 177 | - voice_assistant.start_continuous: # wake_word: !lambda return wake_word; not yet supported for continuous 178 | else: 179 | - logger.log: "MICRO WAKE WORD - START" 180 | - lambda: id(va).set_use_wake_word(false); 181 | - voice_assistant.start: 182 | wake_word: !lambda return wake_word; 183 | 184 | 185 | # This is our two i2s buses with the correct pins. 186 | # You can refer to the wiring diagram of our voice assistant for more details 187 | i2s_audio: 188 | - id: i2s_in 189 | i2s_lrclk_pin: GPIO18 #WS 190 | i2s_bclk_pin: GPIO2 #SCK 191 | - id: i2s_out 192 | i2s_lrclk_pin: GPIO6 #LRC 193 | i2s_bclk_pin: GPIO7 #BLCK 194 | 195 | # This is the declaration of our microphone. 196 | # It includes the data pin (You can refer to the wiring diagram of our voice assistant for more details) 197 | # It references the correct i2s bus declared above. 198 | microphone: 199 | - platform: i2s_audio 200 | id: box_mic 201 | adc_type: external 202 | i2s_audio_id: i2s_in 203 | i2s_din_pin: GPIO4 204 | pdm: false 205 | channel: left 206 | bits_per_sample: 32 bit 207 | 208 | # This is the declaration of our speaker. 209 | # It includes the data pin (You can refer to the wiring diagram of our voice assistant for more details) 210 | # It references the correct i2s bus declared above. 211 | speaker: 212 | platform: i2s_audio 213 | id: box_speaker 214 | dac_type: external 215 | i2s_audio_id: i2s_out 216 | i2s_dout_pin: GPIO8 217 | channel: mono 218 | 219 | 220 | voice_assistant: 221 | id: va 222 | microphone: box_mic 223 | speaker: box_speaker 224 | noise_suppression_level: 4 # Between 0 and 4 inclusive. Defaults to 0 (disabled). 225 | auto_gain: 4dBFS # Between 0dBFS and 31dBFS inclusive. Defaults to 0 (disabled). 226 | volume_multiplier: 1.0 # Must be larger than 0. Defaults to 1 (disabled). 227 | vad_threshold: 3 228 | on_listening: # perform when the voice assistant microphone starts listening. 229 | - lambda: id(voice_assistant_phase) = ${voice_assist_listening_phase_id}; 230 | - logger.log: "ON-LISTENING" 231 | - text_sensor.template.publish: 232 | id: text_request 233 | state: "..." 234 | - text_sensor.template.publish: 235 | id: text_response 236 | state: "..." 237 | - if: 238 | condition: 239 | switch.is_on: continued_conversation 240 | then: 241 | - script.execute: stt_timeout_to_idle 242 | - lambda: id(va).set_use_wake_word(false); 243 | - if: 244 | condition: 245 | - switch.is_on: wake_up_bell 246 | then: 247 | - lambda: id(box_speaker).play(id(timer_finished_wave_file), sizeof(id(timer_finished_wave_file))); 248 | - switch.turn_off: wake_up_bell 249 | - script.execute: control_led 250 | on_stt_vad_end: # perform when voice activity detection ends speech-to-text processing. 251 | - lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id}; 252 | - logger.log: "ON STT VAD END" 253 | - if: 254 | condition: 255 | switch.is_on: continued_conversation 256 | then: 257 | - script.execute: stt_timeout_to_idle 258 | - script.execute: control_led 259 | on_stt_end: # perform when the voice assistant has finished speech-to-text. The resulting text is available to automations as the variable x. 260 | - logger.log: "ON STT END" 261 | - text_sensor.template.publish: 262 | id: text_request 263 | state: !lambda return x; 264 | - script.execute: control_led 265 | on_tts_start: # perform when the voice assistant has started text-to-speech. The text to be spoken is available to automations as the variable x. 266 | - logger.log: "ON TTS START" 267 | - text_sensor.template.publish: 268 | id: text_response 269 | state: !lambda return x; 270 | on_tts_stream_start: # perform when audio stream (voice response) playback starts. Requires speaker to be configured. 271 | - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; 272 | - logger.log: "ON TTS STREAM START" 273 | - script.execute: control_led 274 | on_tts_stream_end: # perform when audio stream (voice response) playback ends. Requires speaker to be configured. 275 | - logger.log: "ON_TTS STREAM END" 276 | - if: 277 | condition: 278 | switch.is_on: continued_conversation 279 | then: 280 | - logger.log: "ON_TTS STREAM END 1" 281 | - lambda: id(voice_assistant_phase) = ${voice_assist_listening_phase_id}; 282 | - script.execute: stt_timeout_to_idle 283 | else: 284 | - logger.log: "ON_TTS STREAM END 2" 285 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 286 | 287 | - script.execute: control_led 288 | on_end: # perform when the voice assistant is finished all tasks. 289 | - logger.log: "ON_END" 290 | - if: 291 | condition: 292 | and: 293 | - switch.is_off: mute 294 | - lambda: return id(wake_word_engine_location).state == "On device"; 295 | then: 296 | - script.execute: stt_timeout_to_idle 297 | 298 | on_error: # perform when the voice assistant has encountered an error. The error code and message are available to automations as the variables code and message. 299 | - logger.log: "ON_ERROR" 300 | - if: 301 | condition: 302 | and: 303 | - lambda: return !id(init_in_progress); 304 | - lambda: return !(code == "stt-no-text-recognized"); 305 | then: 306 | - lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id}; 307 | - script.execute: control_led 308 | - delay: 1s 309 | - script.stop: stt_timeout_to_idle 310 | - if: 311 | condition: 312 | switch.is_off: mute 313 | then: 314 | # - logger.log: "(on_client_connected) Returning to idle by script" 315 | - script.execute: return_to_idle 316 | else: 317 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 318 | - script.execute: control_led 319 | on_client_connected: 320 | - if: 321 | condition: 322 | switch.is_off: mute 323 | then: 324 | - script.execute: return_to_idle 325 | else: 326 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 327 | - lambda: id(init_in_progress) = false; 328 | - script.execute: control_led 329 | on_client_disconnected: 330 | - if: 331 | condition: 332 | lambda: return id(wake_word_engine_location).state == "In Home Assistant"; 333 | then: 334 | - lambda: id(va).set_use_wake_word(false); 335 | - voice_assistant.stop: 336 | - if: 337 | condition: 338 | lambda: return id(wake_word_engine_location).state == "On device"; 339 | then: 340 | - micro_wake_word.stop 341 | - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id}; 342 | - script.execute: control_led 343 | on_timer_started: 344 | - script.execute: control_led 345 | on_timer_cancelled: 346 | - script.execute: control_led 347 | on_timer_updated: 348 | - script.execute: control_led 349 | on_timer_tick: 350 | - script.execute: control_led 351 | on_timer_finished: 352 | - script.execute: stop_voice_assistant 353 | - lambda: id(voice_assistant_phase) = ${voice_assist_timer_finished_phase_id}; 354 | - switch.turn_on: timer_ringing 355 | - script.execute: control_led 356 | - wait_until: 357 | not: 358 | microphone.is_capturing: 359 | - while: 360 | condition: 361 | switch.is_on: timer_ringing 362 | then: 363 | - lambda: id(box_speaker).play(id(timer_finished_wave_file), sizeof(id(timer_finished_wave_file))); 364 | - delay: 1s 365 | - wait_until: 366 | not: 367 | speaker.is_playing: 368 | - switch.turn_off: timer_ringing 369 | - script.execute: start_voice_assistant 370 | - script.execute: control_led 371 | 372 | 373 | script: 374 | - id: control_led 375 | then: 376 | - if: 377 | condition: 378 | lambda: return !id(init_in_progress); 379 | then: 380 | - if: 381 | condition: 382 | wifi.connected: 383 | then: 384 | - if: 385 | condition: 386 | api.connected: 387 | then: 388 | - lambda: | 389 | switch(id(voice_assistant_phase)) { 390 | case ${voice_assist_listening_phase_id}: 391 | id(control_led_voice_assist_listening_phase).execute(); 392 | break; 393 | case ${voice_assist_thinking_phase_id}: 394 | id(control_led_voice_assist_thinking_phase).execute(); 395 | break; 396 | case ${voice_assist_replying_phase_id}: 397 | id(control_led_voice_assist_replying_phase).execute(); 398 | break; 399 | case ${voice_assist_error_phase_id}: 400 | id(control_led_voice_assist_error_phase).execute(); 401 | break; 402 | case ${voice_assist_muted_phase_id}: 403 | id(control_led_voice_assist_muted_phase).execute(); 404 | break; 405 | case ${voice_assist_not_ready_phase_id}: 406 | id(control_led_voice_assist_not_ready_phase).execute(); 407 | break; 408 | case ${voice_assist_timer_finished_phase_id}: 409 | id(control_led_voice_assist_timer_finished_phase).execute(); 410 | break; 411 | default: 412 | id(control_led_voice_assist_idle_phase).execute(); 413 | break; 414 | } 415 | else: 416 | - script.execute: control_led_no_ha_connection_state 417 | else: 418 | - script.execute: control_led_no_ha_connection_state 419 | else: 420 | - script.execute: control_led_init_state 421 | 422 | - id: stt_timeout_to_idle 423 | mode: restart # restart the timer if called before timeout 424 | then: 425 | if: 426 | condition: 427 | switch.is_on: continued_conversation 428 | then: 429 | - logger.log: 430 | format: "THE TIME OUT IS %.1f SECONDS" 431 | args: [ 'id(continued_timeout).state' ] 432 | - delay: !lambda "return id(continued_timeout).state * 1000;" 433 | - logger.log: "SCRIPT - STT_TIMEOUT_TO_IDLE" 434 | - if: 435 | condition: 436 | lambda: return (id(voice_assistant_phase) == ${voice_assist_replying_phase_id}); 437 | then: 438 | - wait_until: 439 | condition: 440 | lambda: return !(id(voice_assistant_phase) == ${voice_assist_replying_phase_id}); 441 | # normally this would complete and move to next phase with on_tts_stream_end, 442 | # but sometimes this is missed so put a time limit on the wait 443 | timeout: 6s 444 | - delay: 600ms # Give time for the stream to end and the phase to be switched back to listening and this timeout to be reset 445 | - logger.log: "STT_TIMEOUT_TO_IDLE - CONDITION FINISHED" 446 | - script.execute: return_to_idle 447 | 448 | - id: return_to_idle 449 | then: 450 | - if: 451 | condition: 452 | lambda: return id(wake_word_engine_location).state == "On device"; 453 | then: 454 | - logger.log: "SCRIPT - RETURN_TO_IDLE 1" 455 | - script.stop: stt_timeout_to_idle 456 | - if: 457 | condition: 458 | voice_assistant.is_running 459 | then: 460 | - lambda: id(va).set_use_wake_word(false); 461 | - voice_assistant.stop 462 | - wait_until: 463 | condition: 464 | not: 465 | voice_assistant.is_running 466 | timeout: 5s 467 | - if: 468 | condition: 469 | not: 470 | micro_wake_word.is_running 471 | then: 472 | - micro_wake_word.start: 473 | - if: 474 | condition: 475 | lambda: return id(wake_word_engine_location).state == "In Home Assistant"; 476 | then: 477 | - logger.log: "SCRIPT - RETURN_TO_IDLE 2" 478 | - if: 479 | condition: 480 | micro_wake_word.is_running 481 | then: 482 | - micro_wake_word.stop: 483 | - wait_until: 484 | condition: 485 | not: 486 | micro_wake_word.is_running 487 | timeout: 5s 488 | - wait_until: 489 | condition: 490 | not: 491 | voice_assistant.is_running 492 | timeout: 5s 493 | - lambda: id(va).set_use_wake_word(true); 494 | - if: 495 | condition: 496 | - not: 497 | voice_assistant.is_running 498 | then: 499 | - voice_assistant.start_continuous: 500 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 501 | - script.execute: control_led 502 | 503 | 504 | # Script executed during initialisation: In this example: Turn the LED in green with a slow pulse 🟢 505 | - id: control_led_init_state 506 | then: 507 | - light.turn_on: 508 | id: led_strip 509 | blue: 0% 510 | red: 0% 511 | green: 100% 512 | brightness: 50% 513 | effect: "Fast Pulse" 514 | 515 | 516 | # Script executed when the device has no connection to Home Assistant: In this example: Turn off the LED 517 | - id: control_led_no_ha_connection_state 518 | then: 519 | - light.turn_off: 520 | id: led_strip 521 | 522 | 523 | # Script executed when the voice assistant is idle (waiting for a wake word): In this example: Turn the LED in white with 20% of brightness ⚪ 524 | - id: control_led_voice_assist_idle_phase 525 | then: 526 | - light.turn_on: 527 | id: led_strip 528 | blue: 10% 529 | red: 10% 530 | green: 100% 531 | effect: "Waiting for wake word" 532 | 533 | 534 | # Script executed when the voice assistant is listening to a command: In this example: Turn the LED in blue with a slow pulse 🔵 535 | - id: control_led_voice_assist_listening_phase 536 | then: 537 | - light.turn_on: 538 | id: led_strip 539 | blue: 100% 540 | red: 0% 541 | green: 0% 542 | brightness: 80% 543 | effect: "Scanning" 544 | 545 | 546 | # Script executed when the voice assistant is processing the command: In this example: Turn the LED in blue with a fast pulse 🔵 547 | - id: control_led_voice_assist_thinking_phase 548 | then: 549 | - light.turn_on: 550 | id: led_strip 551 | blue: 100% 552 | red: 30% 553 | green: 30% 554 | brightness: 80% 555 | effect: "Fast Pulse" 556 | 557 | 558 | # Script executed when the voice assistant is replying to a command: In this example: Turn the LED in blue, solid (no pulse) 🔵 559 | - id: control_led_voice_assist_replying_phase 560 | then: 561 | - light.turn_on: 562 | id: led_strip 563 | blue: 100% 564 | red: 0% 565 | green: 0% 566 | brightness: 99% 567 | effect: "none" 568 | 569 | 570 | # Script executed when the voice assistant encounters an error: In this example: Turn the LED in red, solid (no pulse) 🔴 571 | - id: control_led_voice_assist_error_phase 572 | then: 573 | - light.turn_on: 574 | id: led_strip 575 | blue: 0% 576 | red: 100% 577 | green: 0% 578 | brightness: 98% 579 | effect: "none" 580 | 581 | 582 | # Script executed when the voice assistant is muted: In this example: Turn off the LED 583 | - id: control_led_voice_assist_muted_phase 584 | then: 585 | - light.turn_off: 586 | id: led_strip 587 | 588 | 589 | # Script executed when the voice assistant is not ready: In this example: Turn off the LED 590 | - id: control_led_voice_assist_not_ready_phase 591 | then: 592 | - light.turn_off: 593 | id: led_strip 594 | 595 | # TIMER scripts 596 | - id: control_led_voice_assist_timer_finished_phase 597 | then: 598 | - light.turn_on: 599 | id: led_strip 600 | blue: 50% 601 | red: 50% 602 | green: 50% 603 | brightness: 76% 604 | effect: "Fast Pulse" 605 | 606 | 607 | - id: check_if_timers_active 608 | then: 609 | - lambda: | 610 | const auto timers = id(va).get_timers(); 611 | bool output = false; 612 | if (timers.size() > 0) { 613 | for (auto &iterable_timer : timers) { 614 | if(iterable_timer.second.is_active) { 615 | output = true; 616 | } 617 | } 618 | } 619 | id(global_is_timer_active) = output; 620 | 621 | 622 | - id: check_if_timers 623 | then: 624 | - lambda: | 625 | const auto timers = id(va).get_timers(); 626 | bool output = false; 627 | if (timers.size() > 0) { 628 | output = true; 629 | } 630 | id(global_is_timer) = output; 631 | 632 | 633 | - id: timer_by_index 634 | then: 635 | - lambda: | 636 | const auto timers = id(va).get_timers(); 637 | auto timer_id = 0; 638 | if (timers.size() > 0) { 639 | for (auto &iterable_timer : timers) { 640 | if(iterable_timer.second.is_active) { 641 | if(timer_id == id(timer_list).active_index() ) { 642 | id(global_first_active_timer) = iterable_timer.second; 643 | } 644 | } 645 | timer_id++; 646 | } 647 | } 648 | 649 | 650 | - id: active_timer_widget 651 | then: 652 | - lambda: | 653 | id(check_if_timers_active).execute(); 654 | if (id(global_is_timer_active)){ 655 | 656 | id(timer_by_index).execute(); 657 | int hours_left = floor(id(global_first_active_timer).seconds_left / 3600); 658 | int minutes_left = floor((id(global_first_active_timer).seconds_left - hours_left * 3600) / 60); 659 | int seconds_left = id(global_first_active_timer).seconds_left - hours_left * 3600 - minutes_left * 60 ; 660 | auto display_hours = (hours_left < 10 ? "0" : "") + std::to_string(hours_left); 661 | auto display_minute = (minutes_left < 10 ? "0" : "") + std::to_string(minutes_left); 662 | auto display_seconds = (seconds_left < 10 ? "0" : "") + std::to_string(seconds_left) ; 663 | 664 | std::string display_string = ""; 665 | if (hours_left > 0) { 666 | display_string = display_hours + ":" + display_minute; 667 | } else { 668 | display_string = display_minute + ":" + display_seconds; 669 | } 670 | id(text_timer).publish_state(display_string.c_str()); 671 | } 672 | else { 673 | id(text_timer).publish_state("--:--"); 674 | } 675 | 676 | 677 | - id: start_voice_assistant 678 | then: 679 | - if: 680 | condition: 681 | switch.is_off: mute 682 | then: 683 | - logger.log: "START_VOICE_ASSISTANT 1" 684 | - if: 685 | condition: 686 | lambda: return id(wake_word_engine_location).state == "In Home Assistant"; 687 | then: 688 | - logger.log: "start_voice_assistant STARTED 1" 689 | - lambda: id(va).set_use_wake_word(true); 690 | - voice_assistant.start_continuous: 691 | - if: 692 | condition: 693 | lambda: return id(wake_word_engine_location).state == "On device"; 694 | then: 695 | - logger.log: "start_voice_assistant STARTED 2" 696 | - lambda: id(va).set_use_wake_word(false); 697 | - micro_wake_word.start 698 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 699 | else: 700 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 701 | - logger.log: "START_VOICE_ASSISTANT 2" 702 | 703 | 704 | - id: stop_voice_assistant 705 | then: 706 | - if: 707 | condition: 708 | lambda: return id(wake_word_engine_location).state == "In Home Assistant"; 709 | then: 710 | - logger.log: "STOP_VOICE_ASSISTANT 1" 711 | - lambda: id(va).set_use_wake_word(false); 712 | - voice_assistant.stop: 713 | - if: 714 | condition: 715 | lambda: return id(wake_word_engine_location).state == "On device"; 716 | then: 717 | - logger.log: "STOP_VOICE_ASSISTANT 2" 718 | - voice_assistant.stop: 719 | - micro_wake_word.stop: 720 | - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id}; 721 | 722 | 723 | number: # Continued timeout 724 | - platform: template 725 | entity_category: config 726 | name: Continued timeout 727 | id: continued_timeout 728 | icon: mdi:clock 729 | optimistic: true 730 | restore_value: true 731 | initial_value: 4 #8 732 | min_value: 1 733 | step: 1 734 | max_value: 20 735 | unit_of_measurement: s 736 | mode: slider 737 | 738 | 739 | switch: 740 | - platform: template # Mute 741 | name: Mute 742 | id: mute 743 | icon: "mdi:microphone-off" 744 | optimistic: true 745 | restore_mode: RESTORE_DEFAULT_OFF 746 | entity_category: config 747 | on_turn_off: 748 | - if: 749 | condition: 750 | lambda: return !id(init_in_progress); 751 | then: 752 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 753 | - if: 754 | condition: 755 | not: 756 | - voice_assistant.is_running 757 | then: 758 | - if: 759 | condition: 760 | lambda: return id(wake_word_engine_location).state == "In Home Assistant"; 761 | then: 762 | - lambda: id(va).set_use_wake_word(true); 763 | - voice_assistant.start_continuous 764 | - if: 765 | condition: 766 | lambda: return id(wake_word_engine_location).state == "On device"; 767 | then: 768 | - lambda: id(va).set_use_wake_word(false); 769 | - micro_wake_word.start 770 | - script.execute: control_led 771 | on_turn_on: 772 | - if: 773 | condition: 774 | lambda: return !id(init_in_progress); 775 | then: 776 | - lambda: id(va).set_use_wake_word(false); 777 | - voice_assistant.stop 778 | - micro_wake_word.stop 779 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 780 | - script.execute: control_led 781 | 782 | - platform: template # timer ringing 783 | id: timer_ringing 784 | optimistic: true 785 | internal: true 786 | restore_mode: ALWAYS_OFF 787 | on_turn_on: 788 | - delay: 2min 789 | - switch.turn_off: timer_ringing 790 | 791 | - platform: template # wake up bell 792 | name: Wake up bel 793 | id: wake_up_bell 794 | optimistic: true 795 | restore_mode: RESTORE_DEFAULT_ON 796 | entity_category: config 797 | icon: mdi:bell 798 | on_turn_on: 799 | then: 800 | - lambda: id(wake_up_bell_active) = true; 801 | on_turn_off: 802 | then: 803 | - lambda: id(wake_up_bell_active) = false; 804 | 805 | - platform: template 806 | name: Continued conversation 807 | id: continued_conversation 808 | optimistic: true 809 | restore_mode: RESTORE_DEFAULT_OFF 810 | entity_category: config 811 | on_turn_off: 812 | - script.execute: return_to_idle 813 | on_turn_on: 814 | - script.execute: return_to_idle 815 | 816 | 817 | select: 818 | - platform: template # Wake word engine location 819 | entity_category: config 820 | name: Wake word engine location 821 | id: wake_word_engine_location 822 | optimistic: true 823 | restore_value: true 824 | options: 825 | - In Home Assistant 826 | - On device 827 | initial_option: On device 828 | on_value: 829 | - wait_until: 830 | lambda: return id(voice_assistant_phase) == ${voice_assist_muted_phase_id} || id(voice_assistant_phase) == ${voice_assist_idle_phase_id}; 831 | - script.execute: return_to_idle 832 | 833 | 834 | - platform: template # Show timer 835 | name: Show timer 836 | id: timer_list 837 | icon: "mdi:account-voice" 838 | optimistic: true 839 | restore_value: true 840 | options: 841 | - "Timer 1" 842 | - "Timer 2" 843 | - "Timer 3" 844 | - "Timer 4" 845 | initial_option: "Timer 1" 846 | on_value: 847 | - logger.log: 848 | format: "# of timer: %d - Index: %d" 849 | args: ["id(va).get_timers().size()", "i"] 850 | 851 | 852 | globals: 853 | - id: init_in_progress 854 | type: bool 855 | restore_value: false 856 | initial_value: "true" 857 | - id: voice_assistant_phase 858 | type: int 859 | restore_value: false 860 | initial_value: ${voice_assist_not_ready_phase_id} 861 | - id: global_first_active_timer 862 | type: voice_assistant::Timer 863 | restore_value: false 864 | - id: global_is_timer_active 865 | type: bool 866 | restore_value: false 867 | - id: global_first_timer 868 | type: voice_assistant::Timer 869 | restore_value: false 870 | - id: global_is_timer 871 | type: bool 872 | restore_value: false 873 | - id: wake_up_bell_active 874 | type: bool 875 | restore_value: false 876 | 877 | 878 | # Request and response text sensors so you can see what was understood 879 | text_sensor: 880 | - id: text_request 881 | name: Request 882 | platform: template 883 | icon: mdi:ear-hearing 884 | on_value: 885 | lambda: |- 886 | if(id(text_request).state.length()>32) { 887 | std::string name = id(text_request).state.c_str(); 888 | std::string truncated = esphome::str_truncate(name.c_str(),31); 889 | id(text_request).state = (truncated+"...").c_str(); 890 | } 891 | - id: text_response 892 | name: Response 893 | platform: template 894 | icon: mdi:react 895 | on_value: 896 | lambda: |- 897 | if(id(text_response).state.length()>32) { 898 | std::string name = id(text_response).state.c_str(); 899 | std::string truncated = esphome::str_truncate(name.c_str(),31); 900 | id(text_response).state = (truncated+"...").c_str(); 901 | } 902 | - id: text_timer 903 | name: Timer 904 | platform: template 905 | icon: mdi:camera-timer 906 | # Expose WiFi information as sensors. 907 | - platform: wifi_info 908 | ip_address: 909 | name: IP 910 | ssid: 911 | name: SSID 912 | bssid: 913 | name: BSSID 914 | 915 | 916 | sensor: # Diagnositcs 917 | - platform: wifi_signal # Reports the WiFi signal strength/RSSI in dB 918 | name: "WiFi Signal dB" 919 | id: wifi_signal_db 920 | update_interval: 60s 921 | entity_category: "diagnostic" 922 | 923 | - platform: copy # Reports the WiFi signal strength in % 924 | source_id: wifi_signal_db 925 | name: "WiFi Signal Percent" 926 | filters: 927 | - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0); 928 | unit_of_measurement: "Signal %" 929 | entity_category: "diagnostic" 930 | device_class: "" 931 | 932 | - platform: uptime 933 | name: Uptime Sensor 934 | 935 | 936 | file: 937 | - id: timer_finished_wave_file 938 | file: ${timer_alarm_sound} 939 | -------------------------------------------------------------------------------- /ESP Assistant.yaml: -------------------------------------------------------------------------------- 1 | # ESP Voice Assistant 2 | # version: 6 - 31 Jul 2024 (v 1.0.0) 3 | # - Select on timers limits to number of timers 4 | # - display remaining time for selected timer 5 | # version: 5 - 30 Jul 2024 6 | # - code cleaning 7 | # - Timer canceling 8 | # version: 4 - 29 Jul 2024 9 | # - Added Request and response text sensors 10 | # - Text_sensor in HA for tracking the first (shortest) running timer 11 | # version: 3 - 26 Jul 2024 12 | # - Added Timers 13 | # - Wake word bell signal on/off 14 | # 15 | # by A.A. van Zoelen 16 | # Based on the work of Giants 17 | 18 | 19 | substitutions: 20 | name: "esp-assistant" 21 | friendly_name: ESP Assistant 22 | 23 | micro_wake_word_model: okay_nabu # Other options are: hey_jarvis or alexa 24 | timer_alarm_sound: "sounds/timer_finished.wav" # in the esphome/sounds folder 25 | 26 | # Phases of the Voice Assistant 27 | # IDLE: The voice assistant is ready to be triggered by a wake-word 28 | voice_assist_idle_phase_id: '1' 29 | # LISTENING: The voice assistant is ready to listen to a voice command (after being triggered by the wake word) 30 | voice_assist_listening_phase_id: '2' 31 | # THINKING: The voice assistant is currently processing the command 32 | voice_assist_thinking_phase_id: '3' 33 | # REPLYING: The voice assistant is replying to the command 34 | voice_assist_replying_phase_id: '4' 35 | # NOT_READY: The voice assistant is not ready 36 | voice_assist_not_ready_phase_id: '10' 37 | # ERROR: The voice assistant encountered an error 38 | voice_assist_error_phase_id: '11' 39 | # MUTED: The voice assistant is muted and will not reply to a wake-word 40 | voice_assist_muted_phase_id: '12' 41 | # TIMER: Timer fase 42 | voice_assist_timer_finished_phase_id: '20' 43 | 44 | 45 | 46 | esphome: 47 | name: ${name} 48 | friendly_name: ${friendly_name} 49 | min_version: 2024.6.0 50 | compile_process_limit: 1 # Helps when compiling on light weight systems 51 | name_add_mac_suffix: false 52 | platformio_options: 53 | board_build.flash_mode: dio 54 | project: 55 | name: AA_van_Zoelen.VoicePuck 56 | version: '1.0.0' 57 | # Automation to perform every time the device boots 58 | on_boot: 59 | priority: 600 60 | then: 61 | # Run the script to refresh the LED status 62 | - script.execute: control_led 63 | # If after 30 seconds, the device is still initializing (It did not yet connect to Home Assistant), turn off the init_in_progress variable and run the script to refresh the LED status 64 | - delay: 30s 65 | - if: 66 | condition: 67 | lambda: return id(init_in_progress); 68 | then: 69 | - lambda: id(init_in_progress) = false; 70 | - script.execute: control_led 71 | 72 | esp32: 73 | board: esp32-s3-devkitc-1 74 | variant: esp32s3 75 | # This is important. ESPHome supports two frameworks: Arduino and ESP-IDF. ESP-IDF is needed to include an audio library called ESP_ADF 76 | framework: 77 | type: esp-idf 78 | # type: arduino 79 | # components: 80 | # - name: esphome_board 81 | # source: github://jesserockz/esphome-esp-adf-board@main 82 | # refresh: 0s 83 | 84 | sdkconfig_options: 85 | CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y" 86 | CONFIG_ESP32S3_DATA_CACHE_64KB: "y" 87 | CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y" 88 | CONFIG_AUDIO_BOARD_CUSTOM: "y" 89 | 90 | psram: 91 | mode: octal # quad for N8R2 and octal for N16R8 92 | speed: 80MHz 93 | 94 | 95 | # Enable logging 96 | logger: 97 | # level: DEBUG 98 | 99 | # Enable Home Assistant API 100 | api: 101 | # If the device connects, or disconnects, to Home Assistant: Run the script to refresh the LED status 102 | on_client_connected: 103 | - script.execute: control_led 104 | on_client_disconnected: 105 | - script.execute: control_led 106 | 107 | 108 | ota: 109 | - platform: esphome 110 | 111 | 112 | wifi: 113 | ssid: !secret wifi_ssid 114 | password: !secret wifi_password 115 | output_power: 8.5dB 116 | 117 | # If the device connects, or disconnects, to the Wifi: Run the script to refresh the LED status 118 | on_connect: 119 | - script.execute: control_led 120 | on_disconnect: 121 | - script.execute: control_led 122 | 123 | # Enable fallback hotspot (captive portal) in case wifi connection fails 124 | ap: 125 | ssid: !secret fallback_ssid 126 | password: !secret fallback_password 127 | 128 | 129 | captive_portal: 130 | 131 | globals: 132 | # Global initialisation variable. Initialized to true and set to false once everything is connected. Only used to have a smooth "plugging" experience 133 | - id: init_in_progress 134 | type: bool 135 | restore_value: no 136 | initial_value: 'true' 137 | # Global variable tracking the phase of the voice assistant (defined above). Initialized to not_ready 138 | - id: voice_assistant_phase 139 | type: int 140 | restore_value: no 141 | initial_value: ${voice_assist_not_ready_phase_id} 142 | # Global variables for tracking timers 143 | - id: global_first_active_timer 144 | type: voice_assistant::Timer 145 | restore_value: false 146 | - id: global_is_timer_active 147 | type: bool 148 | restore_value: false 149 | - id: global_first_timer 150 | type: voice_assistant::Timer 151 | restore_value: false 152 | - id: global_is_timer 153 | type: bool 154 | restore_value: false 155 | - id: timer_id 156 | type: int 157 | restore_value: false 158 | 159 | 160 | # Declaration of our LED, with two effects: A "Slow Pulse" and a "Fast Pulse" that will be used as feedback for the different phases of our voice assistant 161 | light: 162 | - platform: esp32_rmt_led_strip 163 | rgb_order: GRB 164 | pin: GPIO17 #GPIO48 # On board light 165 | num_leds: 3 166 | rmt_channel: 0 167 | chipset: WS2812 168 | name: "Status LED" 169 | id: led_strip 170 | disabled_by_default: True 171 | entity_category: diagnostic 172 | icon: mdi:led-on 173 | default_transition_length: 0s 174 | effects: 175 | - pulse: 176 | name: "Slow Pulse" 177 | transition_length: 250ms 178 | update_interval: 250ms 179 | min_brightness: 60% 180 | max_brightness: 80% 181 | - pulse: 182 | name: "Fast Pulse" 183 | transition_length: 100ms 184 | update_interval: 100ms 185 | min_brightness: 60% 186 | max_brightness: 80% 187 | - addressable_scan: 188 | name: "Scanning" 189 | move_interval: 70ms 190 | scan_width: 1 191 | - pulse: 192 | name: "Waiting for wake word" 193 | min_brightness: 15% 194 | max_brightness: 35% 195 | transition_length: 3s # defaults to 1s 196 | update_interval: 3s 197 | 198 | 199 | script: 200 | # Master script controlling the LED, based on different conditions: initialization in progress, wifi and API connected, and the current voice assistant phase. 201 | # For the sake of simplicity and re-usability, the script calls child scripts defined below. 202 | # This script will be called every time one of these conditions is changing. 203 | - id: control_led 204 | then: 205 | - if: 206 | condition: 207 | lambda: return !id(init_in_progress); 208 | then: 209 | - if: 210 | condition: 211 | wifi.connected: 212 | then: 213 | - if: 214 | condition: 215 | api.connected: 216 | then: 217 | - lambda: | 218 | switch(id(voice_assistant_phase)) { 219 | case ${voice_assist_listening_phase_id}: 220 | id(control_led_voice_assist_listening_phase).execute(); 221 | break; 222 | case ${voice_assist_thinking_phase_id}: 223 | id(control_led_voice_assist_thinking_phase).execute(); 224 | break; 225 | case ${voice_assist_replying_phase_id}: 226 | id(control_led_voice_assist_replying_phase).execute(); 227 | break; 228 | case ${voice_assist_error_phase_id}: 229 | id(control_led_voice_assist_error_phase).execute(); 230 | break; 231 | case ${voice_assist_muted_phase_id}: 232 | id(control_led_voice_assist_muted_phase).execute(); 233 | break; 234 | case ${voice_assist_not_ready_phase_id}: 235 | id(control_led_voice_assist_not_ready_phase).execute(); 236 | break; 237 | case ${voice_assist_timer_finished_phase_id}: 238 | id(control_led_voice_assist_timer_finished_phase).execute(); 239 | break; 240 | default: 241 | id(control_led_voice_assist_idle_phase).execute(); 242 | break; 243 | } 244 | else: 245 | - script.execute: control_led_no_ha_connection_state 246 | else: 247 | - script.execute: control_led_no_ha_connection_state 248 | else: 249 | - script.execute: control_led_init_state 250 | 251 | 252 | # Script executed during initialisation: In this example: Turn the LED in green with a slow pulse 🟢 253 | - id: control_led_init_state 254 | then: 255 | - light.turn_on: 256 | id: led_strip 257 | blue: 0% 258 | red: 0% 259 | green: 100% 260 | brightness: 50% 261 | effect: "Fast Pulse" 262 | 263 | 264 | # Script executed when the device has no connection to Home Assistant: In this example: Turn off the LED 265 | - id: control_led_no_ha_connection_state 266 | then: 267 | - light.turn_off: 268 | id: led_strip 269 | 270 | 271 | # Script executed when the voice assistant is idle (waiting for a wake word): In this example: Turn the LED in white with 20% of brightness ⚪ 272 | - id: control_led_voice_assist_idle_phase 273 | then: 274 | - light.turn_on: 275 | id: led_strip 276 | blue: 10% 277 | red: 10% 278 | green: 100% 279 | effect: "Waiting for wake word" 280 | 281 | 282 | # Script executed when the voice assistant is listening to a command: In this example: Turn the LED in blue with a slow pulse 🔵 283 | - id: control_led_voice_assist_listening_phase 284 | then: 285 | - light.turn_on: 286 | id: led_strip 287 | blue: 100% 288 | red: 0% 289 | green: 0% 290 | brightness: 80% 291 | effect: "Scanning" 292 | 293 | 294 | # Script executed when the voice assistant is processing the command: In this example: Turn the LED in blue with a fast pulse 🔵 295 | - id: control_led_voice_assist_thinking_phase 296 | then: 297 | - light.turn_on: 298 | id: led_strip 299 | blue: 100% 300 | red: 30% 301 | green: 30% 302 | brightness: 80% 303 | effect: "Fast Pulse" 304 | 305 | 306 | # Script executed when the voice assistant is replying to a command: In this example: Turn the LED in blue, solid (no pulse) 🔵 307 | - id: control_led_voice_assist_replying_phase 308 | then: 309 | - light.turn_on: 310 | id: led_strip 311 | blue: 100% 312 | red: 0% 313 | green: 0% 314 | brightness: 99% 315 | effect: "none" 316 | 317 | 318 | # Script executed when the voice assistant encounters an error: In this example: Turn the LED in red, solid (no pulse) 🔴 319 | - id: control_led_voice_assist_error_phase 320 | then: 321 | - light.turn_on: 322 | id: led_strip 323 | blue: 0% 324 | red: 100% 325 | green: 0% 326 | brightness: 98% 327 | effect: "none" 328 | 329 | 330 | # Script executed when the voice assistant is muted: In this example: Turn off the LED 331 | - id: control_led_voice_assist_muted_phase 332 | then: 333 | - light.turn_off: 334 | id: led_strip 335 | 336 | 337 | # Script executed when the voice assistant is not ready: In this example: Turn off the LED 338 | - id: control_led_voice_assist_not_ready_phase 339 | then: 340 | - light.turn_off: 341 | id: led_strip 342 | 343 | - id: start_voice_assistant 344 | then: 345 | - if: 346 | condition: 347 | switch.is_off: mute 348 | then: 349 | - if: 350 | condition: 351 | lambda: return id(wake_word_engine_location).state == "In Home Assistant"; 352 | then: 353 | - lambda: id(va).set_use_wake_word(true); 354 | - voice_assistant.start_continuous: 355 | - if: 356 | condition: 357 | lambda: return id(wake_word_engine_location).state == "On device"; 358 | then: 359 | - lambda: id(va).set_use_wake_word(false); 360 | - micro_wake_word.start 361 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 362 | else: 363 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 364 | 365 | - id: stop_voice_assistant 366 | then: 367 | - if: 368 | condition: 369 | lambda: return id(wake_word_engine_location).state == "In Home Assistant"; 370 | then: 371 | - lambda: id(va).set_use_wake_word(false); 372 | - voice_assistant.stop: 373 | - if: 374 | condition: 375 | lambda: return id(wake_word_engine_location).state == "On device"; 376 | then: 377 | - voice_assistant.stop: 378 | - micro_wake_word.stop: 379 | - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id}; 380 | 381 | # Script executed during timer_finished 382 | - id: control_led_timer_finished 383 | then: 384 | - light.turn_on: 385 | id: led_strip 386 | blue: 50% 387 | red: 50% 388 | green: 100% 389 | brightness: 75% 390 | effect: "Slow Pulse" 391 | 392 | # TIMER scripts 393 | - id: control_led_voice_assist_timer_finished_phase 394 | then: 395 | - light.turn_on: 396 | id: led_strip 397 | blue: 50% 398 | red: 50% 399 | green: 50% 400 | brightness: 76% 401 | effect: "Fast Pulse" 402 | 403 | 404 | - id: check_if_timers_active 405 | then: 406 | - lambda: | 407 | const auto timers = id(va).get_timers(); 408 | bool output = false; 409 | if (timers.size() > 0) { 410 | for (auto &iterable_timer : timers) { 411 | if(iterable_timer.second.is_active) { 412 | output = true; 413 | } 414 | } 415 | } 416 | id(global_is_timer_active) = output; 417 | 418 | - id: check_if_timers 419 | then: 420 | - lambda: | 421 | const auto timers = id(va).get_timers(); 422 | bool output = false; 423 | if (timers.size() > 0) { 424 | output = true; 425 | } 426 | id(global_is_timer) = output; 427 | 428 | - id: timer_by_index 429 | then: 430 | - lambda: | 431 | const auto timers = id(va).get_timers(); 432 | auto timer_id = 0; 433 | if (timers.size() > 0) { 434 | for (auto &iterable_timer : timers) { 435 | if(iterable_timer.second.is_active) { 436 | if(timer_id == id(timer_list).active_index() ) { 437 | id(global_first_active_timer) = iterable_timer.second; 438 | } 439 | } 440 | timer_id++; 441 | } 442 | } 443 | 444 | - id: active_timer_widget 445 | then: 446 | - lambda: | 447 | id(check_if_timers_active).execute(); 448 | if (id(global_is_timer_active)){ 449 | id(timer_by_index).execute(); 450 | int hours_left = floor(id(global_first_active_timer).seconds_left / 3600); 451 | int minutes_left = floor((id(global_first_active_timer).seconds_left - hours_left * 3600) / 60); 452 | int seconds_left = id(global_first_active_timer).seconds_left - hours_left * 3600 - minutes_left * 60 ; 453 | auto display_hours = (hours_left < 10 ? "0" : "") + std::to_string(hours_left); 454 | auto display_minute = (minutes_left < 10 ? "0" : "") + std::to_string(minutes_left); 455 | auto display_seconds = (seconds_left < 10 ? "0" : "") + std::to_string(seconds_left) ; 456 | 457 | std::string display_string = ""; 458 | if (hours_left > 0) { 459 | display_string = id(global_first_active_timer).name + " " + display_hours + ":" + display_minute; 460 | } else { 461 | display_string = id(global_first_active_timer).name + " " + display_minute + ":" + display_seconds; 462 | } 463 | id(text_timer).publish_state(display_string.c_str()); 464 | } 465 | else { 466 | id(text_timer).publish_state("--:--"); 467 | } 468 | 469 | button: 470 | - platform: template 471 | name: "Delete timer" 472 | 473 | 474 | select: # Wake word engine location 475 | - platform: template 476 | entity_category: config 477 | name: Wake word engine location 478 | id: wake_word_engine_location 479 | icon: "mdi:account-voice" 480 | optimistic: true 481 | restore_value: true 482 | options: 483 | - In Home Assistant 484 | - On device 485 | initial_option: On device 486 | on_value: 487 | - if: 488 | condition: 489 | lambda: return !id(init_in_progress); 490 | then: 491 | - wait_until: 492 | lambda: return id(voice_assistant_phase) == ${voice_assist_muted_phase_id} || id(voice_assistant_phase) == ${voice_assist_idle_phase_id}; 493 | - if: 494 | condition: 495 | lambda: return x == "In Home Assistant"; 496 | then: 497 | - micro_wake_word.stop 498 | - delay: 500ms 499 | - if: 500 | condition: 501 | switch.is_off: mute 502 | then: 503 | - lambda: id(va).set_use_wake_word(true); 504 | - voice_assistant.start_continuous: 505 | - if: 506 | condition: 507 | lambda: return x == "On device"; 508 | then: 509 | - lambda: id(va).set_use_wake_word(false); 510 | - voice_assistant.stop 511 | - delay: 500ms 512 | - if: 513 | condition: 514 | switch.is_off: mute 515 | then: 516 | - micro_wake_word.start 517 | - platform: template 518 | name: Show timer 519 | id: timer_list 520 | icon: "mdi:account-voice" 521 | optimistic: true 522 | restore_value: true 523 | options: 524 | - "Timer 1" 525 | - "Timer 2" 526 | - "Timer 3" 527 | - "Timer 4" 528 | initial_option: "Timer 1" 529 | on_value: 530 | - logger.log: 531 | format: "# of timer: %d - Index: %d" 532 | args: ["id(va).get_timers().size()", "i"] 533 | 534 | # Declaration of the switch that will be used to turn on or off (mute) or voice assistant 535 | switch: # mute, timer_ringing 536 | - platform: template 537 | name: Mute 538 | id: mute 539 | optimistic: true 540 | restore_mode: RESTORE_DEFAULT_ON 541 | icon: mdi:assistant 542 | # When the switch is turned OFF (on Home Assistant): 543 | # Start the voice assistant component 544 | # Set the correct phase and run the script to refresh the LED status 545 | on_turn_off: 546 | - if: 547 | condition: 548 | lambda: return !id(init_in_progress); 549 | then: 550 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 551 | - if: 552 | condition: 553 | not: 554 | - voice_assistant.is_running 555 | then: 556 | - voice_assistant.start_continuous 557 | - script.execute: control_led 558 | # When the switch is turned ON (on Home Assistant): 559 | # Stop the voice assistant component 560 | # Set the correct phase and run the script to refresh the LED status 561 | on_turn_on: 562 | - if: 563 | condition: 564 | lambda: return !id(init_in_progress); 565 | then: 566 | - voice_assistant.stop 567 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 568 | - script.execute: control_led 569 | - platform: template 570 | name: Timer ringing 571 | id: timer_ringing 572 | optimistic: true 573 | restore_mode: ALWAYS_OFF 574 | icon: mdi:bell-off-outline 575 | #icon: mdi:bell-ring-outline 576 | on_turn_on: 577 | - delay: 2min 578 | - switch.turn_off: timer_ringing 579 | - platform: template 580 | name: Wake up bel 581 | id: wake_up_bell 582 | optimistic: true 583 | restore_mode: RESTORE_DEFAULT_ON 584 | icon: mdi:bell 585 | # icon: mdi:bell-cancel 586 | 587 | # Request and response text sensors so you can see what was understood 588 | text_sensor: 589 | - id: text_request 590 | name: Request 591 | platform: template 592 | icon: mdi:ear-hearing 593 | - id: text_response 594 | name: Response 595 | platform: template 596 | icon: mdi:react 597 | - id: text_timer 598 | name: Timer 599 | platform: template 600 | icon: mdi:camera-timer 601 | 602 | # This is our two i2s buses with the correct pins. 603 | # You can refer to the wiring diagram of our voice assistant for more details 604 | i2s_audio: 605 | - id: i2s_in 606 | i2s_lrclk_pin: GPIO18 #WS 607 | i2s_bclk_pin: GPIO2 #SCK 608 | - id: i2s_out 609 | i2s_lrclk_pin: GPIO6 #LRC 610 | i2s_bclk_pin: GPIO7 #BLCK 611 | 612 | # This is the declaration of our microphone. 613 | # It includes the data pin (You can refer to the wiring diagram of our voice assistant for more details) 614 | # It references the correct i2s bus declared above. 615 | microphone: 616 | - platform: i2s_audio 617 | id: external_microphone 618 | adc_type: external 619 | i2s_audio_id: i2s_in 620 | i2s_din_pin: GPIO4 621 | pdm: false 622 | channel: left 623 | bits_per_sample: 32 bit 624 | 625 | # This is the declaration of our speaker. 626 | # It includes the data pin (You can refer to the wiring diagram of our voice assistant for more details) 627 | # It references the correct i2s bus declared above. 628 | speaker: 629 | platform: i2s_audio 630 | id: external_speaker 631 | dac_type: external 632 | i2s_audio_id: i2s_out 633 | i2s_dout_pin: GPIO8 634 | mode: mono 635 | 636 | micro_wake_word: 637 | models: 638 | - ${micro_wake_word_model} 639 | on_wake_word_detected: 640 | - voice_assistant.start: 641 | wake_word: !lambda return wake_word; 642 | 643 | # This component allows you to "inject" any raw file into the firmware as a progmem byte array. 644 | external_components: 645 | - source: github://jesserockz/esphome-components 646 | components: [file] 647 | # Loading the external file(s) 648 | file: 649 | - id: timer_finished_wave_file 650 | file: ${timer_alarm_sound} #"sounds/timer_finished.wav" in the esphome/sounds folder 651 | 652 | # This is the declaration of our voice assistant 653 | # It references the microphone and speaker declared above. 654 | voice_assistant: 655 | id: va 656 | microphone: external_microphone 657 | speaker: external_speaker 658 | use_wake_word: true 659 | # You may have to test a few values for the 3 parameters below 660 | noise_suppression_level: 4 # Between 0 and 4 inclusive. Defaults to 0 (disabled). 661 | auto_gain: 31dBFS # Between 0dBFS and 31dBFS inclusive. Defaults to 0 (disabled). 662 | volume_multiplier: 8.0 # Must be larger than 0. Defaults to 1 (disabled). 663 | 664 | # When the voice assistant connects to HA: 665 | # Set init_in_progress to false (Initialization is over). 666 | # If the mute switch is off, start the voice assistant 667 | # In any case: Set the correct phase and run the script to refresh the LED status 668 | on_client_connected: 669 | - lambda: id(init_in_progress) = false; 670 | - lambda: id(text_timer).publish_state("--:--"); 671 | - if: 672 | condition: 673 | switch.is_off: mute 674 | then: 675 | - voice_assistant.start_continuous: 676 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 677 | else: 678 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 679 | - script.execute: control_led 680 | 681 | # When the voice assistant disconnects to HA: 682 | # Stop the voice assistant 683 | # Set the correct phase and run the script to refresh the LED status 684 | on_client_disconnected: 685 | - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id}; 686 | - voice_assistant.stop 687 | - script.execute: control_led 688 | 689 | # When the voice assistant starts to listen: Set the correct phase and run the script to refresh the LED status 690 | on_listening: 691 | - lambda: id(voice_assistant_phase) = ${voice_assist_listening_phase_id}; 692 | - if: 693 | condition: 694 | switch.is_on: wake_up_bell 695 | then: 696 | - lambda: id(external_speaker).play(id(timer_finished_wave_file), sizeof(id(timer_finished_wave_file))); 697 | - script.execute: control_led 698 | 699 | # When the voice assistant starts to think: Set the correct phase and run the script to refresh the LED status 700 | on_stt_vad_end: 701 | - lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id}; 702 | - script.execute: control_led 703 | 704 | # When the voice assistant starts to reply: Set the correct phase and run the script to refresh the LED status 705 | on_tts_stream_start: 706 | - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; 707 | - script.execute: control_led 708 | 709 | # When the voice assistant finished to reply: Set the correct phase and run the script to refresh the LED status 710 | on_tts_stream_end: 711 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 712 | - script.execute: control_led 713 | 714 | # When the voice gives a response then save this in a text sensor 715 | on_tts_start: 716 | - text_sensor.template.publish: 717 | id: text_response 718 | state: !lambda return x; 719 | 720 | # When the voice hears a request then save this in a text sensor 721 | on_stt_end: 722 | - text_sensor.template.publish: 723 | id: text_request 724 | state: !lambda return x; 725 | 726 | # An automation to perform when the voice assistant is finished all tasks. 727 | on_end: 728 | - wait_until: 729 | not: 730 | voice_assistant.is_running: 731 | - if: 732 | condition: 733 | and: 734 | - switch.is_off: mute 735 | - switch.is_off: timer_ringing 736 | - lambda: return id(wake_word_engine_location).state == "On device"; 737 | - lambda: return id(voice_assistant_phase) != ${voice_assist_timer_finished_phase_id}; 738 | then: 739 | - micro_wake_word.start: 740 | 741 | # Possible timer related actions 742 | # on_timer_started: 743 | # - script.execute: timer_timeline 744 | # - script.execute: active_timer_widget 745 | # on_timer_updated: 746 | # - script.execute: timer_timeline 747 | # - script.execute: active_timer_widget 748 | on_timer_cancelled: 749 | - lambda: id(text_timer).publish_state("--:--"); 750 | on_timer_tick: 751 | # This only gets activated when a timer is active 752 | #- script.execute: timer_timeline 753 | - script.execute: active_timer_widget 754 | - if: 755 | condition: 756 | lambda: | 757 | int t1 = id(va).get_timers().size(); 758 | const auto t2 = id(timer_list).active_index(); 759 | const unsigned int val = t2.value(); 760 | return ( t1 < (val + 1) ); 761 | then: 762 | - select.first: timer_list 763 | # - logger.log: "FIRST SELECTED !!!!!!" 764 | 765 | on_timer_finished: 766 | - script.execute: stop_voice_assistant 767 | - switch.turn_on: timer_ringing 768 | - script.execute: control_led 769 | - wait_until: 770 | not: 771 | microphone.is_capturing: 772 | - delay: 1s 773 | - while: 774 | condition: 775 | switch.is_on: timer_ringing 776 | then: 777 | - lambda: id(external_speaker).play(id(timer_finished_wave_file), sizeof(id(timer_finished_wave_file))); 778 | - delay: 1s 779 | - wait_until: 780 | not: 781 | speaker.is_playing: 782 | - switch.turn_off: timer_ringing 783 | - lambda: id(voice_assistant_phase) = ${voice_assist_timer_finished_phase_id}; 784 | - script.execute: start_voice_assistant 785 | 786 | # When the voice assistant encounters an error: 787 | # Set the error phase and run the script to refresh the LED status 788 | # Wait 1 second and set the correct phase (idle or muted depending on the state of the switch) and run the script to refresh the LED status 789 | on_error: 790 | - if: 791 | condition: 792 | lambda: return !id(init_in_progress); 793 | then: 794 | - lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id}; 795 | - script.execute: control_led 796 | - delay: 2s 797 | - if: 798 | condition: 799 | switch.is_off: mute 800 | then: 801 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 802 | else: 803 | - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; 804 | - script.execute: control_led 805 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP-Assistant 2 | An ESP32 S3 voice assistant for Home Assistant. Light weight and should compile on low end systems. 3 | 4 | The main goal of this project was to create a voice assistant (VA) for HA that is not depending on external files*. A second requirement was that for compiling you don't need much resources. This yaml compile many times faster then the ESP32-S3_box version. This version of the voice assistant is also capable to set multiple timers. 5 | 6 | _* There is one small dependency in the code if you want a sound when a timer is finished. This dependecy is only there when compiling. A future release may solve this. For now this isn't a big show stopper_ 7 | 8 | **UPDATE Version 2** 9 | 10 | While version 1 works it still suffered from stalls from time to time. 11 | The ESP32-S3-BOX version that I also have showed to be far more stable. Because of this I abandoned to 'no external files' concept and went for the full blown version and took the ESP32-S3-BOX version as start. I stripped everything that deals with the screen and the buttons on the box. I've added my own sensors and other tools. Things I like to see and control in HA such as diagnostics, request and response sensors and timer info. The hisardware stayed the same so this installs directly on the VoicePuck. This version is a lot heavier when compiling. My advise is to start with cleaning your build files and start compiling. When it fails just restart again. File locks will occure but they will be cleared. Files that are already compiled will be skipped so the processor load on lower end systems will handle in the end a full compile. 12 | 13 | **UPDATE Version 3** 14 | 15 | This the latest version. It is nearly the same as version 2 but this one makes it possible to have a continues conversation. 16 | 17 | **Hardware needed** 18 | - CPU: ESP32-S3 N16R8 19 | - Mic: INMP441 20 | - Amp: MAX98357A 21 | - Led: 3 WS2812B leds. 22 | - Speaker: 40mm Headset Driver Hifi Headphone Speaker Unit 32ohm [AliExpress](https://www.aliexpress.com/item/1005001352277084.html) 23 | 24 | I've include the schematic that I used and placed it in the media folder. It follows to general approach for this setup but i had to use 2 diffentent pins to get it working reliably. 25 | Schematic is based on the one found [here](https://smarthomecircle.com/How-to-setup-on-device-wake-word-for-voice-assistant-home-assistant#circuit-diagram-for-esp32-s3-with-inmp441-microphone--max98357a-audio-amplifier) 26 | 27 | **Requirements** 28 | - Home Assistant 29 | - ESPHome installed 30 | 31 | **Setup** 32 | Go to your config/esphome folder 33 | Create a folder and name it sounds 34 | Place the sounds file(s) there 35 | 36 | Have a look at the yaml file and change the following substitution for the wake word you want. 37 | At the moment okay_nabu, hey_jarvis and alexa are the available options 38 | 39 | _ micro_wake_word_model: okay_nabu _ 40 | 41 | And don't forget to create your secret SSID een password for the wifi section 42 | 43 | ssid: !secret wifi_ssid 44 | 45 | password: !secret wifi_password 46 | 47 | 48 | I've include the 3D print files for the body and the cover. I used a resin printer for these. 49 | Version 2 has a bit better wire management. 50 | 51 | ![VoicePuck-top](https://github.com/user-attachments/assets/860735a3-23ba-4d62-90ca-2ab0160d5e5d) 52 | 53 | ![VoicePuck-bottom](https://github.com/user-attachments/assets/b499539f-c70a-4596-942c-3c0e93b9055e) 54 | 55 | For comparison this is a Voice puck with with a dark cover which is perforated to let the light shine through. 56 | Left the Google home version, right the ESP Assistant 57 | 58 | ![VoicePuck-GoogleHome](https://github.com/user-attachments/assets/5bf028dc-2269-41cd-9bdd-1a9a011f9e1a) 59 | 60 | You can choose your cover freely. This one has a light wooden vineer cover. 61 | This thin wood let the light shine through without a problem. 62 | 63 | ![VoicePuck-white](https://github.com/user-attachments/assets/48c8b008-5497-4cdd-bbc0-d8b4e1e2929a) 64 | 65 | ![20240810_091030](https://github.com/user-attachments/assets/dab989f1-f182-4e96-a11f-3fbb608e9481) 66 | 67 | [Infinity VoicePuck](https://www.youtube.com/shorts/t8ANTnrit_I) 68 | 69 | The entities that are available in Home Assistant 70 | 71 | ![HA-device](https://github.com/user-attachments/assets/dca3b294-eca2-4e0a-87b3-f8b0228a2dab) 72 | 73 | 74 | **The troubleshooting part** 75 | 76 | I had one small issue that was simply my own fault. The speaker and LED's didn't work. I used the 5V pin to get my 5V from but I forgot to add a small solder blob to make the 5V pin an output pin. So make sure you close these pads with a bit of solder 77 | 78 | ![solderpad](https://github.com/user-attachments/assets/013e3fdf-9ada-4561-8fd3-8d0e5fba6034) 79 | -------------------------------------------------------------------------------- /hardware/VoicePuck-body.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshaiRey/ESP-Assistant/b92c7f9f4dfb3f1f185ce3e86a198140fe88465b/hardware/VoicePuck-body.stl -------------------------------------------------------------------------------- /hardware/VoicePuck-cover.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshaiRey/ESP-Assistant/b92c7f9f4dfb3f1f185ce3e86a198140fe88465b/hardware/VoicePuck-cover.stl -------------------------------------------------------------------------------- /hardware/VoicePuck_V2-body.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshaiRey/ESP-Assistant/b92c7f9f4dfb3f1f185ce3e86a198140fe88465b/hardware/VoicePuck_V2-body.stl -------------------------------------------------------------------------------- /hardware/VoicePuck_V2-lid.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshaiRey/ESP-Assistant/b92c7f9f4dfb3f1f185ce3e86a198140fe88465b/hardware/VoicePuck_V2-lid.stl -------------------------------------------------------------------------------- /hardware/Voicepuck-body.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshaiRey/ESP-Assistant/b92c7f9f4dfb3f1f185ce3e86a198140fe88465b/hardware/Voicepuck-body.png -------------------------------------------------------------------------------- /media/HA-device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshaiRey/ESP-Assistant/b92c7f9f4dfb3f1f185ce3e86a198140fe88465b/media/HA-device.png -------------------------------------------------------------------------------- /media/VoicePuck-GoogleHome.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshaiRey/ESP-Assistant/b92c7f9f4dfb3f1f185ce3e86a198140fe88465b/media/VoicePuck-GoogleHome.jpg -------------------------------------------------------------------------------- /media/VoicePuck-bottom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshaiRey/ESP-Assistant/b92c7f9f4dfb3f1f185ce3e86a198140fe88465b/media/VoicePuck-bottom.jpg -------------------------------------------------------------------------------- /media/VoicePuck-double.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshaiRey/ESP-Assistant/b92c7f9f4dfb3f1f185ce3e86a198140fe88465b/media/VoicePuck-double.jpg -------------------------------------------------------------------------------- /media/VoicePuck-top.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshaiRey/ESP-Assistant/b92c7f9f4dfb3f1f185ce3e86a198140fe88465b/media/VoicePuck-top.jpg -------------------------------------------------------------------------------- /media/VoicePuck-white.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshaiRey/ESP-Assistant/b92c7f9f4dfb3f1f185ce3e86a198140fe88465b/media/VoicePuck-white.jpg -------------------------------------------------------------------------------- /media/schematic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshaiRey/ESP-Assistant/b92c7f9f4dfb3f1f185ce3e86a198140fe88465b/media/schematic.png -------------------------------------------------------------------------------- /sounds/timer_finished.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshaiRey/ESP-Assistant/b92c7f9f4dfb3f1f185ce3e86a198140fe88465b/sounds/timer_finished.mp3 --------------------------------------------------------------------------------