├── README.md ├── docs ├── 01.jpg ├── 02.jpg ├── 03.jpg └── 04.jpg └── esphome-voice-satellite.yaml /README.md: -------------------------------------------------------------------------------- 1 | YAML code for ESPHome on a ESP32. For making a unit with microphone and speaker to use with Home Assistant: Voice Assistant. 2 | 3 | - It has support for addressable LED-strip. 4 | - 5 buttons for interactions. 5 | - It has audio sounds for key-click and 'Listening' start/end. 6 | 7 | I have addressed the issue with voice assistant programs like this sometime locks up. It happens when the recorded sound has no recognisable speach in it. And thus it doesn't feel the need to reply. Which would normally be followed by an on_stt_vad_end event. Instead it send out an error: 'stt-no-text-recognized'. 8 | 9 | > [!NOTE] 10 | > Notice that Volume is not supported by Voice Assistant as of now. But probably will be in the future. 11 | 12 | ## Buttons 13 | I used a cheap (and faulty) bluetooth speaker for this project. Which happened to have 5 buttons. Though you can of course choose to use less than 5 or none. 14 | - ⏻ Power: Wake Word On/Of 15 | - ✆ Call: Push To Listen 16 | - ⏯ Play/Pause: Change config selection. E.g. Volume, LED Brightness. (Future maybe: Mic-volume, Noise suppression, Gain). 17 | - ➕ Next: Config selection increase 18 | - ➖ Prev: Config selection decrease 19 | 20 | ## LED-strip 21 | I added an addressable LED-strip around it for state feedback. And made some custom light effects that can support my LED-brightness setting. 22 | 23 | - White: "Not Ready" Not connected to HA. 24 | - Orange: "Muted" Wake word disabled. 25 | - Green: "Idle" Wake word enabled. 26 | - Blue: "Listening" Listening for command. 27 | - Pink: "Thinking" Waiting for interpretation of speach. 28 | - Yellow: "Replying" Talking. 29 | - Red: "Error" Is not used. As I am unaware of any errors that would need it. 30 | 31 | State is also exposed in Home Assistant. So automations can be activated by it. 32 | 33 | Non-addressable and single LED could be used too. I have added the option 'led_addressable' in the code. But I don't have a setup to test it. And I expect that some of the code would have to be deleted to not throw errors during compilation. Like 'addressable_lambda'. 34 | 35 | My LED-Strip is a 35 LED (144 / m) RGBW. I do not use the White channel. The LED effects will scale with the number of LEDs. Though I will recommend at least 10 LEDs. 36 | 37 | ## Future 38 | 39 | I hope that it will be possible to send Text-To-Speach to the speaker in the future. Which could have many uses. Like annoucing which config-option is selected. 40 | 41 | I experimented with continous listening (no wake word). Which I didn't find especially useful. So I removed that code, as I didn't feel like debugging it right now. Might add it again later. 42 | 43 | > [!TIP] 44 | > - When connecting the physical modules together: Make sure the I2S connections are as short as possible. 45 | > - The amplifier module can use 2 Amp of power. Though I assume that is only if gain it set to max. (Gain here, is not refering the mic gain in the code.) 46 | > - If you use a LED-strip with many diodes, don't feed it power though the ESP32. But connect the strip and the ESP32 through a common power plug like an USB-C or a barrel plug. 47 | 48 | ## Photos of the unit I made 49 | ![01](/docs/01.jpg) 50 | There was just the perfect room for the electronics in a corner. I extended the screw 'pins' by 5 mm. So that there was room for a 6 mm latex tube (originally for aquariums) around all the edge. To diffuse the led-strip. 51 | 52 | ![02](/docs/02.jpg) 53 | Idle green. Which is more like teal. 54 | 55 | ![03](/docs/03.jpg) 56 | Brin-colored scan animation while thinking. 57 | 58 | ![04](/docs/04.jpg) 59 | Example of LED colors while setting volume: Blue indicate it's volumen. White fills up the rest of the space according to percentage set. 60 | -------------------------------------------------------------------------------- /docs/01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Djelle/ESPHomeVoiceSatellite/bf1976bb911ddbbfc4f74b497b4a9ee892bbfeb0/docs/01.jpg -------------------------------------------------------------------------------- /docs/02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Djelle/ESPHomeVoiceSatellite/bf1976bb911ddbbfc4f74b497b4a9ee892bbfeb0/docs/02.jpg -------------------------------------------------------------------------------- /docs/03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Djelle/ESPHomeVoiceSatellite/bf1976bb911ddbbfc4f74b497b4a9ee892bbfeb0/docs/03.jpg -------------------------------------------------------------------------------- /docs/04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Djelle/ESPHomeVoiceSatellite/bf1976bb911ddbbfc4f74b497b4a9ee892bbfeb0/docs/04.jpg -------------------------------------------------------------------------------- /esphome-voice-satellite.yaml: -------------------------------------------------------------------------------- 1 | # = PINS USED = 2 | # GPIO4 : LED Strip 3 | # GPIO14 : MAX98357A / BCLK 4 | # GPIO13 : MAX98357A / LRC 5 | # GPIO27 : MAX98357A / DIN 6 | # GPIO18 : Button Power (Wake Word On/Of) 7 | # GPIO19 : Button Next (Config selection Increse) 8 | # GPIO21 : Button Play/Pause (Change config selection. E.g. Volume, LED Brightness.)(Future maybe: Mic-volume, Noise suppression, Gain.) 9 | # GPIO22 : Button Call (Push To Listen) 10 | # GPIO23 : Button Prev (Config selection Decrese) 11 | # GPIO25 : INMP441 / SCK 12 | # GPIO26 : INMP441 / WS 13 | # GPIO33 : INMP441 / SD 14 | 15 | substitutions: 16 | name: esphome-voice-satellite 17 | friendly_name: 'Voice Satellite' 18 | led_pwr_factor: '0.3' # 0 - 1 (My LED-strip draws too much power.) 19 | led_addressable: 'true' # true / false. 20 | led_num: '35' # Number of LEDs 21 | error_phase: 'Error' 22 | idle_phase: 'Idle' 23 | listening_phase: 'Listening' 24 | thinking_phase: 'Thinking' 25 | replying_phase: 'Replying' 26 | not_ready_phase: 'Not Ready' 27 | muted_phase: 'Mic Muted' 28 | config_volume: '0' 29 | config_brightness: '1' 30 | config_timeout: '5s' 31 | 32 | globals: 33 | - id: 'use_wake_word_forced_off' 34 | type: bool 35 | restore_value: false 36 | initial_value: 'false' 37 | - id: 'led_brightness_pf' 38 | type: float 39 | restore_value: false 40 | initial_value: '0.5' 41 | - id: 'config_selected' 42 | type: int 43 | restore_value: false 44 | initial_value: '0' 45 | - id: 'config_led_red' 46 | type: float 47 | restore_value: false 48 | initial_value: '0' 49 | - id: 'config_led_green' 50 | type: float 51 | restore_value: false 52 | initial_value: '0' 53 | - id: 'config_led_blue' 54 | type: float 55 | restore_value: false 56 | initial_value: '0' 57 | - id: 'config_range_color' 58 | type: int 59 | restore_value: false 60 | initial_value: '0' 61 | - id: 'config_range_white' 62 | type: int 63 | restore_value: false 64 | initial_value: '0' 65 | - id: 'config_range_black' 66 | type: int 67 | restore_value: false 68 | initial_value: '0' 69 | 70 | 71 | esphome: 72 | name: ${name} 73 | friendly_name: ${friendly_name} 74 | name_add_mac_suffix: false 75 | project: 76 | name: djelle.voice_satellite 77 | version: '1.0' 78 | on_boot: 79 | priority: 600 80 | then: 81 | - text_sensor.template.publish: 82 | id: va_state 83 | state: "${not_ready_phase}" 84 | 85 | esp32: 86 | board: esp32dev 87 | framework: 88 | type: esp-idf 89 | version: recommended 90 | # type: arduino 91 | 92 | # Enable logging 93 | logger: 94 | level: DEBUG 95 | logs: 96 | light: INFO 97 | 98 | # Enable Home Assistant API 99 | api: 100 | 101 | # Allow Over-The-Air updates 102 | ota: 103 | 104 | # Allow provisioning Wi-Fi via serial 105 | improv_serial: 106 | 107 | wifi: 108 | ssid: !secret wifi_ssid 109 | password: !secret wifi_password 110 | 111 | # Enable fallback hotspot (captive portal) in case wifi connection fails 112 | ap: 113 | ssid: ${friendly_name} 114 | password: !secret wifi_password 115 | 116 | captive_portal: 117 | 118 | voice_assistant: 119 | id: va 120 | microphone: external_microphone 121 | speaker: external_speaker 122 | use_wake_word: false 123 | noise_suppression_level: 2 # 0, 1, 2, 3, 4 124 | auto_gain: 31dBFS # 0dBFS - 31dBFS 125 | volume_multiplier: 2 # >0 126 | 127 | on_wake_word_detected: 128 | - script.execute: sound_beep 129 | 130 | on_listening: 131 | - text_sensor.template.publish: 132 | id: va_state 133 | state: "${listening_phase}" 134 | 135 | on_stt_vad_end: 136 | - text_sensor.template.publish: 137 | id: va_state 138 | state: "${thinking_phase}" 139 | - script.execute: sound_beep 140 | - delay: 160ms 141 | - script.execute: sound_beep 142 | 143 | on_tts_stream_start: 144 | - text_sensor.template.publish: 145 | id: va_state 146 | state: "${replying_phase}" 147 | 148 | on_tts_stream_end: 149 | - script.execute: stop_listening 150 | 151 | on_error: 152 | - if: 153 | condition: 154 | and: 155 | - text_sensor.state: 156 | id: va_state 157 | state: "${thinking_phase}" 158 | - lambda: 'return (code == "stt-no-text-recognized");' # Error-code-text MUST be in double-quotes. 159 | then: 160 | - script.execute: stop_listening 161 | 162 | # - logger.log: 163 | # level: WARN 164 | # format: "Err.code: %s Msg: %s" 165 | # args: [ code.c_str(), message.c_str() ] 166 | # 167 | # [E][voice_assistant:653]: Error: no_wake_word - No wake word detected 168 | # [E][voice_assistant:653]: Error: stt-no-text-recognized - No text recognized 169 | 170 | on_client_connected: 171 | - if: 172 | condition: 173 | switch.is_on: use_wake_word 174 | then: 175 | - voice_assistant.start_continuous 176 | - text_sensor.template.publish: 177 | id: va_state 178 | state: "${idle_phase}" 179 | else: 180 | - text_sensor.template.publish: 181 | id: va_state 182 | state: "${muted_phase}" 183 | 184 | on_client_disconnected: 185 | - text_sensor.template.publish: 186 | id: va_state 187 | state: "${not_ready_phase}" 188 | 189 | i2s_audio: 190 | - id: i2s_in 191 | i2s_lrclk_pin: GPIO26 # WS 192 | i2s_bclk_pin: GPIO25 # SCK 193 | - id: i2s_out 194 | i2s_lrclk_pin: GPIO13 # LRC 195 | i2s_bclk_pin: GPIO14 # BCLK 196 | 197 | microphone: # INMP441 198 | - platform: i2s_audio 199 | i2s_audio_id: i2s_in 200 | id: external_microphone 201 | adc_type: external 202 | i2s_din_pin: GPIO33 # SD 203 | pdm: false 204 | bits_per_sample: 32bit 205 | 206 | speaker: # MAX98357A 207 | - platform: i2s_audio 208 | i2s_audio_id: i2s_out 209 | id: external_speaker 210 | dac_type: external 211 | i2s_dout_pin: GPIO27 # DIN 212 | mode: mono 213 | 214 | light: 215 | - platform: esp32_rmt_led_strip # Change to used type of LED-strip. 216 | chipset: WS2812 # Change to used chip on LED-strip. 217 | name: "Status LED" 218 | id: led 219 | icon: 'mdi:alarm-light' 220 | restore_mode: RESTORE_DEFAULT_ON 221 | rmt_channel: 0 222 | rgb_order: GRB 223 | pin: GPIO4 224 | num_leds: ${led_num} 225 | #max_refresh_rate: 226 | default_transition_length: 0s 227 | effects: 228 | - lambda: 229 | name: 'listening_effect' 230 | update_interval: 30ms 231 | lambda: |- 232 | static float step = 16; 233 | static esphome::Color rgb = Color(0,1,1); 234 | auto call = id(led).turn_on(); 235 | call.set_rgb(rgb[0],rgb[1]*abs(step)*0.06,rgb[2]); 236 | call.set_transition_length(30); 237 | call.perform(); 238 | step += 1; 239 | if (step > 16) 240 | step = -16; 241 | - lambda: 242 | name: 'thinking_effect' 243 | update_interval: 50ms 244 | lambda: |- 245 | static float step = -10; 246 | auto call = id(led).turn_on(); 247 | call.set_brightness( id(led_brightness_pf) * ((abs(step)*0.03)+0.7) ); 248 | call.perform(); 249 | step += 1; 250 | if (step > 10) 251 | step = -10; 252 | - addressable_lambda: 253 | name: 'adr_thinking_effect' 254 | update_interval: 30ms 255 | lambda: |- 256 | static int step = 0; 257 | static int size = it.size(); 258 | static int width = size/7; 259 | static esphome::Color rgb = Color(255,147,211); 260 | static esphome::Color fade = Color(16,32,16); 261 | if(initial_run) { 262 | step = 0; 263 | it.all() = Color(0,0,0); 264 | if( width < 1 ) width=1; 265 | return; 266 | } 267 | if(step < size-width) { 268 | if( step==0 ) { 269 | it.all() = it[size-1].get(); 270 | it.range(1, width+1) = rgb; 271 | } 272 | it[0] = Color(it[1].get())-fade; 273 | it.shift_right(1); 274 | } 275 | else { 276 | if( step==size-width ) { 277 | it.all() = it[0].get(); 278 | it.range(size-width-1, size-1) = rgb; 279 | } 280 | it[size-1] = Color(it[size-2].get())-fade; 281 | it.shift_left(1); 282 | } 283 | step += 1; 284 | if (step > (size-width)*2) { step = 0; } 285 | - lambda: 286 | name: 'replying_effect' 287 | update_interval: 300ms 288 | lambda: |- 289 | static int step = 0; 290 | float b = id(led_brightness_pf) * 0.25; 291 | auto call = id(led).turn_on(); 292 | call.set_transition_length( 200 + (((float)rand()/RAND_MAX)*200) ); // ms 293 | if (step == 0) call.set_brightness( (b*(float)rand()/RAND_MAX) + (b*3) ); 294 | else call.set_brightness( (b*(float)rand()/RAND_MAX) + (b*2) ); 295 | call.perform(); 296 | step += 1; 297 | if (step > 1) 298 | step = 0; 299 | - addressable_lambda: 300 | name: 'adr_replying_effect' 301 | update_interval: 100ms 302 | lambda: |- 303 | static int size = it.size(); 304 | static int nr = size/5; 305 | int nrlist[nr]; 306 | static esphome::Color rgb = Color(255,255,0); 307 | static esphome::Color fade = Color(16,16,0); 308 | if(initial_run) { 309 | if( nr < 1 ) nr=1; 310 | it.all() = Color(16,16,0); 311 | return; 312 | } 313 | for( int i=0; i < size; i++ ) { 314 | it[i] = Color(it[i].get())-fade;; 315 | } 316 | for( int i=0; i < nr; i++ ) { 317 | nrlist[i] = ((float)rand()/RAND_MAX)*size; 318 | it[nrlist[i]] = rgb; 319 | } 320 | - lambda: 321 | name: 'error_effect' 322 | update_interval: 500ms 323 | lambda: |- 324 | static int step = 0; 325 | auto call = id(led).turn_on(); 326 | if (step == 0) { 327 | call.set_transition_length(0); // ms 328 | call.set_brightness(id(led_brightness_pf)); 329 | } 330 | else { 331 | call.set_transition_length(200); // ms 332 | call.set_brightness(0.1); 333 | } 334 | call.perform(); 335 | step += 1; 336 | if (step > 1) 337 | step = 0; 338 | 339 | on_turn_on: 340 | - script.execute: state_set_led 341 | 342 | number: 343 | - platform: template 344 | name: 'Volumen' 345 | id: speaker_volume 346 | icon: 'mdi:volume-high' 347 | optimistic: true 348 | update_interval: never 349 | unit_of_measurement: '%' 350 | device_class: power_factor 351 | mode: slider 352 | min_value: 0 353 | max_value: 100 354 | step: 10 355 | restore_value: true 356 | # on_value: <=================================================================================== Set volume 357 | # then: 358 | 359 | 360 | - platform: template 361 | name: 'Brightness' 362 | id: led_brightness 363 | icon: 'mdi:brightness-7' 364 | optimistic: true 365 | update_interval: never 366 | unit_of_measurement: '%' 367 | device_class: power_factor 368 | mode: slider 369 | min_value: 10 370 | max_value: 100 371 | step: 1 372 | restore_value: true 373 | on_value: 374 | then: 375 | - lambda: "id(led_brightness_pf) = x*0.01*${led_pwr_factor};" 376 | - if: 377 | condition: 378 | for: 379 | time: 1s 380 | condition: 381 | api.connected: 382 | then: 383 | if: 384 | condition: 385 | - light.is_on: led 386 | then: 387 | - light.control: 388 | id: led 389 | color_mode: RGB 390 | brightness: !lambda "return(id(led_brightness_pf));" 391 | 392 | text_sensor: 393 | - platform: template 394 | name: 'Status' 395 | id: va_state 396 | update_interval: never 397 | on_value: 398 | - script.execute: state_set_led 399 | 400 | switch: 401 | - platform: template 402 | name: 'Use Wake Word' 403 | id: use_wake_word 404 | icon: 'mdi:chat-sleep' 405 | optimistic: true 406 | restore_mode: RESTORE_DEFAULT_OFF 407 | on_turn_on: 408 | - lambda: "id(va).set_use_wake_word(true);" 409 | - if: 410 | condition: 411 | not: 412 | - voice_assistant.is_running 413 | then: 414 | - voice_assistant.start_continuous 415 | - text_sensor.template.publish: 416 | id: va_state 417 | state: "${idle_phase}" 418 | 419 | on_turn_off: 420 | - lambda: "id(va).set_use_wake_word(false);" 421 | - voice_assistant.stop 422 | - text_sensor.template.publish: 423 | id: va_state 424 | state: "${muted_phase}" 425 | 426 | binary_sensor: 427 | # Push Button Power 428 | - platform: gpio 429 | id: pb_power 430 | pin: 431 | number: 18 432 | mode: 433 | input: true 434 | pulldown: true 435 | filters: 436 | - delayed_on: 10ms 437 | on_press: 438 | - script.execute: sound_key_click 439 | - delay: 150ms 440 | - if: 441 | condition: 442 | switch.is_on: use_wake_word 443 | then: 444 | - switch.template.publish: 445 | id: use_wake_word 446 | state: OFF 447 | - speaker.stop: 448 | id: external_speaker 449 | else: 450 | - switch.template.publish: 451 | id: use_wake_word 452 | state: ON 453 | 454 | # Push Button Next 455 | - platform: gpio 456 | id: pb_next 457 | pin: 458 | number: 19 459 | mode: 460 | input: true 461 | pulldown: true 462 | filters: 463 | - delayed_on: 10ms 464 | - autorepeat: 465 | - delay: 500ms 466 | time_off: 100ms 467 | time_on: 100ms 468 | on_press: 469 | - script.execute: sound_key_click 470 | - if: 471 | condition: 472 | lambda: "return(id(config_selected) == ${config_volume});" 473 | then: 474 | - number.increment: 475 | id: speaker_volume 476 | cycle: false 477 | - if: 478 | condition: 479 | lambda: "return(id(config_selected) == ${config_brightness});" 480 | then: 481 | - if: 482 | condition: 483 | lambda: "return( id(led_brightness).state <= 90 );" 484 | then: 485 | - number.set: 486 | id: led_brightness 487 | value: !lambda "return( id(led_brightness).state += 10 );" 488 | else: 489 | - number.to_max: 490 | id: led_brightness 491 | - script.execute: config_set_led 492 | 493 | # Push Button Previous 494 | - platform: gpio 495 | id: pb_prev 496 | pin: 497 | number: 23 498 | mode: 499 | input: true 500 | pulldown: true 501 | filters: 502 | - delayed_on: 10ms 503 | - autorepeat: 504 | - delay: 500ms 505 | time_off: 100ms 506 | time_on: 100ms 507 | on_press: 508 | - script.execute: sound_key_click 509 | - if: 510 | condition: 511 | lambda: "return(id(config_selected) == ${config_volume});" 512 | then: 513 | - number.decrement: 514 | id: speaker_volume 515 | cycle: false 516 | - if: 517 | condition: 518 | lambda: "return(id(config_selected) == ${config_brightness});" 519 | then: 520 | - if: 521 | condition: 522 | lambda: "return( id(led_brightness).state >= 20 );" 523 | then: 524 | - number.set: 525 | id: led_brightness 526 | value: !lambda "return( id(led_brightness).state -= 10 );" 527 | else: 528 | - number.to_min: 529 | id: led_brightness 530 | - script.execute: config_set_led 531 | 532 | # Push Button Play/Pause 533 | - platform: gpio 534 | id: pb_play 535 | pin: 536 | number: 21 537 | mode: 538 | input: true 539 | pulldown: true 540 | filters: 541 | - delayed_on: 10ms 542 | on_press: 543 | - script.execute: sound_key_click 544 | - if: 545 | condition: 546 | lambda: "return(id(config_selected) < 1);" 547 | then: 548 | - lambda: "id(config_selected) += 1;" 549 | else: 550 | - lambda: "id(config_selected) = 0;" 551 | - script.execute: config_set_led 552 | # - homeassistant.service: 553 | # service: tts.speak 554 | # data: 555 | # entity_id: tts.piper 556 | # cache: 'true' 557 | # media_player_entity_id: media_player.esphome_voice_satellite_1_test 558 | # message: 'Volume' 559 | 560 | # Push Button Call 561 | - platform: gpio 562 | id: pb_call 563 | pin: 564 | number: 22 565 | mode: 566 | input: true 567 | pulldown: true 568 | filters: 569 | - delayed_on: 10ms 570 | on_press: 571 | - if: 572 | condition: 573 | - voice_assistant.is_running 574 | then: 575 | - if: 576 | condition: 577 | - text_sensor.state: 578 | id: va_state 579 | state: ${idle_phase} 580 | then: 581 | - switch.template.publish: 582 | id: use_wake_word 583 | state: OFF 584 | - wait_until: 585 | not: 586 | voice_assistant.is_running 587 | - lambda: "id(use_wake_word_forced_off) = true;" 588 | - text_sensor.template.publish: 589 | id: va_state 590 | state: "${listening_phase}" 591 | - script.execute: sound_beep 592 | - delay: 400ms 593 | - voice_assistant.start 594 | else: 595 | - if: 596 | condition: 597 | - text_sensor.state: 598 | id: va_state 599 | state: ${muted_phase} 600 | then: 601 | - text_sensor.template.publish: 602 | id: va_state 603 | state: "${listening_phase}" 604 | - script.execute: sound_beep 605 | - delay: 400ms 606 | - voice_assistant.start 607 | 608 | script: 609 | - id: state_set_led 610 | then: 611 | - delay: 200ms # To prevent light.control from miss-writing LED-strip while VA is working hard. 612 | - if: 613 | condition: 614 | - text_sensor.state: 615 | id: va_state 616 | state: "${idle_phase}" 617 | then: 618 | - light.control: 619 | id: led 620 | color_mode: RGB 621 | brightness: !lambda "return(id(led_brightness_pf));" 622 | red: 0.0 623 | green: 1.0 624 | blue: 0.5 625 | effect: none 626 | - if: 627 | condition: 628 | - text_sensor.state: 629 | id: va_state 630 | state: "${listening_phase}" 631 | then: 632 | - light.control: 633 | id: led 634 | color_mode: RGB 635 | brightness: !lambda "return(id(led_brightness_pf));" 636 | red: 0.0 637 | green: 1.0 638 | blue: 1.0 639 | effect: listening_effect 640 | - if: 641 | condition: 642 | - text_sensor.state: 643 | id: va_state 644 | state: "${thinking_phase}" 645 | then: 646 | - if: 647 | condition: 648 | lambda: "return ${led_addressable};" 649 | then: 650 | - light.control: 651 | id: led 652 | color_mode: RGB 653 | brightness: !lambda "return(id(led_brightness_pf));" 654 | red: 1.0 655 | green: 0.0 656 | blue: 0.8 657 | effect: adr_thinking_effect 658 | else: 659 | - light.control: 660 | id: led 661 | color_mode: RGB 662 | brightness: !lambda "return(id(led_brightness_pf));" 663 | red: 1.0 664 | green: 0.0 665 | blue: 0.8 666 | effect: thinking_effect 667 | - if: 668 | condition: 669 | - text_sensor.state: 670 | id: va_state 671 | state: "${replying_phase}" 672 | then: 673 | - if: 674 | condition: 675 | lambda: "return ${led_addressable};" 676 | then: 677 | - light.control: 678 | id: led 679 | color_mode: RGB 680 | brightness: !lambda "return(id(led_brightness_pf));" 681 | red: 1.0 682 | green: 1.0 683 | blue: 0.0 684 | effect: adr_replying_effect 685 | else: 686 | - light.control: 687 | id: led 688 | color_mode: RGB 689 | brightness: !lambda "return(id(led_brightness_pf));" 690 | red: 1.0 691 | green: 1.0 692 | blue: 0.0 693 | effect: replying_effect 694 | - if: 695 | condition: 696 | - text_sensor.state: 697 | id: va_state 698 | state: "${error_phase}" 699 | then: 700 | - light.control: 701 | id: led 702 | color_mode: RGB 703 | brightness: !lambda "return(id(led_brightness_pf));" 704 | red: 1.0 705 | green: 0.0 706 | blue: 0.0 707 | effect: error_effect 708 | - if: 709 | condition: 710 | - text_sensor.state: 711 | id: va_state 712 | state: "${muted_phase}" 713 | then: 714 | - light.control: 715 | id: led 716 | color_mode: RGB 717 | brightness: !lambda "return(id(led_brightness_pf));" 718 | red: 1.0 719 | green: 0.7 720 | blue: 0.0 721 | effect: none 722 | - if: 723 | condition: 724 | - text_sensor.state: 725 | id: va_state 726 | state: "${not_ready_phase}" 727 | then: 728 | - light.control: 729 | id: led 730 | color_mode: RGB 731 | brightness: !lambda "return(id(led_brightness_pf));" 732 | red: 1.0 733 | green: 1.0 734 | blue: 1.0 735 | effect: none 736 | 737 | 738 | - id: stop_listening 739 | then: 740 | - if: 741 | condition: 742 | switch.is_on: use_wake_word 743 | then: 744 | - text_sensor.template.publish: 745 | id: va_state 746 | state: "${idle_phase}" 747 | else: 748 | - if: 749 | condition: 750 | lambda: "return(id(use_wake_word_forced_off));" 751 | then: 752 | - switch.template.publish: 753 | id: use_wake_word 754 | state: ON 755 | - lambda: "id(use_wake_word_forced_off) = false;" 756 | - text_sensor.template.publish: 757 | id: va_state 758 | state: "${idle_phase}" 759 | else: 760 | - text_sensor.template.publish: 761 | id: va_state 762 | state: "${muted_phase}" 763 | 764 | 765 | - id: config_set_led 766 | then: 767 | - script.execute: config_led_set_vars 768 | - if: 769 | condition: 770 | lambda: "return ${led_addressable};" 771 | then: 772 | - light.control: 773 | id: led 774 | color_mode: RGB 775 | brightness: !lambda "return(id(led_brightness_pf));" 776 | effect: none 777 | - light.addressable_set: 778 | id: led 779 | range_from: 0 780 | range_to: !lambda "return id(config_range_color) - 1;" 781 | red: !lambda "return id(config_led_red);" 782 | green: !lambda "return id(config_led_green);" 783 | blue: !lambda "return id(config_led_blue);" 784 | - light.addressable_set: 785 | id: led 786 | range_from: !lambda "return id(config_range_color);" 787 | range_to: !lambda "return id(config_range_color) + id(config_range_white);" 788 | red: 1 789 | green: 1 790 | blue: 1 791 | - if: 792 | condition: 793 | lambda: "return 1 + id(config_range_white) < ${led_num} - id(config_range_color) - 1;" 794 | then: 795 | - light.addressable_set: 796 | id: led 797 | range_from: !lambda "return 1 + id(config_range_color) + id(config_range_white);" 798 | range_to: !lambda "return ${led_num} - id(config_range_color) - 1;" 799 | red: 0 800 | green: 0 801 | blue: 0 802 | - light.addressable_set: 803 | id: led 804 | range_from: !lambda "return ${led_num} - id(config_range_color);" 805 | range_to: !lambda "return ${led_num} - 1;" 806 | red: !lambda "return id(config_led_red);" 807 | green: !lambda "return id(config_led_green);" 808 | blue: !lambda "return id(config_led_blue);" 809 | else: 810 | - light.control: 811 | id: led 812 | color_mode: RGB 813 | brightness: !lambda "return(id(led_brightness_pf));" 814 | red: !lambda "return id(config_led_red);" 815 | green: !lambda "return id(config_led_green);" 816 | blue: !lambda "return id(config_led_blue);" 817 | effect: none 818 | 819 | 820 | - id: config_led_set_vars 821 | then: 822 | - script.execute: config_timeout_timer 823 | - lambda: |- 824 | id(config_range_color) = ${led_num}/5; 825 | if( id(config_range_color) < 1) id(config_range_color)=1; 826 | static float w = ${led_num} - (id(config_range_color) * 2); 827 | if( w < 1 ) w=1; 828 | switch(id(config_selected)) { 829 | case ${config_volume}: 830 | id(config_range_white) = (int)( id(speaker_volume).state/round(100/w) ); 831 | id(config_led_red) = 0; 832 | id(config_led_green) = 0; 833 | id(config_led_blue) = 1; 834 | break; 835 | case ${config_brightness}: 836 | id(config_range_white) = (int)( id(led_brightness).state/round(100/w) ); 837 | id(config_led_red) = 1; 838 | id(config_led_green) = 1; 839 | id(config_led_blue) = 0; 840 | break; 841 | } 842 | 843 | 844 | - id: config_timeout_timer 845 | mode: restart 846 | then: 847 | - delay: ${config_timeout} 848 | - script.execute: state_set_led 849 | 850 | 851 | - id: sound_beep 852 | then: 853 | - if: 854 | condition: 855 | not: 856 | - speaker.is_playing 857 | # id: external_speaker 858 | then: 859 | - speaker.play: 860 | id: external_speaker 861 | data: [0x00,0x00,0xFF,0x05,0x00,0xFE,0x00,0x00,0xFC,0x03,0x01,0xFD,0x02,0x01,0xFD,0x00,0x02,0xF9,0x03,0x00,0xFD,0xFF,0x03,0xFA,0x01,0x03,0xFC,0x01,0x02,0xFD,0x01,0x06,0xFA,0x03,0x02,0xFF,0xFE,0x05,0xFB,0xFF,0x03,0xFC,0xFF, 862 | 0x01,0xFB,0xFB,0x05,0xFA,0x00,0x00,0x00,0xFB,0x07,0xFE,0xFF,0x03,0x00,0x00,0x05,0x01,0xFA,0x06,0xFD,0x00,0xFD,0x02,0xF6,0x04,0xFD,0xFD,0xFE,0xFF,0xF9,0x02,0x03,0xFA,0x04,0x00,0x03,0xFE,0x0A,0xF9,0x05,0x00,0x03,0xFE,0x05, 863 | 0xF9,0xFE,0x01,0xFB,0xFD,0xFB,0x00,0xF5,0x07,0xF9,0x02,0xFA,0x05,0xFA,0x0B,0xFF,0x01,0x02,0x07,0x01,0x01,0x07,0xF7,0x05,0xFC,0x03,0xF6,0x05,0xF0,0x03,0xFB,0xFF,0xF5,0x05,0xFB,0xFD,0x09,0xFC,0x02,0x00,0x0B,0xFA,0x11,0xF8, 864 | 0x06,0xFF,0x0C,0xF2,0x08,0xFA,0xF7,0x00,0xFF,0xF8,0xF8,0x04,0xEE,0x0A,0xFA,0x00,0xF7,0x14,0xF1,0x0E,0x05,0x00,0xFE,0x0F,0xFE,0xFF,0x0C,0xF1,0x03,0x01,0xFF,0xE9,0x12,0xE8,0xFF,0xFF,0x01,0xEC,0x0D,0xF9,0xFB,0x10,0xFD,0xFD, 865 | 0x0C,0x10,0xED,0x1D,0xF7,0xFF,0x00,0x11,0xE6,0x0F,0xF7,0xF1,0x02,0x03,0xE8,0xFD,0x0C,0xE2,0x10,0x01,0xFB,0xF7,0x1E,0xE9,0x15,0x09,0xFC,0xFE,0x1D,0xEF,0xFE,0x15,0xE7,0xFE,0x08,0xF9,0xE4,0x19,0xDE,0xFF,0x05,0xFC,0xE4,0x21, 866 | 0xF2,0xF8,0x1C,0xFD,0xF5,0x16,0x0D,0xE9,0x26,0xF3,0xF9,0x0A,0x0F,0xD4,0x1B,0xF4,0xE6,0x07,0x07,0xDD,0x07,0x0B,0xDD,0x18,0x04,0xF0,0x04,0x28,0xDB,0x1E,0x0F,0xF2,0xFF,0x24,0xE3,0x04,0x16,0xDE,0x00,0x10,0xE7,0xE6,0x25,0xD4, 867 | 0xFF,0x11,0xF6,0xE4,0x2C,0xE9,0xFC,0x23,0xF7,0xF3,0x29,0x00,0xE4,0x2F,0xED,0xEF,0x13,0x08,0xD0,0x24,0xEC,0xE3,0x10,0x01,0xD2,0x1A,0x08,0xD8,0x22,0x09,0xE7,0x10,0x24,0xD6,0x27,0x0D,0xE9,0x0B,0x24,0xD1,0x0D,0x15,0xD4,0x01, 868 | 0x17,0xDB,0xF1,0x23,0xCE,0x05,0x17,0xE8,0xF0,0x37,0xDF,0x01,0x2B,0xF0,0xF2,0x30,0xF3,0xEB,0x31,0xE5,0xEE,0x1E,0xF4,0xD0,0x2B,0xE5,0xDF,0x1B,0xFD,0xD2,0x25,0xFE,0xDB,0x2A,0x04,0xE3,0x24,0x19,0xD4,0x2C,0x0C,0xDE,0x13,0x1B, 869 | 0xCD,0x16,0x0E,0xCF,0x0A,0x11,0xCD,0x02,0x1F,0xCD,0x0B,0x1E,0xE0,0xFD,0x31,0xDB,0x0A,0x2B,0xE7,0xFE,0x32,0xE2,0xF3,0x2D,0xDF,0xEC,0x24,0xE7,0xDD,0x29,0xDF,0xE4,0x22,0xEE,0xDD,0x2E,0xF6,0xE3,0x2E,0x01,0xE2,0x2B,0x09,0xDD, 870 | 0x2D,0x04,0xDD,0x20,0x08,0xCE,0x1A,0x06,0xCF,0x10,0x0E,0xCE,0x0F,0x13,0xD2,0x12,0x19,0xDA,0x12,0x26,0xDD,0x0F,0x27,0xE0,0x04,0x28,0xDD,0xFF,0x24,0xDB,0xF5,0x20,0xD8,0xEE,0x21,0xE0,0xEB,0x26,0xE8,0xEB,0x29,0xEF,0xED,0x2C, 871 | 0xF7,0xEE,0x2F,0xF9,0xE8,0x25,0xFE,0xDD,0x22,0xFB,0xDC,0x19,0xFE,0xD5,0x16,0xFE,0xD7,0x18,0x09,0xDD,0x15,0x16,0xDD,0x1A,0x15,0xE4,0x12,0x1D,0xE0,0x12,0x16,0xDE,0x05,0x16,0xDD,0xFB,0x19,0xDA,0xFD,0x15,0xE3,0xF4,0x1F,0xE2, 872 | 0x00,0x1F,0xF1,0xF8,0x25,0xF3,0xF5,0x24,0xF1,0xF5,0x1D,0xF8,0xE8,0x1E,0xEC,0xEB,0x12,0xFA,0xE1,0x17,0xF9,0xE6,0x17,0xFF,0xE9,0x14,0x0A,0xE8,0x1E,0x05,0xF0,0x0F,0x12,0xE4,0x12,0x08,0xE8,0x09,0x0B,0xE5,0x02,0x0B,0xE0,0x07, 873 | 0x08,0xEF,0xF8,0x19,0xE9,0x02,0x12,0xF5,0xF9,0x19,0xFA,0xF9,0x1B,0xF6,0xFA,0x0D,0x04,0xEA,0x14,0xFA,0xF1,0x08,0x03,0xEA,0x06,0x03,0xE8,0x09,0x03,0xF4,0xFD,0x11,0xED,0x08,0x0B,0xF8,0xFE,0x12,0xF9,0xFF,0x12,0xF6,0xFF,0x09, 874 | 0x00,0xF1,0x0F,0xF6,0xF8,0x05,0x00,0xF1,0x06,0xFE,0xF1,0x08,0xFF,0xF9,0x00,0x0A,0xF3,0x09,0x04,0xFB,0x01,0x0B,0xFA,0x03,0x09,0xF7,0x01,0x04,0xFD,0xF8,0x09,0xF6,0xFD,0x03,0xFD,0xF8,0x04,0xFB,0xFA,0x07,0xFD,0xFD,0x02,0x04, 875 | 0xF9,0x08,0x00,0xFE,0x02,0x04,0xFC,0x04,0x02,0xFA,0x02,0x00,0xFC,0xFE,0x03,0xF9,0x00,0x01,0xFD,0xFD,0x03,0xFB,0x00,0x03,0xFE,0xFF,0x03,0x01,0xFC,0x06,0xFD,0x01,0x00,0x00,0xFE,0x00,0x00,0xFB,0x03,0xFD,0x00,0xFF,0x00,0x00] 876 | - wait_until: 877 | not: 878 | speaker.is_playing 879 | 880 | 881 | - id: sound_key_click 882 | then: 883 | - if: 884 | condition: 885 | or: 886 | - text_sensor.state: 887 | id: va_state 888 | state: ${replying_phase} );" 889 | - speaker.is_playing 890 | # id: external_speaker 891 | else: 892 | - speaker.play: 893 | id: external_speaker 894 | data: [0x01,0x02,0x04,0x06,0x07,0x08,0x08,0x06,0x01,0xFA,0xF4,0xEF,0xEE,0xF1,0xF7,0xFF,0x07,0x0E,0x11,0x11,0x10,0x0C,0x06,0xFF,0xF8,0xF1,0xEC,0xEA,0xEC,0xF2,0xF9,0xFF,0x05,0x0A,0x10,0x14,0x17,0x17,0x14,0x0B,0xFD,0xED, 895 | 0xE0,0xD9,0xDB,0xE6,0xF7,0x0A,0x19,0x20,0x1E,0x14,0x05,0xF4,0xE5,0xDF,0xE4,0xF4,0x0C,0x24,0x36,0x3D,0x35,0x20,0xFF,0xDD,0xC0,0xAE,0xA9,0xB0,0xC4,0xE0,0x03,0x26,0x48,0x62,0x70,0x6D,0x59,0x35,0x08,0xD8,0xAC,0x8D,0x80, 896 | 0x85,0x9C,0xC1,0xF0,0x21,0x4E,0x6F,0x7F,0x7A,0x63,0x3D,0x0F,0xE0,0xB7,0x9B,0x90,0x96,0xAC,0xCE,0xF6,0x1D,0x3C,0x50,0x55,0x4D,0x3A,0x20,0x04,0xEC,0xD8,0xCC,0xC9,0xCD,0xD8,0xE8,0xF9,0x0A,0x17,0x1F,0x20,0x1C,0x12,0x07, 897 | 0xFC,0xF2,0xEC,0xEB,0xED,0xF3,0xFA,0x02,0x0A,0x10,0x12,0x12,0x0E,0x07,0xFE,0xF5,0xED,0xEA,0xEB,0xEF,0xF7,0x00,0x0A,0x10,0x13,0x12,0x0F,0x09,0x02,0xFB,0xF5,0xF3,0xF2,0xF3,0xF6,0xFB,0xFF,0x03,0x05,0x07,0x08,0x07,0x05, 898 | 0x03,0x01,0xFF,0xFE,0xFC,0xFB,0xFB,0xFC,0xFC,0xFD,0xFE,0xFF,0x00,0x01,0x02,0x02,0x01] 899 | - wait_until: 900 | not: 901 | - speaker.is_playing 902 | # id: external_speaker 903 | --------------------------------------------------------------------------------