diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5de73da..6a937d7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: uses: esphome/workflows/.github/workflows/build.yml@main with: files: | - voice-kit.factory.yaml + home-assistant-voice.factory.yaml esphome-version: dev release-summary: ${{ github.event_name == 'release' && github.event.release.body || '' }} release-url: ${{ github.event_name == 'release' && github.event.release.html_url || '' }} @@ -50,18 +50,18 @@ jobs: [Download][download] and extract the firmware to install with https://web.esphome.io - Make sure to choose \`esphome-voice-kit-esp32s3/esphome-voice-kit-esp32s3.factory.bin\`. + Make sure to choose \`home-assistant-voice-esp32s3.factory.bin\`. [download]: ${url}` }) upload: - if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev') + if: github.event_name == 'release' name: Upload to R2 needs: - build-firmware uses: esphome/workflows/.github/workflows/upload.yml@main with: - directory: voice-kit + directory: home-assistant-voice-pe version: ${{ needs.build-firmware.outputs.version }} secrets: inherit diff --git a/voice-kit.factory.yaml b/home-assistant-voice.factory.yaml similarity index 88% rename from voice-kit.factory.yaml rename to home-assistant-voice.factory.yaml index c26f093..f1c377b 100644 --- a/voice-kit.factory.yaml +++ b/home-assistant-voice.factory.yaml @@ -11,11 +11,11 @@ packages: wifi: on_disconnect: - ble.enable: - voice-kit: !include voice-kit.yaml + home-assistant-voice: !include home-assistant-voice.yaml esphome: project: - name: esphome.voice-kit + name: Nabu Casa.Home Assistant Voice PE version: dev ota: @@ -31,7 +31,7 @@ update: source: https://firmware.esphome.io/home-assistant-voice-pe/home-assistant-voice/manifest.json dashboard_import: - package_import_url: github://esphome/voice-kit/voice-kit.yaml + package_import_url: github://esphome/voice-kit/home-assistant-voice.yaml wifi: on_connect: diff --git a/home-assistant-voice.yaml b/home-assistant-voice.yaml new file mode 100644 index 0000000..54b67d4 --- /dev/null +++ b/home-assistant-voice.yaml @@ -0,0 +1,1476 @@ +substitutions: + # Phases of the Voice Assistant + # The voice assistant is ready to be triggered by a wake word + voice_assist_idle_phase_id: '1' + # The voice assistant is waiting for a voice command (after being triggered by the wake word) + voice_assist_waiting_for_command_phase_id: '2' + # The voice assistant is listening for a voice command + voice_assist_listening_for_command_phase_id: '3' + # The voice assistant is currently processing the command + voice_assist_thinking_phase_id: '4' + # The voice assistant is replying to the command + voice_assist_replying_phase_id: '5' + # The voice assistant is not ready + voice_assist_not_ready_phase_id: '10' + # The voice assistant encountered an error + voice_assist_error_phase_id: '11' + +esphome: + name: home-assistant-voice + friendly_name: Home Assistant Voice + name_add_mac_suffix: true + min_version: 2024.9.0 + platformio_options: + board_build.flash_mode: dio + on_boot: + priority: 375 + then: + # Run the script to refresh the LED status + - script.execute: control_leds + - delay: 1s + # TODO: make it internal before launch + - switch.turn_on: internal_speaker_amp + # If the hardware switch is ON, force the software switch to be ON too. This covers the case where the Mute hardware switch is operated when the device is turned off + - if: + condition: + binary_sensor.is_on: hardware_mute_switch + then: + - switch.template.publish: + id: master_mute_switch + state: ON + # If after 10 minutes, 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 + - delay: 10min + - if: + condition: + lambda: return id(init_in_progress); + then: + - lambda: id(init_in_progress) = false; + - script.execute: control_leds + +esp32: + board: esp32-s3-devkitc-1 + variant: esp32s3 + flash_size: 8MB + framework: + type: esp-idf + version: recommended + sdkconfig_options: + CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y" + CONFIG_ESP32S3_DATA_CACHE_64KB: "y" + CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y" + CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB: "y" + CONFIG_ESP32_S3_BOX_BOARD: "y" + CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY: "y" + + CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP: "y" + + # Settings based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702 + CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM: "16" + CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM: "512" + CONFIG_ESP32_WIFI_STATIC_TX_BUFFER: "y" + CONFIG_ESP32_WIFI_TX_BUFFER_TYPE: "0" + CONFIG_ESP32_WIFI_STATIC_TX_BUFFER_NUM: "8" + CONFIG_ESP32_WIFI_CACHE_TX_BUFFER_NUM: "32" + CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED: "y" + CONFIG_ESP32_WIFI_TX_BA_WIN: "16" + CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED: "y" + CONFIG_ESP32_WIFI_RX_BA_WIN: "32" + CONFIG_LWIP_MAX_ACTIVE_TCP: "16" + CONFIG_LWIP_MAX_LISTENING_TCP: "16" + CONFIG_TCP_MAXRTX: "12" + CONFIG_TCP_SYNMAXRTX: "6" + CONFIG_TCP_MSS: "1436" + CONFIG_TCP_MSL: "60000" + CONFIG_TCP_SND_BUF_DEFAULT: "65535" + CONFIG_TCP_WND_DEFAULT: "65535" # Adjusted from linked settings to avoid compilation error + CONFIG_TCP_RECVMBOX_SIZE: "512" + CONFIG_TCP_QUEUE_OOSEQ: "y" + CONFIG_TCP_OVERSIZE_MSS: "y" + CONFIG_LWIP_WND_SCALE: "y" + CONFIG_TCP_RCV_SCALE: "3" + CONFIG_LWIP_TCPIP_RECVMBOX_SIZE: "512" + + CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST: "y" + CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY: "y" + +wifi: + ap: + on_connect: + - script.execute: control_leds + on_disconnect: + - script.execute: control_leds + +logger: + level: DEBUG + logs: + sensor: WARN # avoids logging debug sensor updates + +api: + on_client_connected: + - script.execute: control_leds + on_client_disconnected: + - script.execute: control_leds + +ota: + - platform: esphome + id: ota_esphome + +i2c: + sda: GPIO5 + scl: GPIO6 + frequency: 400kHz + +psram: + mode: octal + speed: 80MHz + +globals: + # Global index for our LEDs. So that switching between different animation does not lead to unwanted effects. + - id: global_led_animation_index + type: int + restore_value: no + initial_value: '0' + # Global initialization variable. Initialized to true and set to false once everything is connected. Only used to have a smooth "plugging" experience + - id: init_in_progress + type: bool + restore_value: no + initial_value: 'true' + # Global variable tracking the phase of the voice assistant (defined above). Initialized to not_ready + - id: voice_assistant_phase + type: int + restore_value: no + initial_value: ${voice_assist_not_ready_phase_id} + # Global variable tracking if the dial was recently touched. + - id: dial_touched + type: bool + restore_value: no + initial_value: 'false' + # Global variable tracking if the jack has been plugged touched. + - id: jack_plugged_recently + type: bool + restore_value: no + initial_value: 'false' + # Global variable tracking if the jack has been unplugged touched. + - id: jack_unplugged_recently + type: bool + restore_value: no + initial_value: 'false' + +switch: + # This is the master mute switch. It is exposed to Home Assistant. The user can only turn it on and off if the hardware switch is off. (The hardware switch overrides the software one) + - platform: template + id: master_mute_switch + restore_mode: RESTORE_DEFAULT_OFF + icon: "mdi:microphone-off" + name: Mute + entity_category: config + turn_on_action: + - if: + condition: + binary_sensor.is_off: hardware_mute_switch + then: + - switch.template.publish: + id: master_mute_switch + state: ON + turn_off_action: + - if: + condition: + binary_sensor.is_off: hardware_mute_switch + then: + - switch.template.publish: + id: master_mute_switch + state: OFF + on_turn_on: + - script.execute: control_leds + on_turn_off: + - script.execute: control_leds + # Wake Word Sound Switch. + - platform: template + id: wake_sound + name: Wake sound + icon: "mdi:bullhorn" + entity_category: config + optimistic: true + restore_mode: RESTORE_DEFAULT_ON + # Internal switch to track when a timer is ringing on the device. + - platform: template + id: timer_ringing + optimistic: true + internal: true + restore_mode: ALWAYS_OFF + on_turn_off: + - delay: 200ms + - nabu.set_ducking: + decibel_reduction: 0 + duration: 1.0s + - script.execute: control_leds + on_turn_on: + - nabu.set_ducking: + decibel_reduction: 20 + duration: 0.0s + - script.execute: ring_timer + - script.execute: control_leds + - delay: 15min + - switch.turn_off: timer_ringing + - platform: gpio + pin: GPIO47 + id: internal_speaker_amp + name: "Internal speaker amp" + entity_category: config + restore_mode: ALWAYS_OFF + internal: true +binary_sensor: + # Center Button. Used for many things (See on_multi_click) + - platform: gpio + id: center_button + pin: + number: GPIO0 + inverted: true + on_press: + - script.execute: control_leds + on_release: + - script.execute: control_leds + on_multi_click: + # Simple Click: + # - Abort "things" in order + # - Timer + # - Announcements + # - Voice Assistant Pipeline run + # - Music + # - Starts the voice assistant if it is not yet running and if the device is not muted. + - timing: + - ON for at most 1s + - OFF for at least 0.25s + then: + - if: + condition: + switch.is_on: timer_ringing + then: + - switch.turn_off: timer_ringing + else: + - if: + condition: + lambda: return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; + then: + - lambda: | + id(nabu_media_player) + ->make_call() + .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP) + .set_announcement(true) + .perform(); + else: + - if: + condition: + voice_assistant.is_running: + then: + - voice_assistant.stop: + else: + - if: + condition: + media_player.is_playing: + then: + - media_player.pause: + else: + - if: + condition: + and: + - switch.is_off: master_mute_switch + - not: + voice_assistant.is_running + then: + - script.execute: + id: play_sound + priority: true + sound_file: !lambda return id(center_button_press_sound); + - wait_until: + lambda: |- + return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; + - wait_until: + not: + lambda: |- + return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; + - voice_assistant.start: + # Double Click + # . Exposed as an event entity. To be used in automations inside Home Assistant + - timing: + - ON for at most 1s + - OFF for at most 0.25s + - ON for at most 1s + - OFF for at least 0.25s + then: + - script.execute: + id: play_sound + priority: false + sound_file: !lambda return id(center_button_double_press_sound); + - event.trigger: + id: button_press_event + event_type: "double_press" + # Triple Click + # . Exposed as an event entity. To be used in automations inside Home Assistant + - timing: + - ON for at most 1s + - OFF for at most 0.25s + - ON for at most 1s + - OFF for at most 0.25s + - ON for at most 1s + - OFF for at least 0.25s + then: + - script.execute: + id: play_sound + priority: false + sound_file: !lambda return id(center_button_triple_press_sound); + - event.trigger: + id: button_press_event + event_type: "triple_press" + # Long Press + # . Exposed as an event entity. To be used in automations inside Home Assistant + - timing: + - ON for at least 1s + then: + - if: + condition: + lambda: return !id(dial_touched); + then: + - script.execute: + id: play_sound + priority: false + sound_file: !lambda return id(center_button_long_press_sound); + - light.turn_off: voice_assistant_leds + - event.trigger: + id: button_press_event + event_type: "long_press" + # Very important do not remove. Trust me :D + - timing: + # E . + - ON for at most 0.2s + - OFF for 0.5s to 2s + # S ... + - ON for at most 0.2s + - OFF for 0s to 2s + - ON for at most 0.2s + - OFF for 0s to 2s + - ON for at most 0.2s + - OFF for 0.5s to 2s + # P .--. + - ON for at most 0.2s + - OFF for 0s to 2s + - ON for 0.2s to 2s + - OFF for 0s to 2s + - ON for 0.2s to 2s + - OFF for 0s to 2s + - ON for at most 0.2s + - OFF for 0.5s to 2s + # H .... + - ON for at most 0.2s + - OFF for 0s to 2s + - ON for at most 0.2s + - OFF for 0s to 2s + - ON for at most 0.2s + - OFF for 0s to 2s + - ON for at most 0.2s + - OFF for 0.5s to 2s + # O --- + - ON for 0.2s to 2s + - OFF for 0s to 2s + - ON for 0.2s to 2s + - OFF for 0s to 2s + - ON for 0.2s to 2s + - OFF for 0.5s to 2s + # M -- + - ON for 0.2s to 2s + - OFF for 0s to 2s + - ON for 0.2s to 2s + - OFF for 0.5s to 2s + # E . + - ON for at most 0.2s + - OFF for at least 0.5s + then: + - light.turn_on: + brightness: 100% + id: voice_assistant_leds + effect: "Tick" + - script.execute: + id: play_sound + priority: true + sound_file: !lambda return id(easter_egg_tick_sound); + - delay: 4s + - light.turn_off: voice_assistant_leds + - script.execute: + id: play_sound + priority: true + sound_file: !lambda return id(easter_egg_tada_sound); + - light.turn_on: + brightness: 100% + id: voice_assistant_leds + effect: "Rainbow" + # Factory Reset Warning + # . Audible and Visible warning. + - timing: + - ON for at least 10s + then: + - if: + condition: + lambda: return !id(dial_touched); + then: + - light.turn_on: + brightness: 100% + id: voice_assistant_leds + effect: "Factory Reset Coming Up" + - script.execute: + id: play_sound + priority: true + sound_file: !lambda return id(factory_reset_initiated_sound); + - wait_until: + binary_sensor.is_off: center_button + - light.turn_off: voice_assistant_leds + - script.execute: + id: play_sound + priority: true + sound_file: !lambda return id(factory_reset_cancelled_sound); + # Factory Reset + - timing: + - ON for at least 22s + then: + - if: + condition: + lambda: return !id(dial_touched); + then: + - button.press: factory_reset_button + # Hardware mute switch (Side of the device) + - platform: gpio + id: hardware_mute_switch + internal: true + pin: GPIO3 + on_press: + - script.execute: + id: play_sound + priority: false + sound_file: !lambda return id(mute_switch_on_sound); + - switch.template.publish: + id: master_mute_switch + state: ON + on_release: + - script.execute: + id: play_sound + priority: false + sound_file: !lambda return id(mute_switch_off_sound); + - switch.template.publish: + id: master_mute_switch + state: OFF + - platform: gpio + id: jack_plugged + filters: + - delayed_on: 200ms + - delayed_off: 200ms + pin: + number: GPIO17 + on_press: + - lambda: id(jack_plugged_recently) = true; + - script.execute: control_leds + - delay: 1s + - lambda: id(jack_plugged_recently) = false; + - script.execute: control_leds + - script.execute: + id: play_sound + priority: false + sound_file: !lambda return id(jack_connected_sound); + on_release: + - lambda: id(jack_unplugged_recently) = true; + - script.execute: control_leds + - delay: 1s + - lambda: id(jack_unplugged_recently) = false; + - script.execute: control_leds + - script.execute: + id: play_sound + priority: false + sound_file: !lambda return id(jack_disconnected_sound); + +light: + # Hardware LED ring. Not used because remapping needed + - platform: esp32_rmt_led_strip + id: leds_internal + pin: GPIO21 + rmt_channel: 1 + num_leds: 12 + rgb_order: GRB + chipset: WS2812 + default_transition_length: 0ms + power_supply: led_power + + # Voice Assistant LED ring. Remapping of the internal LED. + # This light is not exposed. The device controls it + - platform: partition + id: voice_assistant_leds + internal: true + default_transition_length: 0ms + segments: + - id: leds_internal + from: 7 + to: 11 + - id: leds_internal + from: 0 + to: 6 + effects: + - addressable_lambda: + name: "Waiting for Command" + update_interval: 100ms + lambda: |- + auto light_color = id(led_ring).current_values; + Color color(light_color.get_red() * 255, light_color.get_green() * 255, + light_color.get_blue() * 255); + for (int i = 0; i < 12; i++) { + if (i == id(global_led_animation_index) % 12) { + it[i] = color; + } else if (i == (id(global_led_animation_index) + 11) % 12) { + it[i] = color * 192; + } else if (i == (id(global_led_animation_index) + 10) % 12) { + it[i] = color * 128; + } else if (i == (id(global_led_animation_index) + 6) % 12) { + it[i] = color; + } else if (i == (id(global_led_animation_index) + 5) % 12) { + it[i] = color * 192; + } else if (i == (id(global_led_animation_index) + 4) % 12) { + it[i] = color * 128; + } else { + it[i] = Color::BLACK; + } + } + id(global_led_animation_index) = (id(global_led_animation_index) + 1) % 12; + - addressable_lambda: + name: "Listening For Command" + update_interval: 50ms + lambda: |- + auto light_color = id(led_ring).current_values; + Color color(light_color.get_red() * 255, light_color.get_green() * 255, + light_color.get_blue() * 255); + for (int i = 0; i < 12; i++) { + if (i == id(global_led_animation_index) % 12) { + it[i] = color; + } else if (i == (id(global_led_animation_index) + 11) % 12) { + it[i] = color * 192; + } else if (i == (id(global_led_animation_index) + 10) % 12) { + it[i] = color * 128; + } else if (i == (id(global_led_animation_index) + 6) % 12) { + it[i] = color; + } else if (i == (id(global_led_animation_index) + 5) % 12) { + it[i] = color * 192; + } else if (i == (id(global_led_animation_index) + 4) % 12) { + it[i] = color * 128; + } else { + it[i] = Color::BLACK; + } + } + id(global_led_animation_index) = (id(global_led_animation_index) + 1) % 12; + - addressable_lambda: + name: "Thinking" + update_interval: 10ms + lambda: |- + static uint8_t brightness_step = 0; + static bool brightness_decreasing = true; + static uint8_t brightness_step_number = 10; + if (initial_run) { + brightness_step = 0; + brightness_decreasing = true; + } + auto light_color = id(led_ring).current_values; + Color color(light_color.get_red() * 255, light_color.get_green() * 255, + light_color.get_blue() * 255); + for (int i = 0; i < 12; i++) { + if (i == id(global_led_animation_index) % 12) { + it[i] = color * uint8_t(255/brightness_step_number*(brightness_step_number-brightness_step)); + } else if (i == (id(global_led_animation_index) + 6) % 12) { + it[i] = color * uint8_t(255/brightness_step_number*(brightness_step_number-brightness_step)); + } else { + it[i] = Color::BLACK; + } + } + if (brightness_decreasing) { + brightness_step++; + } else { + brightness_step--; + } + if (brightness_step == 0 || brightness_step == brightness_step_number) { + brightness_decreasing = !brightness_decreasing; + } + - addressable_lambda: + name: "Replying" + update_interval: 50ms + lambda: |- + id(global_led_animation_index) = (12 + id(global_led_animation_index) - 1) % 12; + auto light_color = id(led_ring).current_values; + Color color(light_color.get_red() * 255, light_color.get_green() * 255, + light_color.get_blue() * 255); + for (int i = 0; i < 12; i++) { + if (i == (id(global_led_animation_index)) % 12) { + it[i] = color; + } else if (i == ( id(global_led_animation_index) + 1) % 12) { + it[i] = color * 192; + } else if (i == ( id(global_led_animation_index) + 2) % 12) { + it[i] = color * 128; + } else if (i == ( id(global_led_animation_index) + 6) % 12) { + it[i] = color; + } else if (i == ( id(global_led_animation_index) + 7) % 12) { + it[i] = color * 192; + } else if (i == ( id(global_led_animation_index) + 8) % 12) { + it[i] = color * 128; + } else { + it[i] = Color::BLACK; + } + } + - addressable_lambda: + name: "Muted or Silent" + update_interval: 16ms + lambda: |- + static int8_t index = 0; + Color color(255, 0, 0); + for (int i = 0; i < 12; i++) { + if (i == 3 && id(master_mute_switch).state ) { + it[i] = color; + } else if (i == 9 && id(master_mute_switch).state ) { + it[i] = color; + } else if (i == 6 && (id(nabu_media_player).volume == 0.0f || id(nabu_media_player).is_muted()) ) { + it[i] = color; + } else { + it[i] = Color::BLACK; + } + } + - addressable_lambda: + name: "Volume Display" + update_interval: 50ms + lambda: |- + auto light_color = id(led_ring).current_values; + Color color(light_color.get_red() * 255, light_color.get_green() * 255, + light_color.get_blue() * 255); + auto nb_leds_on = 12.0f * id(nabu_media_player).volume; + for (int i = 0; i < 12; i++) { + if (i < nb_leds_on) { + it[i] = color; + } else { + it[i] = Color::BLACK; + } + } + - addressable_lambda: + name: "Center Button Touched" + update_interval: 16ms + lambda: |- + auto light_color = id(led_ring).current_values; + Color color(light_color.get_red() * 255, light_color.get_green() * 255, + light_color.get_blue() * 255); + for (int i = 0; i < 12; i++) { + it[i] = color; + } + - addressable_twinkle: + name: "Twinkle" + twinkle_probability: 50% + - pulse: + name: "Fast Pulse" + transition_length: 100ms + update_interval: 100ms + min_brightness: 50% + max_brightness: 100% + - addressable_lambda: + name: "Timer Ring" + update_interval: 10ms + lambda: |- + static uint8_t brightness_step = 0; + static bool brightness_decreasing = true; + static uint8_t brightness_step_number = 10; + if (initial_run) { + brightness_step = 0; + brightness_decreasing = true; + } + auto light_color = id(led_ring).current_values; + Color color(light_color.get_red() * 255, light_color.get_green() * 255, + light_color.get_blue() * 255); + for (int i = 0; i < 12; i++) { + it[i] = color * uint8_t(255/brightness_step_number*(brightness_step_number-brightness_step)); + } + if (brightness_decreasing) { + brightness_step++; + } else { + brightness_step--; + } + if (brightness_step == 0 || brightness_step == brightness_step_number) { + brightness_decreasing = !brightness_decreasing; + } + - addressable_rainbow: + name: "Rainbow" + width: 12 + - addressable_lambda: + name: "Tick" + update_interval: 333ms + lambda: |- + static uint8_t index = 0; + Color color(255, 0, 0); + if (initial_run) { + index = 0; + } + for (int i = 0; i < 12; i++) { + if (i <= index ) { + it[i] = Color::BLACK; + } else { + it[i] = color; + } + } + index = (index + 1) % 12; + - addressable_lambda: + name: "Factory Reset Coming Up" + update_interval: 1s + lambda: |- + static uint8_t index = 0; + Color color(255, 0, 0); + if (initial_run) { + index = 0; + } + for (int i = 0; i < 12; i++) { + if (i <= index ) { + it[i] = color; + } else { + it[i] = Color::BLACK; + } + } + index = (index + 1) % 12; + - addressable_lambda: + name: "Jack Plugged" + update_interval: 40ms + lambda: |- + static uint8_t index = 0; + if (initial_run) { + index = 0; + } + auto light_color = id(led_ring).current_values; + Color color(light_color.get_red() * 255, light_color.get_green() * 255, + light_color.get_blue() * 255); + if (index <= 6) { + for (int i = 0; i < 12; i++) { + if (i == index) { + it[i] = color; + } else if (i == (12 - index) % 12) { + it[i] = color; + } else { + it[i] = Color::BLACK; + } + } + } + index = (index + 1); + - addressable_lambda: + name: "Jack Unplugged" + update_interval: 40ms + lambda: |- + static uint8_t index = 0; + if (initial_run) { + index = 0; + } + auto light_color = id(led_ring).current_values; + Color color(light_color.get_red() * 255, light_color.get_green() * 255, + light_color.get_blue() * 255); + if (index <= 6) { + for (int i = 0; i < 12; i++) { + if (i == 6 - index) { + it[i] = color; + } else if (i == (6 + index) % 12) { + it[i] = color; + } else { + it[i] = Color::BLACK; + } + } + } + index = (index + 1); + + # User facing LED ring. Remapping of the internal LEDs. + # Exposed to be used by the user. + - platform: partition + id: led_ring + name: LED Ring + entity_category: config + icon: "mdi:circle-outline" + default_transition_length: 0ms + restore_mode: RESTORE_DEFAULT_OFF + segments: + - id: leds_internal + from: 7 + to: 11 + - id: leds_internal + from: 0 + to: 6 + +power_supply: + - id: led_power + pin: GPIO45 + +sensor: + # The dial. Used to control volume and Hue of the LED ring. + - platform: rotary_encoder + id: dial + pin_a: GPIO16 + pin_b: GPIO18 + resolution: 2 + on_clockwise: + - lambda: id(dial_touched) = true; + - if: + condition: + binary_sensor.is_off: center_button + then: + - script.execute: + id: control_volume + increase_volume: true + else: + - script.execute: + id: control_hue + increase_hue: true + on_anticlockwise: + - lambda: id(dial_touched) = true; + - if: + condition: + binary_sensor.is_off: center_button + then: + - script.execute: + id: control_volume + increase_volume: false + else: + - script.execute: + id: control_hue + increase_hue: false + + # To be removed before launch + - platform: debug + free: + name: "Heap Free" + block: + name: "Max Block Free" + loop_time: + name: "Loop Time" + psram: + name: "PSRAM Free" + +event: + # Event entity exposed to the user to automate on complex center button presses. + # The simple press is not exposed as it is used to control the device itself. + - platform: template + id: button_press_event + name: "Button press" + icon: mdi:button-pointer + device_class: button + event_types: + - double_press + - triple_press + - long_press + +script: + # Master script controlling the LEDs, based on different conditions : initialization in progress, wifi and api connected and voice assistant phase. + # For the sake of simplicity and re-usability, the script calls child scripts defined below. + # This script will be called every time one of these conditions is changing. + - id: control_leds + then: + - if: + condition: + lambda: return !id(init_in_progress); + then: + - if: + condition: + wifi.connected: + then: + - if: + condition: + api.connected: + then: + # One the initial connection is done, this is the master order in which the animationd are displayed. Higer priority takes precendence + # - Center button touched + # - Jack Plugged / Unplugged + # - Dial touched + # - Timer ringing + # - All the active state of the voice assistant + # - Muted / Silent + # - The idle state of the voice assistant + - lambda: | + if (id(center_button).state) { + id(control_leds_center_button_touched).execute(); + } else if (id(jack_plugged_recently)) { + id(control_leds_jack_plugged_recently).execute(); + } else if (id(jack_unplugged_recently)) { + id(control_leds_jack_unplugged_recently).execute(); + } else if (id(dial_touched)) { + id(control_leds_dial_touched).execute(); + } else if (id(timer_ringing).state) { + id(control_leds_timer_ringing).execute(); + } else if (id(voice_assistant_phase) == ${voice_assist_waiting_for_command_phase_id}) { + id(control_leds_voice_assistant_waiting_for_command_phase).execute(); + } else if (id(voice_assistant_phase) == ${voice_assist_listening_for_command_phase_id}) { + id(control_leds_voice_assistant_listening_for_command_phase).execute(); + } else if (id(voice_assistant_phase) == ${voice_assist_thinking_phase_id}) { + id(control_leds_voice_assistant_thinking_phase).execute(); + } else if (id(voice_assistant_phase) == ${voice_assist_replying_phase_id}) { + id(control_leds_voice_assistant_replying_phase).execute(); + } else if (id(voice_assistant_phase) == ${voice_assist_error_phase_id}) { + id(control_leds_voice_assistant_error_phase).execute(); + } else if (id(voice_assistant_phase) == ${voice_assist_not_ready_phase_id}) { + id(control_leds_voice_assistant_not_ready_phase).execute(); + } else if (id(master_mute_switch).state) { + id(control_leds_muted_or_silent).execute(); + } else if (id(nabu_media_player).volume == 0.0f || id(nabu_media_player).is_muted()) { + id(control_leds_muted_or_silent).execute(); + } else if (id(voice_assistant_phase) == ${voice_assist_idle_phase_id}) { + id(control_leds_voice_assistant_idle_phase).execute(); + } + else: + - script.execute: control_leds_no_ha_connection_state + else: + - script.execute: control_leds_no_ha_connection_state + else: + - script.execute: control_leds_init_state + + # Script executed during initialization + # Blue Twinkle if Wifi is not connected + # Green Twinkle if Wifi is connected + - id: control_leds_init_state + then: + - if: + condition: + not: wifi.connected + then: + - light.turn_on: + brightness: 100% + red: 0 + green: 0 + blue: 1.0 + id: voice_assistant_leds + effect: "Twinkle" + else: + - light.turn_on: + brightness: 100% + red: 0 + green: 1.0 + blue: 0 + id: voice_assistant_leds + effect: "Twinkle" + + # Script executed when the device has no connection to Home Assistant + # Red Twinkle (This will be visible during HA updates for example) + - id: control_leds_no_ha_connection_state + then: + - light.turn_on: + brightness: 100% + red: 1 + green: 0 + blue: 0 + id: voice_assistant_leds + effect: "Twinkle" + + # Script executed when the voice assistant is idle (waiting for a wake word) + # Nothing (Either LED ring off or LED ring on if the user decided to turn the user facing LED ring on) + - id: control_leds_voice_assistant_idle_phase + then: + - light.turn_off: voice_assistant_leds + - if: + condition: + light.is_on: led_ring + then: + light.turn_on: led_ring + + # Script executed when the voice assistant is waiting for a command (After the wake word) + # Slow clockwise spin of the LED ring. + - id: control_leds_voice_assistant_waiting_for_command_phase + then: + - light.turn_on: + brightness: 100% + id: voice_assistant_leds + effect: "Waiting for Command" + + # Script executed when the voice assistant is listening to a command + # Fast clockwise spin of the LED ring. + - id: control_leds_voice_assistant_listening_for_command_phase + then: + - light.turn_on: + brightness: 100% + id: voice_assistant_leds + effect: "Listening For Command" + + # Script executed when the voice assistant is thinking to a command + # The spin stops and the 2 LEDs that are currently on and blinking indicating the commend is being processed. + - id: control_leds_voice_assistant_thinking_phase + then: + - light.turn_on: + brightness: 100% + id: voice_assistant_leds + effect: "Thinking" + + # Script executed when the voice assistant is thinking to a command + # Fast anticlockwise spin of the LED ring. + - id: control_leds_voice_assistant_replying_phase + then: + - light.turn_on: + brightness: 100% + id: voice_assistant_leds + effect: "Replying" + + # Script executed when the voice assistant is in error + # Fast Red Pulse + # Note: Today on_end happens too fast after an error. This is barely visible. + - id: control_leds_voice_assistant_error_phase + then: + - light.turn_on: + brightness: 100% + red: 1 + green: 0 + blue: 0 + id: voice_assistant_leds + effect: "Fast Pulse" + + # Script executed when the voice assistant is muted or silent + # The LED next to the 2 microphones turn red / one red LED next to the speaker grill + - id: control_leds_muted_or_silent + then: + - light.turn_on: + brightness: 50% + id: voice_assistant_leds + effect: "Muted or Silent" + + # Script executed when the voice assistant is not ready + - id: control_leds_voice_assistant_not_ready_phase + then: + - light.turn_on: + brightness: 100% + red: 1 + green: 0 + blue: 0 + id: voice_assistant_leds + effect: "Twinkle" + + # Script executed when the dial is touched + # A number of LEDs turn on indicating a visual representation of the volume of the media player entity. + - id: control_leds_dial_touched + then: + - light.turn_on: + brightness: 100% + id: voice_assistant_leds + effect: "Volume Display" + + # Script executed when the jack has just been unplugged + # A ripple effect + - id: control_leds_jack_unplugged_recently + then: + - light.turn_on: + brightness: 100% + id: voice_assistant_leds + effect: "Jack Unplugged" + + # Script executed when the jack has just been plugged + # A ripple effect + - id: control_leds_jack_plugged_recently + then: + - light.turn_on: + brightness: 100% + id: voice_assistant_leds + effect: "Jack Plugged" + + # Script executed when the center button is touched + # The complete LED ring turns on + - id: control_leds_center_button_touched + then: + - light.turn_on: + brightness: 100% + id: voice_assistant_leds + effect: "Center Button Touched" + + # Script executed when the timer is ringing, to control the LEDs + # The LED ring blinks. + - id: control_leds_timer_ringing + then: + - light.turn_on: + brightness: 100% + id: voice_assistant_leds + effect: "Timer Ring" + + # Script executed when the volume is increased/decreased from the dial + - id: control_volume + mode: restart + parameters: + increase_volume: bool # True: Increase volume / False: Decrease volume. + then: + - delay: 16ms + - if: + condition: + lambda: return increase_volume; + then: + - media_player.volume_up: + else: + - media_player.volume_down: + - script.execute: control_leds + - delay: 2s + - lambda: id(dial_touched) = false; + - script.execute: control_leds + + # Script executed when the hue is increased/decreased from the dial + - id: control_hue + mode: restart + parameters: + increase_hue: bool # True: Increase volume / False: Decrease volume. + then: + - delay: 16ms + - lambda: | + auto light_color = id(led_ring).current_values; + int hue = 0; + float saturation = 0; + float value = 0; + rgb_to_hsv( light_color.get_red(), + light_color.get_green(), + light_color.get_blue(), + hue, + saturation, + value); + if (increase_hue) { + hue = (hue + 10) % 360; + } else { + hue = (hue + 350) % 360; + } + if (saturation < 0.05) { + saturation = 1; + } + float red = 0; + float green = 0; + float blue = 0; + hsv_to_rgb( hue, + saturation, + value, + red, + green, + blue); + id(led_ring).make_call().set_rgb(red, green, blue).perform(); + - wait_until: + binary_sensor.is_off: center_button + - lambda: id(dial_touched) = false; + - script.execute: control_leds + + # Script executed when the timer is ringing, to playback sounds. + - id: ring_timer + then: + - while: + condition: + switch.is_on: timer_ringing + then: + - script.execute: + id: play_sound + priority: true + sound_file: !lambda return id(timer_finished_sound); + - wait_until: + lambda: |- + return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; + - wait_until: + not: + lambda: |- + return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; + + # Script executed when we want to play sounds on the device. + - id: play_sound + parameters: + priority: bool + sound_file: "media_player::MediaFile*" + then: + - lambda: |- + if (priority) { + id(nabu_media_player) + ->make_call() + .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP) + .set_announcement(true) + .perform(); + } + if ( (id(nabu_media_player).state != media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING ) || priority) { + id(nabu_media_player) + ->make_call() + .set_announcement(true) + .set_local_media_file(sound_file) + .perform(); + } + +i2s_audio: + - id: i2s_output + # i2s_output data pin is gpio10 + i2s_lrclk_pin: + number: GPIO7 + i2s_bclk_pin: + number: GPIO8 + + - id: i2s_input + # data line is GPIO15 + i2s_lrclk_pin: + number: GPIO14 + i2s_bclk_pin: + number: GPIO13 + +microphone: + - platform: nabu_microphone + i2s_din_pin: GPIO15 + adc_type: external + pdm: false + sample_rate: 16000 + bits_per_sample: 32bit + i2s_mode: secondary + i2s_audio_id: i2s_input + channel_1: + id: asr_mic + amplify: false + channel_2: + id: comm_mic + amplify: false # May be useful to enable, but may also result in worse wake word detection if speaking close to the device + +media_player: + - platform: nabu + id: nabu_media_player + name: Media Player + internal: false + audio_dac: + sample_rate: 48000 + i2s_dout_pin: GPIO10 + bits_per_sample: 32bit + i2s_mode: secondary + i2s_audio_id: i2s_output + on_mute: + - script.execute: control_leds + on_unmute: + - script.execute: control_leds + on_volume: + - script.execute: control_leds + on_announcement: + - nabu.set_ducking: + decibel_reduction: 20 + duration: 0.0s + on_state: + if: + condition: + and: + - switch.is_off: timer_ringing + - not: + voice_assistant.is_running: + - not: + lambda: return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; + then: + - nabu.set_ducking: + decibel_reduction: 0 + duration: 1.0s + files: + - id: center_button_press_sound + file: sounds/center_button_press.flac + - id: center_button_double_press_sound + file: sounds/center_button_double_press.flac + - id: center_button_triple_press_sound + file: sounds/center_button_triple_press.flac + - id: center_button_long_press_sound + file: sounds/center_button_long_press.flac + - id: device_connected_sound + file: sounds/device_connected.mp3 + - id: factory_reset_initiated_sound + file: sounds/factory_reset_initiated.mp3 + - id: factory_reset_cancelled_sound + file: sounds/factory_reset_cancelled.mp3 + - id: jack_connected_sound + file: sounds/jack_connected.flac + - id: jack_disconnected_sound + file: sounds/jack_disconnected.flac + - id: mute_switch_on_sound + file: sounds/mute_switch_on.flac + - id: mute_switch_off_sound + file: sounds/mute_switch_off.flac + - id: timer_finished_sound + file: sounds/timer_finished.flac + - id: wake_word_triggered_sound + file: sounds/wake_word_triggered.flac + - id: easter_egg_tick_sound + file: sounds/easter_egg_tick.mp3 + - id: easter_egg_tada_sound + file: sounds/easter_egg_tada.mp3 + +voice_kit: + reset_pin: GPIO4 + firmware: + url: https://github.com/esphome/voice-kit-xmos-firmware/releases/download/v1.2.0/ffva_v1.2.0_upgrade.bin + version: "1.2.0" + md5: 912b84ea15df4a0081e39ffd9740b884 + +external_components: + - source: + type: git + url: https://github.com/esphome/voice-kit + ref: dev + components: + - aic3204 + - audio_dac + - media_player + - micro_wake_word + - microphone + - nabu + - nabu_microphone + - voice_assistant + - voice_kit + refresh: 0s + +audio_dac: + - platform: aic3204 + +micro_wake_word: + models: + - model: https://github.com/kahrendt/microWakeWord/releases/download/okay_nabu/okay_nabu.json + id: okay_nabu + - model: hey_jarvis + id: hey_jarvis + - model: hey_mycroft + id: hey_mycroft + vad: + microphone: comm_mic + on_wake_word_detected: + # If the wake word is detected when the device is muted (Possible with the software mute switch): Do nothing + - if: + condition: + switch.is_off: master_mute_switch + then: + # If a timer is ringing: Stop it, do not start the voice assistant (We can stop timer from voice!) + - if: + condition: + switch.is_on: timer_ringing + then: + - switch.turn_off: timer_ringing + # Start voice assistant, stop current announcement. + else: + - if: + condition: + lambda: return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; + then: + lambda: |- + id(nabu_media_player) + ->make_call() + .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP) + .set_announcement(true) + .perform(); + - if: + condition: + switch.is_on: wake_sound + then: + - script.execute: + id: play_sound + priority: true + sound_file: !lambda return id(wake_word_triggered_sound); + # - wait_until: + # lambda: |- + # return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; + # - wait_until: + # not: + # lambda: |- + # return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; + - delay: 300ms + - voice_assistant.start: + wake_word: !lambda return wake_word; + +select: + - platform: template + name: "Active wake word" + icon: "mdi:bullhorn" + entity_category: config + options: + - "OK Nabu" + - "Hey Jarvis" + - "Hey Mycroft" + optimistic: true + restore_value: true + on_value: + then: + - lambda: |- + if (x == "OK Nabu") { + id(okay_nabu).enable(); + id(hey_mycroft).disable(); + id(hey_jarvis).disable(); + } else if (x == "Hey Mycroft") { + id(okay_nabu).disable(); + id(hey_mycroft).enable(); + id(hey_jarvis).disable(); + } else if (x == "Hey Jarvis") { + id(okay_nabu).disable(); + id(hey_mycroft).disable(); + id(hey_jarvis).enable(); + } + +voice_assistant: + id: va + microphone: asr_mic + media_player: nabu_media_player + use_wake_word: false + noise_suppression_level: 0 + auto_gain: 0 dbfs + volume_multiplier: 1 + on_client_connected: + - if: + condition: + lambda: return id(init_in_progress); + then: + - script.execute: + id: play_sound + priority: false + sound_file: !lambda return id(device_connected_sound); + - lambda: id(init_in_progress) = false; + - micro_wake_word.start: + - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; + - script.execute: control_leds + on_client_disconnected: + - voice_assistant.stop: + - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id}; + - script.execute: control_leds + on_error: + - if: + condition: + lambda: return !id(init_in_progress); + then: + - lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id}; + - script.execute: control_leds + - delay: 1s + - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; + - script.execute: control_leds + # When the voice assistant starts: Play a wake up sound, duck audio. + on_start: + - nabu.set_ducking: + decibel_reduction: 20 # Number of dB quieter; higher implies more quiet, 0 implies full volume + duration: 0.0s # The duration of the transition (default is 0) + on_listening: + - lambda: id(voice_assistant_phase) = ${voice_assist_waiting_for_command_phase_id}; + - script.execute: control_leds + on_stt_vad_start: + - lambda: id(voice_assistant_phase) = ${voice_assist_listening_for_command_phase_id}; + - script.execute: control_leds + on_stt_vad_end: + - lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id}; + - script.execute: control_leds + on_tts_start: + - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; + - script.execute: control_leds + # When the voice assistant ends: Stop ducking audio. + on_end: + - wait_until: + not: + voice_assistant.is_running: + - nabu.set_ducking: + decibel_reduction: 0 # 0 dB means no reduction + duration: 1.0s + - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; + - script.execute: control_leds + on_timer_finished: + - switch.turn_on: timer_ringing + +button: + # TODO: Remove before launch, added for testing/development + - platform: restart + name: "Restart" + entity_category: diagnostic + # TODO: make it internal before launch + - platform: factory_reset + id: factory_reset_button + name: "Factory Reset" + entity_category: diagnostic + # TODO: Remove before launch, added for testing/development + - platform: template + name: "timer beep" + entity_category: diagnostic + on_press: + - script.execute: + id: play_sound + priority: true + sound_file: !lambda return id(timer_finished_sound); + +debug: + update_interval: 5s diff --git a/static/index.html b/static/index.html index dd0c05c..4ec23ee 100644 --- a/static/index.html +++ b/static/index.html @@ -1,12 +1,12 @@ - ESPHome Voice Kit + Home Assistant Voice PE + manifest="https://firmware.esphome.io/home-assistant-voice-pe/home-assistant-voice/manifest.json"> diff --git a/voice-kit.yaml b/voice-kit.yaml index ddf5e02..eaf2d07 100644 --- a/voice-kit.yaml +++ b/voice-kit.yaml @@ -1,1476 +1,4 @@ -substitutions: - # Phases of the Voice Assistant - # The voice assistant is ready to be triggered by a wake word - voice_assist_idle_phase_id: '1' - # The voice assistant is waiting for a voice command (after being triggered by the wake word) - voice_assist_waiting_for_command_phase_id: '2' - # The voice assistant is listening for a voice command - voice_assist_listening_for_command_phase_id: '3' - # The voice assistant is currently processing the command - voice_assist_thinking_phase_id: '4' - # The voice assistant is replying to the command - voice_assist_replying_phase_id: '5' - # The voice assistant is not ready - voice_assist_not_ready_phase_id: '10' - # The voice assistant encountered an error - voice_assist_error_phase_id: '11' +# File moved to home-assistant-voice.yaml +# This file is kept for older adopted configs -esphome: - name: esphome-voice-kit - friendly_name: ESPHome Voice Kit - name_add_mac_suffix: true - min_version: 2024.9.0 - platformio_options: - board_build.flash_mode: dio - on_boot: - priority: 375 - then: - # Run the script to refresh the LED status - - script.execute: control_leds - - delay: 1s - # TODO: make it internal before launch - - switch.turn_on: internal_speaker_amp - # If the hardware switch is ON, force the software switch to be ON too. This covers the case where the Mute hardware switch is operated when the device is turned off - - if: - condition: - binary_sensor.is_on: hardware_mute_switch - then: - - switch.template.publish: - id: master_mute_switch - state: ON - # If after 10 minutes, 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 - - delay: 10min - - if: - condition: - lambda: return id(init_in_progress); - then: - - lambda: id(init_in_progress) = false; - - script.execute: control_leds - -esp32: - board: esp32-s3-devkitc-1 - variant: esp32s3 - flash_size: 8MB - framework: - type: esp-idf - version: recommended - sdkconfig_options: - CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y" - CONFIG_ESP32S3_DATA_CACHE_64KB: "y" - CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y" - CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB: "y" - CONFIG_ESP32_S3_BOX_BOARD: "y" - CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY: "y" - - CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP: "y" - - # Settings based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702 - CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM: "16" - CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM: "512" - CONFIG_ESP32_WIFI_STATIC_TX_BUFFER: "y" - CONFIG_ESP32_WIFI_TX_BUFFER_TYPE: "0" - CONFIG_ESP32_WIFI_STATIC_TX_BUFFER_NUM: "8" - CONFIG_ESP32_WIFI_CACHE_TX_BUFFER_NUM: "32" - CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED: "y" - CONFIG_ESP32_WIFI_TX_BA_WIN: "16" - CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED: "y" - CONFIG_ESP32_WIFI_RX_BA_WIN: "32" - CONFIG_LWIP_MAX_ACTIVE_TCP: "16" - CONFIG_LWIP_MAX_LISTENING_TCP: "16" - CONFIG_TCP_MAXRTX: "12" - CONFIG_TCP_SYNMAXRTX: "6" - CONFIG_TCP_MSS: "1436" - CONFIG_TCP_MSL: "60000" - CONFIG_TCP_SND_BUF_DEFAULT: "65535" - CONFIG_TCP_WND_DEFAULT: "65535" # Adjusted from linked settings to avoid compilation error - CONFIG_TCP_RECVMBOX_SIZE: "512" - CONFIG_TCP_QUEUE_OOSEQ: "y" - CONFIG_TCP_OVERSIZE_MSS: "y" - CONFIG_LWIP_WND_SCALE: "y" - CONFIG_TCP_RCV_SCALE: "3" - CONFIG_LWIP_TCPIP_RECVMBOX_SIZE: "512" - - CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST: "y" - CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY: "y" - -wifi: - ap: - on_connect: - - script.execute: control_leds - on_disconnect: - - script.execute: control_leds - -logger: - level: DEBUG - logs: - sensor: WARN # avoids logging debug sensor updates - -api: - on_client_connected: - - script.execute: control_leds - on_client_disconnected: - - script.execute: control_leds - -ota: - - platform: esphome - id: ota_esphome - -i2c: - sda: GPIO5 - scl: GPIO6 - frequency: 400kHz - -psram: - mode: octal - speed: 80MHz - -globals: - # Global index for our LEDs. So that switching between different animation does not lead to unwanted effects. - - id: global_led_animation_index - type: int - restore_value: no - initial_value: '0' - # Global initialization variable. Initialized to true and set to false once everything is connected. Only used to have a smooth "plugging" experience - - id: init_in_progress - type: bool - restore_value: no - initial_value: 'true' - # Global variable tracking the phase of the voice assistant (defined above). Initialized to not_ready - - id: voice_assistant_phase - type: int - restore_value: no - initial_value: ${voice_assist_not_ready_phase_id} - # Global variable tracking if the dial was recently touched. - - id: dial_touched - type: bool - restore_value: no - initial_value: 'false' - # Global variable tracking if the jack has been plugged touched. - - id: jack_plugged_recently - type: bool - restore_value: no - initial_value: 'false' - # Global variable tracking if the jack has been unplugged touched. - - id: jack_unplugged_recently - type: bool - restore_value: no - initial_value: 'false' - -switch: - # This is the master mute switch. It is exposed to Home Assistant. The user can only turn it on and off if the hardware switch is off. (The hardware switch overrides the software one) - - platform: template - id: master_mute_switch - restore_mode: RESTORE_DEFAULT_OFF - icon: "mdi:microphone-off" - name: Mute - entity_category: config - turn_on_action: - - if: - condition: - binary_sensor.is_off: hardware_mute_switch - then: - - switch.template.publish: - id: master_mute_switch - state: ON - turn_off_action: - - if: - condition: - binary_sensor.is_off: hardware_mute_switch - then: - - switch.template.publish: - id: master_mute_switch - state: OFF - on_turn_on: - - script.execute: control_leds - on_turn_off: - - script.execute: control_leds - # Wake Word Sound Switch. - - platform: template - id: wake_sound - name: Wake sound - icon: "mdi:bullhorn" - entity_category: config - optimistic: true - restore_mode: RESTORE_DEFAULT_ON - # Internal switch to track when a timer is ringing on the device. - - platform: template - id: timer_ringing - optimistic: true - internal: true - restore_mode: ALWAYS_OFF - on_turn_off: - - delay: 200ms - - nabu.set_ducking: - decibel_reduction: 0 - duration: 1.0s - - script.execute: control_leds - on_turn_on: - - nabu.set_ducking: - decibel_reduction: 20 - duration: 0.0s - - script.execute: ring_timer - - script.execute: control_leds - - delay: 15min - - switch.turn_off: timer_ringing - - platform: gpio - pin: GPIO47 - id: internal_speaker_amp - name: "Internal speaker amp" - entity_category: config - restore_mode: ALWAYS_OFF - internal: true -binary_sensor: - # Center Button. Used for many things (See on_multi_click) - - platform: gpio - id: center_button - pin: - number: GPIO0 - inverted: true - on_press: - - script.execute: control_leds - on_release: - - script.execute: control_leds - on_multi_click: - # Simple Click: - # - Abort "things" in order - # - Timer - # - Announcements - # - Voice Assistant Pipeline run - # - Music - # - Starts the voice assistant if it is not yet running and if the device is not muted. - - timing: - - ON for at most 1s - - OFF for at least 0.25s - then: - - if: - condition: - switch.is_on: timer_ringing - then: - - switch.turn_off: timer_ringing - else: - - if: - condition: - lambda: return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; - then: - - lambda: | - id(nabu_media_player) - ->make_call() - .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP) - .set_announcement(true) - .perform(); - else: - - if: - condition: - voice_assistant.is_running: - then: - - voice_assistant.stop: - else: - - if: - condition: - media_player.is_playing: - then: - - media_player.pause: - else: - - if: - condition: - and: - - switch.is_off: master_mute_switch - - not: - voice_assistant.is_running - then: - - script.execute: - id: play_sound - priority: true - sound_file: !lambda return id(center_button_press_sound); - - wait_until: - lambda: |- - return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; - - wait_until: - not: - lambda: |- - return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; - - voice_assistant.start: - # Double Click - # . Exposed as an event entity. To be used in automations inside Home Assistant - - timing: - - ON for at most 1s - - OFF for at most 0.25s - - ON for at most 1s - - OFF for at least 0.25s - then: - - script.execute: - id: play_sound - priority: false - sound_file: !lambda return id(center_button_double_press_sound); - - event.trigger: - id: button_press_event - event_type: "double_press" - # Triple Click - # . Exposed as an event entity. To be used in automations inside Home Assistant - - timing: - - ON for at most 1s - - OFF for at most 0.25s - - ON for at most 1s - - OFF for at most 0.25s - - ON for at most 1s - - OFF for at least 0.25s - then: - - script.execute: - id: play_sound - priority: false - sound_file: !lambda return id(center_button_triple_press_sound); - - event.trigger: - id: button_press_event - event_type: "triple_press" - # Long Press - # . Exposed as an event entity. To be used in automations inside Home Assistant - - timing: - - ON for at least 1s - then: - - if: - condition: - lambda: return !id(dial_touched); - then: - - script.execute: - id: play_sound - priority: false - sound_file: !lambda return id(center_button_long_press_sound); - - light.turn_off: voice_assistant_leds - - event.trigger: - id: button_press_event - event_type: "long_press" - # Very important do not remove. Trust me :D - - timing: - # E . - - ON for at most 0.2s - - OFF for 0.5s to 2s - # S ... - - ON for at most 0.2s - - OFF for 0s to 2s - - ON for at most 0.2s - - OFF for 0s to 2s - - ON for at most 0.2s - - OFF for 0.5s to 2s - # P .--. - - ON for at most 0.2s - - OFF for 0s to 2s - - ON for 0.2s to 2s - - OFF for 0s to 2s - - ON for 0.2s to 2s - - OFF for 0s to 2s - - ON for at most 0.2s - - OFF for 0.5s to 2s - # H .... - - ON for at most 0.2s - - OFF for 0s to 2s - - ON for at most 0.2s - - OFF for 0s to 2s - - ON for at most 0.2s - - OFF for 0s to 2s - - ON for at most 0.2s - - OFF for 0.5s to 2s - # O --- - - ON for 0.2s to 2s - - OFF for 0s to 2s - - ON for 0.2s to 2s - - OFF for 0s to 2s - - ON for 0.2s to 2s - - OFF for 0.5s to 2s - # M -- - - ON for 0.2s to 2s - - OFF for 0s to 2s - - ON for 0.2s to 2s - - OFF for 0.5s to 2s - # E . - - ON for at most 0.2s - - OFF for at least 0.5s - then: - - light.turn_on: - brightness: 100% - id: voice_assistant_leds - effect: "Tick" - - script.execute: - id: play_sound - priority: true - sound_file: !lambda return id(easter_egg_tick_sound); - - delay: 4s - - light.turn_off: voice_assistant_leds - - script.execute: - id: play_sound - priority: true - sound_file: !lambda return id(easter_egg_tada_sound); - - light.turn_on: - brightness: 100% - id: voice_assistant_leds - effect: "Rainbow" - # Factory Reset Warning - # . Audible and Visible warning. - - timing: - - ON for at least 10s - then: - - if: - condition: - lambda: return !id(dial_touched); - then: - - light.turn_on: - brightness: 100% - id: voice_assistant_leds - effect: "Factory Reset Coming Up" - - script.execute: - id: play_sound - priority: true - sound_file: !lambda return id(factory_reset_initiated_sound); - - wait_until: - binary_sensor.is_off: center_button - - light.turn_off: voice_assistant_leds - - script.execute: - id: play_sound - priority: true - sound_file: !lambda return id(factory_reset_cancelled_sound); - # Factory Reset - - timing: - - ON for at least 22s - then: - - if: - condition: - lambda: return !id(dial_touched); - then: - - button.press: factory_reset_button - # Hardware mute switch (Side of the device) - - platform: gpio - id: hardware_mute_switch - internal: true - pin: GPIO3 - on_press: - - script.execute: - id: play_sound - priority: false - sound_file: !lambda return id(mute_switch_on_sound); - - switch.template.publish: - id: master_mute_switch - state: ON - on_release: - - script.execute: - id: play_sound - priority: false - sound_file: !lambda return id(mute_switch_off_sound); - - switch.template.publish: - id: master_mute_switch - state: OFF - - platform: gpio - id: jack_plugged - filters: - - delayed_on: 200ms - - delayed_off: 200ms - pin: - number: GPIO17 - on_press: - - lambda: id(jack_plugged_recently) = true; - - script.execute: control_leds - - delay: 1s - - lambda: id(jack_plugged_recently) = false; - - script.execute: control_leds - - script.execute: - id: play_sound - priority: false - sound_file: !lambda return id(jack_connected_sound); - on_release: - - lambda: id(jack_unplugged_recently) = true; - - script.execute: control_leds - - delay: 1s - - lambda: id(jack_unplugged_recently) = false; - - script.execute: control_leds - - script.execute: - id: play_sound - priority: false - sound_file: !lambda return id(jack_disconnected_sound); - -light: - # Hardware LED ring. Not used because remapping needed - - platform: esp32_rmt_led_strip - id: leds_internal - pin: GPIO21 - rmt_channel: 1 - num_leds: 12 - rgb_order: GRB - chipset: WS2812 - default_transition_length: 0ms - power_supply: led_power - - # Voice Assistant LED ring. Remapping of the internal LED. - # This light is not exposed. The device controls it - - platform: partition - id: voice_assistant_leds - internal: true - default_transition_length: 0ms - segments: - - id: leds_internal - from: 7 - to: 11 - - id: leds_internal - from: 0 - to: 6 - effects: - - addressable_lambda: - name: "Waiting for Command" - update_interval: 100ms - lambda: |- - auto light_color = id(led_ring).current_values; - Color color(light_color.get_red() * 255, light_color.get_green() * 255, - light_color.get_blue() * 255); - for (int i = 0; i < 12; i++) { - if (i == id(global_led_animation_index) % 12) { - it[i] = color; - } else if (i == (id(global_led_animation_index) + 11) % 12) { - it[i] = color * 192; - } else if (i == (id(global_led_animation_index) + 10) % 12) { - it[i] = color * 128; - } else if (i == (id(global_led_animation_index) + 6) % 12) { - it[i] = color; - } else if (i == (id(global_led_animation_index) + 5) % 12) { - it[i] = color * 192; - } else if (i == (id(global_led_animation_index) + 4) % 12) { - it[i] = color * 128; - } else { - it[i] = Color::BLACK; - } - } - id(global_led_animation_index) = (id(global_led_animation_index) + 1) % 12; - - addressable_lambda: - name: "Listening For Command" - update_interval: 50ms - lambda: |- - auto light_color = id(led_ring).current_values; - Color color(light_color.get_red() * 255, light_color.get_green() * 255, - light_color.get_blue() * 255); - for (int i = 0; i < 12; i++) { - if (i == id(global_led_animation_index) % 12) { - it[i] = color; - } else if (i == (id(global_led_animation_index) + 11) % 12) { - it[i] = color * 192; - } else if (i == (id(global_led_animation_index) + 10) % 12) { - it[i] = color * 128; - } else if (i == (id(global_led_animation_index) + 6) % 12) { - it[i] = color; - } else if (i == (id(global_led_animation_index) + 5) % 12) { - it[i] = color * 192; - } else if (i == (id(global_led_animation_index) + 4) % 12) { - it[i] = color * 128; - } else { - it[i] = Color::BLACK; - } - } - id(global_led_animation_index) = (id(global_led_animation_index) + 1) % 12; - - addressable_lambda: - name: "Thinking" - update_interval: 10ms - lambda: |- - static uint8_t brightness_step = 0; - static bool brightness_decreasing = true; - static uint8_t brightness_step_number = 10; - if (initial_run) { - brightness_step = 0; - brightness_decreasing = true; - } - auto light_color = id(led_ring).current_values; - Color color(light_color.get_red() * 255, light_color.get_green() * 255, - light_color.get_blue() * 255); - for (int i = 0; i < 12; i++) { - if (i == id(global_led_animation_index) % 12) { - it[i] = color * uint8_t(255/brightness_step_number*(brightness_step_number-brightness_step)); - } else if (i == (id(global_led_animation_index) + 6) % 12) { - it[i] = color * uint8_t(255/brightness_step_number*(brightness_step_number-brightness_step)); - } else { - it[i] = Color::BLACK; - } - } - if (brightness_decreasing) { - brightness_step++; - } else { - brightness_step--; - } - if (brightness_step == 0 || brightness_step == brightness_step_number) { - brightness_decreasing = !brightness_decreasing; - } - - addressable_lambda: - name: "Replying" - update_interval: 50ms - lambda: |- - id(global_led_animation_index) = (12 + id(global_led_animation_index) - 1) % 12; - auto light_color = id(led_ring).current_values; - Color color(light_color.get_red() * 255, light_color.get_green() * 255, - light_color.get_blue() * 255); - for (int i = 0; i < 12; i++) { - if (i == (id(global_led_animation_index)) % 12) { - it[i] = color; - } else if (i == ( id(global_led_animation_index) + 1) % 12) { - it[i] = color * 192; - } else if (i == ( id(global_led_animation_index) + 2) % 12) { - it[i] = color * 128; - } else if (i == ( id(global_led_animation_index) + 6) % 12) { - it[i] = color; - } else if (i == ( id(global_led_animation_index) + 7) % 12) { - it[i] = color * 192; - } else if (i == ( id(global_led_animation_index) + 8) % 12) { - it[i] = color * 128; - } else { - it[i] = Color::BLACK; - } - } - - addressable_lambda: - name: "Muted or Silent" - update_interval: 16ms - lambda: |- - static int8_t index = 0; - Color color(255, 0, 0); - for (int i = 0; i < 12; i++) { - if (i == 3 && id(master_mute_switch).state ) { - it[i] = color; - } else if (i == 9 && id(master_mute_switch).state ) { - it[i] = color; - } else if (i == 6 && (id(nabu_media_player).volume == 0.0f || id(nabu_media_player).is_muted()) ) { - it[i] = color; - } else { - it[i] = Color::BLACK; - } - } - - addressable_lambda: - name: "Volume Display" - update_interval: 50ms - lambda: |- - auto light_color = id(led_ring).current_values; - Color color(light_color.get_red() * 255, light_color.get_green() * 255, - light_color.get_blue() * 255); - auto nb_leds_on = 12.0f * id(nabu_media_player).volume; - for (int i = 0; i < 12; i++) { - if (i < nb_leds_on) { - it[i] = color; - } else { - it[i] = Color::BLACK; - } - } - - addressable_lambda: - name: "Center Button Touched" - update_interval: 16ms - lambda: |- - auto light_color = id(led_ring).current_values; - Color color(light_color.get_red() * 255, light_color.get_green() * 255, - light_color.get_blue() * 255); - for (int i = 0; i < 12; i++) { - it[i] = color; - } - - addressable_twinkle: - name: "Twinkle" - twinkle_probability: 50% - - pulse: - name: "Fast Pulse" - transition_length: 100ms - update_interval: 100ms - min_brightness: 50% - max_brightness: 100% - - addressable_lambda: - name: "Timer Ring" - update_interval: 10ms - lambda: |- - static uint8_t brightness_step = 0; - static bool brightness_decreasing = true; - static uint8_t brightness_step_number = 10; - if (initial_run) { - brightness_step = 0; - brightness_decreasing = true; - } - auto light_color = id(led_ring).current_values; - Color color(light_color.get_red() * 255, light_color.get_green() * 255, - light_color.get_blue() * 255); - for (int i = 0; i < 12; i++) { - it[i] = color * uint8_t(255/brightness_step_number*(brightness_step_number-brightness_step)); - } - if (brightness_decreasing) { - brightness_step++; - } else { - brightness_step--; - } - if (brightness_step == 0 || brightness_step == brightness_step_number) { - brightness_decreasing = !brightness_decreasing; - } - - addressable_rainbow: - name: "Rainbow" - width: 12 - - addressable_lambda: - name: "Tick" - update_interval: 333ms - lambda: |- - static uint8_t index = 0; - Color color(255, 0, 0); - if (initial_run) { - index = 0; - } - for (int i = 0; i < 12; i++) { - if (i <= index ) { - it[i] = Color::BLACK; - } else { - it[i] = color; - } - } - index = (index + 1) % 12; - - addressable_lambda: - name: "Factory Reset Coming Up" - update_interval: 1s - lambda: |- - static uint8_t index = 0; - Color color(255, 0, 0); - if (initial_run) { - index = 0; - } - for (int i = 0; i < 12; i++) { - if (i <= index ) { - it[i] = color; - } else { - it[i] = Color::BLACK; - } - } - index = (index + 1) % 12; - - addressable_lambda: - name: "Jack Plugged" - update_interval: 40ms - lambda: |- - static uint8_t index = 0; - if (initial_run) { - index = 0; - } - auto light_color = id(led_ring).current_values; - Color color(light_color.get_red() * 255, light_color.get_green() * 255, - light_color.get_blue() * 255); - if (index <= 6) { - for (int i = 0; i < 12; i++) { - if (i == index) { - it[i] = color; - } else if (i == (12 - index) % 12) { - it[i] = color; - } else { - it[i] = Color::BLACK; - } - } - } - index = (index + 1); - - addressable_lambda: - name: "Jack Unplugged" - update_interval: 40ms - lambda: |- - static uint8_t index = 0; - if (initial_run) { - index = 0; - } - auto light_color = id(led_ring).current_values; - Color color(light_color.get_red() * 255, light_color.get_green() * 255, - light_color.get_blue() * 255); - if (index <= 6) { - for (int i = 0; i < 12; i++) { - if (i == 6 - index) { - it[i] = color; - } else if (i == (6 + index) % 12) { - it[i] = color; - } else { - it[i] = Color::BLACK; - } - } - } - index = (index + 1); - - # User facing LED ring. Remapping of the internal LEDs. - # Exposed to be used by the user. - - platform: partition - id: led_ring - name: LED Ring - entity_category: config - icon: "mdi:circle-outline" - default_transition_length: 0ms - restore_mode: RESTORE_DEFAULT_OFF - segments: - - id: leds_internal - from: 7 - to: 11 - - id: leds_internal - from: 0 - to: 6 - -power_supply: - - id: led_power - pin: GPIO45 - -sensor: - # The dial. Used to control volume and Hue of the LED ring. - - platform: rotary_encoder - id: dial - pin_a: GPIO16 - pin_b: GPIO18 - resolution: 2 - on_clockwise: - - lambda: id(dial_touched) = true; - - if: - condition: - binary_sensor.is_off: center_button - then: - - script.execute: - id: control_volume - increase_volume: true - else: - - script.execute: - id: control_hue - increase_hue: true - on_anticlockwise: - - lambda: id(dial_touched) = true; - - if: - condition: - binary_sensor.is_off: center_button - then: - - script.execute: - id: control_volume - increase_volume: false - else: - - script.execute: - id: control_hue - increase_hue: false - - # To be removed before launch - - platform: debug - free: - name: "Heap Free" - block: - name: "Max Block Free" - loop_time: - name: "Loop Time" - psram: - name: "PSRAM Free" - -event: - # Event entity exposed to the user to automate on complex center button presses. - # The simple press is not exposed as it is used to control the device itself. - - platform: template - id: button_press_event - name: "Button press" - icon: mdi:button-pointer - device_class: button - event_types: - - double_press - - triple_press - - long_press - -script: - # Master script controlling the LEDs, based on different conditions : initialization in progress, wifi and api connected and voice assistant phase. - # For the sake of simplicity and re-usability, the script calls child scripts defined below. - # This script will be called every time one of these conditions is changing. - - id: control_leds - then: - - if: - condition: - lambda: return !id(init_in_progress); - then: - - if: - condition: - wifi.connected: - then: - - if: - condition: - api.connected: - then: - # One the initial connection is done, this is the master order in which the animationd are displayed. Higer priority takes precendence - # - Center button touched - # - Jack Plugged / Unplugged - # - Dial touched - # - Timer ringing - # - All the active state of the voice assistant - # - Muted / Silent - # - The idle state of the voice assistant - - lambda: | - if (id(center_button).state) { - id(control_leds_center_button_touched).execute(); - } else if (id(jack_plugged_recently)) { - id(control_leds_jack_plugged_recently).execute(); - } else if (id(jack_unplugged_recently)) { - id(control_leds_jack_unplugged_recently).execute(); - } else if (id(dial_touched)) { - id(control_leds_dial_touched).execute(); - } else if (id(timer_ringing).state) { - id(control_leds_timer_ringing).execute(); - } else if (id(voice_assistant_phase) == ${voice_assist_waiting_for_command_phase_id}) { - id(control_leds_voice_assistant_waiting_for_command_phase).execute(); - } else if (id(voice_assistant_phase) == ${voice_assist_listening_for_command_phase_id}) { - id(control_leds_voice_assistant_listening_for_command_phase).execute(); - } else if (id(voice_assistant_phase) == ${voice_assist_thinking_phase_id}) { - id(control_leds_voice_assistant_thinking_phase).execute(); - } else if (id(voice_assistant_phase) == ${voice_assist_replying_phase_id}) { - id(control_leds_voice_assistant_replying_phase).execute(); - } else if (id(voice_assistant_phase) == ${voice_assist_error_phase_id}) { - id(control_leds_voice_assistant_error_phase).execute(); - } else if (id(voice_assistant_phase) == ${voice_assist_not_ready_phase_id}) { - id(control_leds_voice_assistant_not_ready_phase).execute(); - } else if (id(master_mute_switch).state) { - id(control_leds_muted_or_silent).execute(); - } else if (id(nabu_media_player).volume == 0.0f || id(nabu_media_player).is_muted()) { - id(control_leds_muted_or_silent).execute(); - } else if (id(voice_assistant_phase) == ${voice_assist_idle_phase_id}) { - id(control_leds_voice_assistant_idle_phase).execute(); - } - else: - - script.execute: control_leds_no_ha_connection_state - else: - - script.execute: control_leds_no_ha_connection_state - else: - - script.execute: control_leds_init_state - - # Script executed during initialization - # Blue Twinkle if Wifi is not connected - # Green Twinkle if Wifi is connected - - id: control_leds_init_state - then: - - if: - condition: - not: wifi.connected - then: - - light.turn_on: - brightness: 100% - red: 0 - green: 0 - blue: 1.0 - id: voice_assistant_leds - effect: "Twinkle" - else: - - light.turn_on: - brightness: 100% - red: 0 - green: 1.0 - blue: 0 - id: voice_assistant_leds - effect: "Twinkle" - - # Script executed when the device has no connection to Home Assistant - # Red Twinkle (This will be visible during HA updates for example) - - id: control_leds_no_ha_connection_state - then: - - light.turn_on: - brightness: 100% - red: 1 - green: 0 - blue: 0 - id: voice_assistant_leds - effect: "Twinkle" - - # Script executed when the voice assistant is idle (waiting for a wake word) - # Nothing (Either LED ring off or LED ring on if the user decided to turn the user facing LED ring on) - - id: control_leds_voice_assistant_idle_phase - then: - - light.turn_off: voice_assistant_leds - - if: - condition: - light.is_on: led_ring - then: - light.turn_on: led_ring - - # Script executed when the voice assistant is waiting for a command (After the wake word) - # Slow clockwise spin of the LED ring. - - id: control_leds_voice_assistant_waiting_for_command_phase - then: - - light.turn_on: - brightness: 100% - id: voice_assistant_leds - effect: "Waiting for Command" - - # Script executed when the voice assistant is listening to a command - # Fast clockwise spin of the LED ring. - - id: control_leds_voice_assistant_listening_for_command_phase - then: - - light.turn_on: - brightness: 100% - id: voice_assistant_leds - effect: "Listening For Command" - - # Script executed when the voice assistant is thinking to a command - # The spin stops and the 2 LEDs that are currently on and blinking indicating the commend is being processed. - - id: control_leds_voice_assistant_thinking_phase - then: - - light.turn_on: - brightness: 100% - id: voice_assistant_leds - effect: "Thinking" - - # Script executed when the voice assistant is thinking to a command - # Fast anticlockwise spin of the LED ring. - - id: control_leds_voice_assistant_replying_phase - then: - - light.turn_on: - brightness: 100% - id: voice_assistant_leds - effect: "Replying" - - # Script executed when the voice assistant is in error - # Fast Red Pulse - # Note: Today on_end happens too fast after an error. This is barely visible. - - id: control_leds_voice_assistant_error_phase - then: - - light.turn_on: - brightness: 100% - red: 1 - green: 0 - blue: 0 - id: voice_assistant_leds - effect: "Fast Pulse" - - # Script executed when the voice assistant is muted or silent - # The LED next to the 2 microphones turn red / one red LED next to the speaker grill - - id: control_leds_muted_or_silent - then: - - light.turn_on: - brightness: 50% - id: voice_assistant_leds - effect: "Muted or Silent" - - # Script executed when the voice assistant is not ready - - id: control_leds_voice_assistant_not_ready_phase - then: - - light.turn_on: - brightness: 100% - red: 1 - green: 0 - blue: 0 - id: voice_assistant_leds - effect: "Twinkle" - - # Script executed when the dial is touched - # A number of LEDs turn on indicating a visual representation of the volume of the media player entity. - - id: control_leds_dial_touched - then: - - light.turn_on: - brightness: 100% - id: voice_assistant_leds - effect: "Volume Display" - - # Script executed when the jack has just been unplugged - # A ripple effect - - id: control_leds_jack_unplugged_recently - then: - - light.turn_on: - brightness: 100% - id: voice_assistant_leds - effect: "Jack Unplugged" - - # Script executed when the jack has just been plugged - # A ripple effect - - id: control_leds_jack_plugged_recently - then: - - light.turn_on: - brightness: 100% - id: voice_assistant_leds - effect: "Jack Plugged" - - # Script executed when the center button is touched - # The complete LED ring turns on - - id: control_leds_center_button_touched - then: - - light.turn_on: - brightness: 100% - id: voice_assistant_leds - effect: "Center Button Touched" - - # Script executed when the timer is ringing, to control the LEDs - # The LED ring blinks. - - id: control_leds_timer_ringing - then: - - light.turn_on: - brightness: 100% - id: voice_assistant_leds - effect: "Timer Ring" - - # Script executed when the volume is increased/decreased from the dial - - id: control_volume - mode: restart - parameters: - increase_volume: bool # True: Increase volume / False: Decrease volume. - then: - - delay: 16ms - - if: - condition: - lambda: return increase_volume; - then: - - media_player.volume_up: - else: - - media_player.volume_down: - - script.execute: control_leds - - delay: 2s - - lambda: id(dial_touched) = false; - - script.execute: control_leds - - # Script executed when the hue is increased/decreased from the dial - - id: control_hue - mode: restart - parameters: - increase_hue: bool # True: Increase volume / False: Decrease volume. - then: - - delay: 16ms - - lambda: | - auto light_color = id(led_ring).current_values; - int hue = 0; - float saturation = 0; - float value = 0; - rgb_to_hsv( light_color.get_red(), - light_color.get_green(), - light_color.get_blue(), - hue, - saturation, - value); - if (increase_hue) { - hue = (hue + 10) % 360; - } else { - hue = (hue + 350) % 360; - } - if (saturation < 0.05) { - saturation = 1; - } - float red = 0; - float green = 0; - float blue = 0; - hsv_to_rgb( hue, - saturation, - value, - red, - green, - blue); - id(led_ring).make_call().set_rgb(red, green, blue).perform(); - - wait_until: - binary_sensor.is_off: center_button - - lambda: id(dial_touched) = false; - - script.execute: control_leds - - # Script executed when the timer is ringing, to playback sounds. - - id: ring_timer - then: - - while: - condition: - switch.is_on: timer_ringing - then: - - script.execute: - id: play_sound - priority: true - sound_file: !lambda return id(timer_finished_sound); - - wait_until: - lambda: |- - return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; - - wait_until: - not: - lambda: |- - return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; - - # Script executed when we want to play sounds on the device. - - id: play_sound - parameters: - priority: bool - sound_file: "media_player::MediaFile*" - then: - - lambda: |- - if (priority) { - id(nabu_media_player) - ->make_call() - .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP) - .set_announcement(true) - .perform(); - } - if ( (id(nabu_media_player).state != media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING ) || priority) { - id(nabu_media_player) - ->make_call() - .set_announcement(true) - .set_local_media_file(sound_file) - .perform(); - } - -i2s_audio: - - id: i2s_output - # i2s_output data pin is gpio10 - i2s_lrclk_pin: - number: GPIO7 - i2s_bclk_pin: - number: GPIO8 - - - id: i2s_input - # data line is GPIO15 - i2s_lrclk_pin: - number: GPIO14 - i2s_bclk_pin: - number: GPIO13 - -microphone: - - platform: nabu_microphone - i2s_din_pin: GPIO15 - adc_type: external - pdm: false - sample_rate: 16000 - bits_per_sample: 32bit - i2s_mode: secondary - i2s_audio_id: i2s_input - channel_1: - id: asr_mic - amplify: false - channel_2: - id: comm_mic - amplify: false # May be useful to enable, but may also result in worse wake word detection if speaking close to the device - -media_player: - - platform: nabu - id: nabu_media_player - name: Media Player - internal: false - audio_dac: - sample_rate: 48000 - i2s_dout_pin: GPIO10 - bits_per_sample: 32bit - i2s_mode: secondary - i2s_audio_id: i2s_output - on_mute: - - script.execute: control_leds - on_unmute: - - script.execute: control_leds - on_volume: - - script.execute: control_leds - on_announcement: - - nabu.set_ducking: - decibel_reduction: 20 - duration: 0.0s - on_state: - if: - condition: - and: - - switch.is_off: timer_ringing - - not: - voice_assistant.is_running: - - not: - lambda: return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; - then: - - nabu.set_ducking: - decibel_reduction: 0 - duration: 1.0s - files: - - id: center_button_press_sound - file: sounds/center_button_press.flac - - id: center_button_double_press_sound - file: sounds/center_button_double_press.flac - - id: center_button_triple_press_sound - file: sounds/center_button_triple_press.flac - - id: center_button_long_press_sound - file: sounds/center_button_long_press.flac - - id: device_connected_sound - file: sounds/device_connected.mp3 - - id: factory_reset_initiated_sound - file: sounds/factory_reset_initiated.mp3 - - id: factory_reset_cancelled_sound - file: sounds/factory_reset_cancelled.mp3 - - id: jack_connected_sound - file: sounds/jack_connected.flac - - id: jack_disconnected_sound - file: sounds/jack_disconnected.flac - - id: mute_switch_on_sound - file: sounds/mute_switch_on.flac - - id: mute_switch_off_sound - file: sounds/mute_switch_off.flac - - id: timer_finished_sound - file: sounds/timer_finished.flac - - id: wake_word_triggered_sound - file: sounds/wake_word_triggered.flac - - id: easter_egg_tick_sound - file: sounds/easter_egg_tick.mp3 - - id: easter_egg_tada_sound - file: sounds/easter_egg_tada.mp3 - -voice_kit: - reset_pin: GPIO4 - firmware: - url: https://github.com/esphome/voice-kit-xmos-firmware/releases/download/v1.2.0/ffva_v1.2.0_upgrade.bin - version: "1.2.0" - md5: 912b84ea15df4a0081e39ffd9740b884 - -external_components: - - source: - type: git - url: https://github.com/esphome/voice-kit - ref: dev - components: - - aic3204 - - audio_dac - - media_player - - micro_wake_word - - microphone - - nabu - - nabu_microphone - - voice_assistant - - voice_kit - refresh: 0s - -audio_dac: - - platform: aic3204 - -micro_wake_word: - models: - - model: https://github.com/kahrendt/microWakeWord/releases/download/okay_nabu/okay_nabu.json - id: okay_nabu - - model: hey_jarvis - id: hey_jarvis - - model: hey_mycroft - id: hey_mycroft - vad: - microphone: comm_mic - on_wake_word_detected: - # If the wake word is detected when the device is muted (Possible with the software mute switch): Do nothing - - if: - condition: - switch.is_off: master_mute_switch - then: - # If a timer is ringing: Stop it, do not start the voice assistant (We can stop timer from voice!) - - if: - condition: - switch.is_on: timer_ringing - then: - - switch.turn_off: timer_ringing - # Start voice assistant, stop current announcement. - else: - - if: - condition: - lambda: return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; - then: - lambda: |- - id(nabu_media_player) - ->make_call() - .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP) - .set_announcement(true) - .perform(); - - if: - condition: - switch.is_on: wake_sound - then: - - script.execute: - id: play_sound - priority: true - sound_file: !lambda return id(wake_word_triggered_sound); - # - wait_until: - # lambda: |- - # return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; - # - wait_until: - # not: - # lambda: |- - # return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; - - delay: 300ms - - voice_assistant.start: - wake_word: !lambda return wake_word; - -select: - - platform: template - name: "Active wake word" - icon: "mdi:bullhorn" - entity_category: config - options: - - "OK Nabu" - - "Hey Jarvis" - - "Hey Mycroft" - optimistic: true - restore_value: true - on_value: - then: - - lambda: |- - if (x == "OK Nabu") { - id(okay_nabu).enable(); - id(hey_mycroft).disable(); - id(hey_jarvis).disable(); - } else if (x == "Hey Mycroft") { - id(okay_nabu).disable(); - id(hey_mycroft).enable(); - id(hey_jarvis).disable(); - } else if (x == "Hey Jarvis") { - id(okay_nabu).disable(); - id(hey_mycroft).disable(); - id(hey_jarvis).enable(); - } - -voice_assistant: - id: va - microphone: asr_mic - media_player: nabu_media_player - use_wake_word: false - noise_suppression_level: 0 - auto_gain: 0 dbfs - volume_multiplier: 1 - on_client_connected: - - if: - condition: - lambda: return id(init_in_progress); - then: - - script.execute: - id: play_sound - priority: false - sound_file: !lambda return id(device_connected_sound); - - lambda: id(init_in_progress) = false; - - micro_wake_word.start: - - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; - - script.execute: control_leds - on_client_disconnected: - - voice_assistant.stop: - - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id}; - - script.execute: control_leds - on_error: - - if: - condition: - lambda: return !id(init_in_progress); - then: - - lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id}; - - script.execute: control_leds - - delay: 1s - - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; - - script.execute: control_leds - # When the voice assistant starts: Play a wake up sound, duck audio. - on_start: - - nabu.set_ducking: - decibel_reduction: 20 # Number of dB quieter; higher implies more quiet, 0 implies full volume - duration: 0.0s # The duration of the transition (default is 0) - on_listening: - - lambda: id(voice_assistant_phase) = ${voice_assist_waiting_for_command_phase_id}; - - script.execute: control_leds - on_stt_vad_start: - - lambda: id(voice_assistant_phase) = ${voice_assist_listening_for_command_phase_id}; - - script.execute: control_leds - on_stt_vad_end: - - lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id}; - - script.execute: control_leds - on_tts_start: - - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; - - script.execute: control_leds - # When the voice assistant ends: Stop ducking audio. - on_end: - - wait_until: - not: - voice_assistant.is_running: - - nabu.set_ducking: - decibel_reduction: 0 # 0 dB means no reduction - duration: 1.0s - - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; - - script.execute: control_leds - on_timer_finished: - - switch.turn_on: timer_ringing - -button: - # TODO: Remove before launch, added for testing/development - - platform: restart - name: "Restart" - entity_category: diagnostic - # TODO: make it internal before launch - - platform: factory_reset - id: factory_reset_button - name: "Factory Reset" - entity_category: diagnostic - # TODO: Remove before launch, added for testing/development - - platform: template - name: "timer beep" - entity_category: diagnostic - on_press: - - script.execute: - id: play_sound - priority: true - sound_file: !lambda return id(timer_finished_sound); - -debug: - update_interval: 5s +<<: !include home-assistant-voice.yaml