mirror of
https://github.com/iMike78/nest-mini-drop-in-pcb.git
synced 2026-02-22 20:46:52 +00:00
1522 lines
52 KiB
YAML
1522 lines
52 KiB
YAML
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'
|
|
# Change this to true in case you ahve a hidden SSID at home.
|
|
hidden_ssid: "false"
|
|
|
|
esphome:
|
|
name: micimike-nappali
|
|
friendly_name: MiciMike nappali
|
|
on_boot:
|
|
then:
|
|
- light.turn_on:
|
|
id: led_ring
|
|
effect: slow_pulse
|
|
red: 100%
|
|
green: 60%
|
|
blue: 0%
|
|
- wait_until:
|
|
condition:
|
|
wifi.connected:
|
|
- light.turn_on:
|
|
id: led_ring
|
|
effect: pulse
|
|
red: 0%
|
|
green: 100%
|
|
blue: 0%
|
|
- wait_until:
|
|
condition:
|
|
api.connected:
|
|
- light.turn_on:
|
|
id: led_ring
|
|
effect: none
|
|
red: 0%
|
|
green: 100%
|
|
blue: 0%
|
|
- delay: 1s
|
|
- script.execute: reset_led
|
|
|
|
|
|
esp32:
|
|
board: esp32-s3-devkitc-1
|
|
cpu_frequency: 240MHz
|
|
variant: esp32s3
|
|
flash_size: 16MB
|
|
framework:
|
|
type: esp-idf
|
|
version: recommended
|
|
sdkconfig_options:
|
|
CONFIG_ESP32S3_DATA_CACHE_64KB: "y"
|
|
CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y"
|
|
CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB: "y"
|
|
|
|
# Moves instructions and read only data from flash into PSRAM on boot.
|
|
# Both enabled allows instructions to execute while a flash operation is in progress without needing to be placed in IRAM.
|
|
# Considerably speeds up mWW at the cost of using more PSRAM.
|
|
CONFIG_SPIRAM_RODATA: "y"
|
|
CONFIG_SPIRAM_FETCH_INSTRUCTIONS: "y"
|
|
|
|
CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST: "y"
|
|
CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY: "y"
|
|
|
|
CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC: "y"
|
|
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3: "y" # TLS1.3 support isn't enabled by default in IDF 5.1.5
|
|
|
|
wifi:
|
|
id: wifi_id
|
|
ssid: !secret ssid
|
|
password: !secret wifi_pass
|
|
output_power: 8.5dB
|
|
power_save_mode: none
|
|
reboot_timeout: 10min
|
|
manual_ip:
|
|
static_ip: 192.168.1.81
|
|
gateway: 192.168.1.1
|
|
subnet: 255.255.255.0
|
|
dns1: !secret dns1
|
|
dns2: !secret dns2
|
|
ap:
|
|
ssid: "${device_name}"
|
|
password: !secret wifi_pass
|
|
|
|
captive_portal:
|
|
|
|
web_server:
|
|
port: 80
|
|
|
|
logger:
|
|
|
|
ota:
|
|
platform: esphome
|
|
|
|
api:
|
|
id: api_id
|
|
encryption:
|
|
key: !secret api_key
|
|
reboot_timeout: 10min
|
|
services:
|
|
- service: start_va
|
|
then:
|
|
- voice_assistant.start
|
|
- service: stop_va
|
|
then:
|
|
- voice_assistant.stop
|
|
- service: notification_on
|
|
then:
|
|
- script.execute: turn_on_notification
|
|
- service: notification_clear
|
|
then:
|
|
- script.execute: clear_notification
|
|
|
|
button:
|
|
- platform: restart
|
|
name: "${friendly_name} újraindítás"
|
|
- platform: safe_mode
|
|
name: "${friendly_name} újraindítás (Safe Mode)"
|
|
|
|
i2c:
|
|
- id: internal_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 storing the state of ImprovBLE. Used to draw different LED animations
|
|
- id: improv_ble_in_progress
|
|
type: bool
|
|
restore_value: no
|
|
initial_value: 'false'
|
|
# 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: color_changed
|
|
type: bool
|
|
restore_value: no
|
|
initial_value: 'false'
|
|
# Global variable tracking if the jack has been plugged touched.
|
|
- id: first_active_timer
|
|
type: voice_assistant::Timer
|
|
restore_value: false
|
|
# Global variable storing if a timer is active
|
|
- id: is_timer_active
|
|
type: bool
|
|
restore_value: false
|
|
- id: thresh_percent
|
|
type: float
|
|
initial_value: "0.03"
|
|
restore_value: false
|
|
- id: touch_calibration_values_left
|
|
type: uint32_t[5]
|
|
restore_value: false
|
|
- id: touch_calibration_values_center
|
|
type: uint32_t[5]
|
|
restore_value: false
|
|
- id: touch_calibration_values_right
|
|
type: uint32_t[5]
|
|
restore_value: false
|
|
- id: notification
|
|
type: bool
|
|
restore_value: false
|
|
- id: dial_touched
|
|
type: bool
|
|
restore_value: no
|
|
initial_value: 'false'
|
|
|
|
|
|
interval:
|
|
- interval: 1s
|
|
then:
|
|
- script.execute:
|
|
id: calibrate_touch
|
|
button: 0
|
|
- script.execute:
|
|
id: calibrate_touch
|
|
button: 1
|
|
- script.execute:
|
|
id: calibrate_touch
|
|
button: 2
|
|
|
|
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
|
|
lambda: |-
|
|
// Muted either if the hardware mute switch is on or the microphone's software mute switch is enabled
|
|
if (id(hardware_mute_switch).state || id(i2s_mics).get_mute_state()) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
turn_on_action:
|
|
- if:
|
|
condition:
|
|
binary_sensor.is_off: hardware_mute_switch
|
|
then:
|
|
- microphone.mute:
|
|
turn_off_action:
|
|
- if:
|
|
condition:
|
|
binary_sensor.is_off: hardware_mute_switch
|
|
then:
|
|
- microphone.unmute:
|
|
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:
|
|
# Disable stop wake word
|
|
- micro_wake_word.disable_model: stop
|
|
- script.execute: disable_repeat
|
|
# Stop any current annoucement (ie: stop the timer ring mid playback)
|
|
- if:
|
|
condition:
|
|
media_player.is_announcing:
|
|
then:
|
|
media_player.stop:
|
|
announcement: true
|
|
# Set back ducking ratio to zero
|
|
- mixer_speaker.apply_ducking:
|
|
id: media_mixing_input
|
|
decibel_reduction: 0
|
|
duration: 1.0s
|
|
# Refresh the LED ring
|
|
- script.execute: control_leds
|
|
on_turn_on:
|
|
# Duck audio
|
|
- mixer_speaker.apply_ducking:
|
|
id: media_mixing_input
|
|
decibel_reduction: 20
|
|
duration: 0.0s
|
|
# Enable stop wake word
|
|
- micro_wake_word.enable_model: stop
|
|
# Ring timer
|
|
- script.execute: ring_timer
|
|
# Refresh LED
|
|
- script.execute: control_leds
|
|
# If 15 minutes have passed and the timer is still ringing, stop it.
|
|
- delay: 15min
|
|
- switch.turn_off: timer_ringing
|
|
- platform: template
|
|
name: Use Wake Word
|
|
id: use_wake_word
|
|
optimistic: true
|
|
restore_mode: RESTORE_DEFAULT_ON
|
|
on_turn_on:
|
|
- script.execute: turn_on_wake_word
|
|
on_turn_off:
|
|
- script.execute: turn_off_wake_word
|
|
- platform: template
|
|
name: Wake Word Listening Light
|
|
id: flicker_wake_word
|
|
entity_category: config
|
|
optimistic: true
|
|
restore_mode: RESTORE_DEFAULT_ON
|
|
on_turn_on:
|
|
- script.execute: reset_led
|
|
on_turn_off:
|
|
- script.execute: reset_led
|
|
|
|
esp32_touch:
|
|
setup_mode: false
|
|
sleep_duration: 2ms
|
|
measurement_duration: 800us
|
|
low_voltage_reference: 0.8V
|
|
high_voltage_reference: 2.4V
|
|
|
|
filter_mode: IIR_16
|
|
debounce_count: 2
|
|
noise_threshold: 0
|
|
jitter_step: 0
|
|
smooth_mode: IIR_2
|
|
|
|
denoise_grade: BIT8
|
|
denoise_cap_level: L0
|
|
|
|
binary_sensor:
|
|
- platform: gpio
|
|
id: hardware_mute_switch
|
|
internal: true
|
|
pin:
|
|
number: GPIO33
|
|
mode: INPUT_PULLUP
|
|
on_press:
|
|
# Play mute on sound only if software mute isn't enabled
|
|
- if:
|
|
condition:
|
|
- switch.is_off: master_mute_switch
|
|
then:
|
|
- script.execute:
|
|
id: play_sound
|
|
priority: false
|
|
sound_file: !lambda return id(mute_switch_on_sound);
|
|
on_release:
|
|
- script.execute:
|
|
id: play_sound
|
|
priority: false
|
|
sound_file: !lambda return id(mute_switch_off_sound);
|
|
- microphone.unmute:
|
|
- platform: esp32_touch
|
|
id: volume_down
|
|
pin: GPIO4
|
|
threshold: 539000
|
|
on_press:
|
|
then:
|
|
- light.turn_on: left_led
|
|
- script.execute:
|
|
id: set_volume
|
|
volume: -0.05
|
|
- delay: 750ms
|
|
- while:
|
|
condition:
|
|
binary_sensor.is_on: volume_down
|
|
then:
|
|
- script.execute:
|
|
id: set_volume
|
|
volume: -0.05
|
|
- delay: 150ms
|
|
on_release:
|
|
then:
|
|
- light.turn_off: left_led
|
|
|
|
- platform: esp32_touch
|
|
id: volume_up
|
|
pin: GPIO2
|
|
threshold: 580000
|
|
on_press:
|
|
then:
|
|
- light.turn_on: right_led
|
|
- script.execute:
|
|
id: set_volume
|
|
volume: 0.05
|
|
- delay: 750ms
|
|
- while:
|
|
condition:
|
|
binary_sensor.is_on: volume_up
|
|
then:
|
|
- script.execute:
|
|
id: set_volume
|
|
volume: 0.05
|
|
- delay: 150ms
|
|
on_release:
|
|
then:
|
|
- light.turn_off: right_led
|
|
|
|
- platform: esp32_touch
|
|
id: action
|
|
pin: GPIO3
|
|
threshold: 751000
|
|
on_click:
|
|
- if:
|
|
condition:
|
|
or:
|
|
- switch.is_off: use_wake_word
|
|
- binary_sensor.is_on: hardware_mute_switch
|
|
then:
|
|
- logger.log:
|
|
tag: "action_click"
|
|
format: "Voice assistant is running: %s"
|
|
args: ['id(va).is_running() ? "yes" : "no"']
|
|
- if:
|
|
condition: media_player.is_playing
|
|
then:
|
|
- media_player.stop
|
|
- if:
|
|
condition: voice_assistant.is_running
|
|
then:
|
|
- voice_assistant.stop:
|
|
else:
|
|
- voice_assistant.start:
|
|
else:
|
|
- logger.log:
|
|
tag: "action_click"
|
|
format: "Voice assistant was running with wake word detection enabled. Starting continuously"
|
|
- if:
|
|
condition: media_player.is_playing
|
|
then:
|
|
- media_player.stop
|
|
- voice_assistant.stop
|
|
- delay: 1s
|
|
- script.execute: reset_led
|
|
- script.wait: reset_led
|
|
- voice_assistant.start_continuous:
|
|
|
|
|
|
|
|
light:
|
|
# Hardware LED ring. Not used because remapping needed
|
|
- platform: esp32_rmt_led_strip
|
|
id: leds_internal
|
|
pin: GPIO21
|
|
chipset: SK6812
|
|
max_refresh_rate: 15ms
|
|
num_leds: 6
|
|
rgb_order: GRB
|
|
rmt_symbols: 192
|
|
default_transition_length: 0ms
|
|
- platform: partition
|
|
id: left_led
|
|
segments:
|
|
- id: leds_internal
|
|
from: 0
|
|
to: 0
|
|
default_transition_length: 100ms
|
|
- platform: partition
|
|
id: voice_assistant_leds
|
|
segments:
|
|
- id: leds_internal
|
|
from: 1
|
|
to: 4
|
|
default_transition_length: 100ms
|
|
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 (uint8_t 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 (uint8_t 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 (uint8_t 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 (uint8_t 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 muted_color(255, 0, 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);
|
|
for (uint8_t i = 0; i < 12; i++) {
|
|
if ( light_color.get_state() ) {
|
|
it[i] = color;
|
|
} else {
|
|
it[i] = Color::BLACK;
|
|
}
|
|
}
|
|
if ( id(master_mute_switch).state ) {
|
|
it[2] = Color::BLACK;
|
|
it[3] = muted_color;
|
|
it[4] = Color::BLACK;
|
|
it[8] = Color::BLACK;
|
|
it[9] = muted_color;
|
|
it[10] = Color::BLACK;
|
|
}
|
|
if ( id(external_media_player).volume == 0.0f || id(external_media_player).is_muted() ) {
|
|
it[5] = Color::BLACK;
|
|
it[6] = muted_color;
|
|
it[7] = Color::BLACK;
|
|
}
|
|
- addressable_lambda:
|
|
name: "Voice kit startup failed"
|
|
# update_interval: 16ms
|
|
lambda: |-
|
|
static int8_t index = 0;
|
|
Color fail_color(255, 0, 0);
|
|
for (uint8_t i = 0; i < 12; i++) {
|
|
if (i % 3) {
|
|
it[i] = Color::BLACK;
|
|
} else {
|
|
it[i] = fail_color;
|
|
}
|
|
}
|
|
- addressable_twinkle:
|
|
name: "Twinkle"
|
|
twinkle_probability: 50%
|
|
- addressable_lambda:
|
|
name: "Error"
|
|
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;
|
|
}
|
|
Color error_color(255, 0, 0);
|
|
for (uint8_t i = 0; i < 12; i++) {
|
|
it[i] = error_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_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);
|
|
Color muted_color(255, 0, 0);
|
|
for (uint8_t i = 0; i < 12; i++) {
|
|
it[i] = color * uint8_t(255/brightness_step_number*(brightness_step_number-brightness_step));
|
|
}
|
|
if ( id(master_mute_switch).state ) {
|
|
it[3] = muted_color;
|
|
it[9] = muted_color;
|
|
}
|
|
if (brightness_decreasing) {
|
|
brightness_step++;
|
|
} else {
|
|
brightness_step--;
|
|
}
|
|
if (brightness_step == 0 || brightness_step == brightness_step_number) {
|
|
brightness_decreasing = !brightness_decreasing;
|
|
}
|
|
- addressable_lambda:
|
|
name: "Timer Tick"
|
|
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);
|
|
Color muted_color(255, 0, 0);
|
|
auto timer_ratio = 12.0f * id(first_active_timer).seconds_left / max(id(first_active_timer).total_seconds , static_cast<uint32_t>(1));
|
|
uint8_t last_led_on = static_cast<uint8_t>(ceil(timer_ratio)) - 1;
|
|
for (uint8_t i = 0; i < 12; i++) {
|
|
float brightness_dip = ( i == id(global_led_animation_index) % 12 && i != last_led_on ) ? 0.9f : 1.0f ;
|
|
if (i <= timer_ratio) {
|
|
it[i] = color * min(255.0f * brightness_dip * (timer_ratio - i) , 255.0f * brightness_dip) ;
|
|
} else {
|
|
it[i] = Color::BLACK;
|
|
}
|
|
}
|
|
if (id(master_mute_switch).state) {
|
|
it[2] = Color::BLACK;
|
|
it[3] = muted_color;
|
|
it[4] = Color::BLACK;
|
|
it[8] = Color::BLACK;
|
|
it[9] = muted_color;
|
|
it[10] = Color::BLACK;
|
|
}
|
|
id(global_led_animation_index) = (12 + id(global_led_animation_index) - 1) % 12;
|
|
|
|
- platform: partition
|
|
id: led_ring
|
|
name: LED Ring
|
|
entity_category: config
|
|
icon: "mdi:circle-outline"
|
|
default_transition_length: 0ms
|
|
restore_mode: RESTORE_DEFAULT_OFF
|
|
initial_state:
|
|
color_mode: rgb
|
|
brightness: 66%
|
|
red: 9.4%
|
|
green: 73.3%
|
|
blue: 94.9%
|
|
segments:
|
|
- id: leds_internal
|
|
from: 1
|
|
to: 4
|
|
- platform: partition
|
|
id: right_led
|
|
segments:
|
|
- id: leds_internal
|
|
from: 5
|
|
to: 5
|
|
default_transition_length: 100ms
|
|
|
|
script:
|
|
- id: enable_repeat_one
|
|
then:
|
|
# Turn on the repeat mode and pause for 500 ms between playlist items/repeats
|
|
- lambda: |-
|
|
id(external_media_player)
|
|
->make_call()
|
|
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ONE)
|
|
.set_announcement(true)
|
|
.perform();
|
|
id(external_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 500);
|
|
|
|
- id: ring_timer
|
|
then:
|
|
- script.execute: enable_repeat_one
|
|
- script.execute:
|
|
id: play_sound
|
|
priority: true
|
|
sound_file: !lambda return id(timer_finished_sound);
|
|
|
|
- id: disable_repeat
|
|
then:
|
|
# Turn off the repeat mode and pause for 0 ms between playlist items/repeats
|
|
- lambda: |-
|
|
id(external_media_player)
|
|
->make_call()
|
|
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_OFF)
|
|
.set_announcement(true)
|
|
.perform();
|
|
id(external_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 0);
|
|
|
|
- id: play_sound
|
|
parameters:
|
|
priority: bool
|
|
sound_file: "audio::AudioFile*"
|
|
then:
|
|
- lambda: |-
|
|
if (priority) {
|
|
id(external_media_player)
|
|
->make_call()
|
|
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP)
|
|
.set_announcement(true)
|
|
.perform();
|
|
}
|
|
if ( (id(external_media_player).state != media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING ) || priority) {
|
|
id(external_media_player)
|
|
->play_file(sound_file, true, false);
|
|
}
|
|
|
|
# Script used to fetch the first active timer (Stored in global first_active_timer)
|
|
- id: reset_led
|
|
then:
|
|
- if:
|
|
condition:
|
|
- lambda: return id(notification);
|
|
then:
|
|
- light.turn_on:
|
|
id: led_ring
|
|
blue: 100%
|
|
red: 100%
|
|
green: 0%
|
|
brightness: 100%
|
|
effect: slow_pulse
|
|
else:
|
|
- if:
|
|
condition:
|
|
and:
|
|
- switch.is_on: use_wake_word
|
|
- switch.is_on: flicker_wake_word
|
|
- binary_sensor.is_off: hardware_mute_switch
|
|
then:
|
|
- light.turn_on:
|
|
id: led_ring
|
|
blue: 100%
|
|
red: 100%
|
|
green: 0%
|
|
brightness: 60%
|
|
effect: listening_ww
|
|
else:
|
|
- light.turn_off: led_ring
|
|
|
|
- id: turn_on_notification
|
|
then:
|
|
- lambda: id(notification) = true;
|
|
- script.execute: reset_led
|
|
|
|
- id: clear_notification
|
|
then:
|
|
- lambda: id(notification) = false;
|
|
- script.execute: reset_led
|
|
|
|
- id: set_volume
|
|
mode: restart
|
|
parameters:
|
|
volume: float
|
|
then:
|
|
- media_player.volume_set:
|
|
id: external_media_player
|
|
volume: !lambda return clamp(id(external_media_player).volume+volume, 0.0f, 1.0f);
|
|
|
|
- id: show_volume
|
|
mode: restart
|
|
then:
|
|
- light.turn_on:
|
|
id: led_ring
|
|
effect: show_volume
|
|
- delay: 1s
|
|
- script.execute: reset_led
|
|
|
|
- id: turn_on_wake_word
|
|
then:
|
|
- if:
|
|
condition:
|
|
and:
|
|
- binary_sensor.is_off: hardware_mute_switch
|
|
- switch.is_on: use_wake_word
|
|
then:
|
|
- micro_wake_word.start
|
|
- script.execute: reset_led
|
|
else:
|
|
- logger.log:
|
|
tag: "turn_on_wake_word"
|
|
format: "Trying to start listening for wake word, but %s"
|
|
args:
|
|
[
|
|
'id(hardware_mute_switch).state ? "mute switch is on" : "use wake word toggle is off"',
|
|
]
|
|
level: "INFO"
|
|
|
|
- id: turn_off_wake_word
|
|
then:
|
|
- micro_wake_word.stop
|
|
- script.execute: reset_led
|
|
|
|
- id: calibrate_touch
|
|
parameters:
|
|
button: int
|
|
then:
|
|
- lambda: |-
|
|
static uint8_t thresh_indices[3] = {0, 0, 0};
|
|
static uint32_t sums[3] = {0, 0, 0};
|
|
static uint8_t qsizes[3] = {0, 0, 0};
|
|
static uint16_t consecutive_anomalies_per_button[3] = {0, 0, 0};
|
|
|
|
uint32_t newval;
|
|
uint32_t* calibration_values;
|
|
switch(button) {
|
|
case 0:
|
|
newval = id(volume_down).get_value();
|
|
calibration_values = id(touch_calibration_values_left);
|
|
break;
|
|
case 1:
|
|
newval = id(action).get_value();
|
|
calibration_values = id(touch_calibration_values_center);
|
|
break;
|
|
case 2:
|
|
newval = id(volume_up).get_value();
|
|
calibration_values = id(touch_calibration_values_right);
|
|
break;
|
|
default:
|
|
ESP_LOGE("touch_calibration", "Invalid button ID (%d)", button);
|
|
return;
|
|
}
|
|
|
|
if(newval == 0) return;
|
|
|
|
//ESP_LOGD("touch_calibration", "[%d] qsize %d, sum %d, thresh_index %d, consecutive_anomalies %d", button, qsizes[button], sums[button], thresh_indices[button], consecutive_anomalies_per_button[button]);
|
|
//ESP_LOGD("touch_calibration", "[%d] New value is %d", button, newval);
|
|
|
|
if(qsizes[button] == 5) {
|
|
float avg = float(sums[button])/float(qsizes[button]);
|
|
if((fabs(float(newval)-avg)/avg) > id(thresh_percent)) {
|
|
consecutive_anomalies_per_button[button]++;
|
|
//ESP_LOGD("touch_calibration", "[%d] %d anomalies detected.", button, consecutive_anomalies_per_button[button]);
|
|
if(consecutive_anomalies_per_button[button] < 10)
|
|
return;
|
|
}
|
|
}
|
|
|
|
//ESP_LOGD("touch_calibration", "[%d] Resetting consecutive anomalies counter.", button);
|
|
consecutive_anomalies_per_button[button] = 0;
|
|
|
|
|
|
if(qsizes[button] == 5) {
|
|
//ESP_LOGD("touch_calibration", "[%d] Queue full, removing %d.", button, id(touch_calibration_values)[thresh_indices[button]]);
|
|
sums[button] -= (uint32_t) *(calibration_values+thresh_indices[button]);// id(touch_calibration_values)[thresh_indices[button]];
|
|
qsizes[button]--;
|
|
}
|
|
*(calibration_values+thresh_indices[button]) = newval;
|
|
sums[button] += newval;
|
|
qsizes[button]++;
|
|
thresh_indices[button] = (thresh_indices[button] + 1) % 5;
|
|
|
|
//ESP_LOGD("touch_calibration", "[%d] Average value is %d", button, sums[button]/qsizes[button]);
|
|
uint32_t newthresh = uint32_t((sums[button]/qsizes[button]) * (1.0 + id(thresh_percent)));
|
|
//ESP_LOGD("touch_calibration", "[%d] Setting threshold %d", button, newthresh);
|
|
|
|
switch(button) {
|
|
case 0:
|
|
id(volume_down).set_threshold(newthresh);
|
|
break;
|
|
case 1:
|
|
id(action).set_threshold(newthresh);
|
|
break;
|
|
case 2:
|
|
id(volume_up).set_threshold(newthresh);
|
|
break;
|
|
default:
|
|
ESP_LOGE("touch_calibration", "Invalid button ID (%d)", button);
|
|
return;
|
|
}
|
|
- id: control_leds_voice_assistant_error_phase
|
|
then:
|
|
- light.turn_on:
|
|
brightness: !lambda return min ( max( id(led_ring).current_values.get_brightness() , 0.2f ) + 0.1f , 1.0f );
|
|
red: 1
|
|
green: 0
|
|
blue: 0
|
|
id: voice_assistant_leds
|
|
effect: "Error"
|
|
|
|
- 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: !lambda return max( id(led_ring).current_values.get_brightness() , 0.2f );
|
|
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: !lambda return max( id(led_ring).current_values.get_brightness() , 0.2f );
|
|
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: !lambda return max( id(led_ring).current_values.get_brightness() , 0.2f );
|
|
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: !lambda return max( id(led_ring).current_values.get_brightness() , 0.2f );
|
|
id: voice_assistant_leds
|
|
effect: "Replying"
|
|
|
|
- id: control_leds_muted_or_silent
|
|
then:
|
|
- light.turn_on:
|
|
brightness: !lambda return max( id(led_ring).current_values.get_brightness() , 0.2f );
|
|
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: 66%
|
|
red: 1
|
|
green: 0
|
|
blue: 0
|
|
id: voice_assistant_leds
|
|
effect: "Twinkle"
|
|
|
|
- id: control_leds_timer_ticking
|
|
then:
|
|
- light.turn_on:
|
|
brightness: !lambda return max( id(led_ring).current_values.get_brightness() , 0.2f );
|
|
id: voice_assistant_leds
|
|
effect: "Timer tick"
|
|
|
|
- id: control_leds_timer_ringing
|
|
then:
|
|
- light.turn_on:
|
|
brightness: !lambda return min ( max( id(led_ring).current_values.get_brightness() , 0.2f ) + 0.1f , 1.0f );
|
|
id: voice_assistant_leds
|
|
effect: "Timer Ring"
|
|
|
|
- id: control_leds_dial_touched
|
|
then:
|
|
- light.turn_on:
|
|
brightness: !lambda return max( id(led_ring).current_values.get_brightness() , 0.2f );
|
|
id: voice_assistant_leds
|
|
effect: "Volume Display"
|
|
|
|
- id: fetch_first_active_timer
|
|
then:
|
|
- lambda: |
|
|
const auto timers = id(va).get_timers();
|
|
auto output_timer = timers.begin()->second;
|
|
for (auto &iterable_timer : timers) {
|
|
if (iterable_timer.second.is_active && iterable_timer.second.seconds_left <= output_timer.seconds_left) {
|
|
output_timer = iterable_timer.second;
|
|
}
|
|
}
|
|
id(first_active_timer) = output_timer;
|
|
|
|
- id: check_if_timers_active
|
|
then:
|
|
- lambda: |
|
|
const auto timers = id(va).get_timers();
|
|
bool output = false;
|
|
if (timers.size() > 0) {
|
|
for (auto &iterable_timer : timers) {
|
|
if(iterable_timer.second.is_active) {
|
|
output = true;
|
|
}
|
|
}
|
|
}
|
|
id(is_timer_active) = output;
|
|
|
|
- id: control_leds
|
|
then:
|
|
- lambda: |
|
|
if (id(voice_kit_component).is_failed()) {
|
|
id(control_leds_voice_kit_startup_failed).execute();
|
|
return;
|
|
}
|
|
id(check_if_timers_active).execute();
|
|
if (id(is_timer_active)){
|
|
id(fetch_first_active_timer).execute();
|
|
}
|
|
if (id(improv_ble_in_progress)) {
|
|
id(control_leds_improv_ble_state).execute();
|
|
} else if (id(init_in_progress)) {
|
|
id(control_leds_init_state).execute();
|
|
} else if (!id(wifi_id).is_connected() || !id(api_id).is_connected()){
|
|
id(control_leds_no_ha_connection_state).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(is_timer_active)) {
|
|
id(control_leds_timer_ticking).execute();
|
|
} else if (id(master_mute_switch).state) {
|
|
id(control_leds_muted_or_silent).execute();
|
|
} else if (id(external_media_player).volume == 0.0f || id(external_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();
|
|
}
|
|
|
|
# Script executed if voice_kit startup failed
|
|
# Static red "X"
|
|
- id: control_leds_voice_kit_startup_failed
|
|
then:
|
|
- light.turn_on:
|
|
brightness: 40%
|
|
red: 0%
|
|
green: 0%
|
|
blue: 0%
|
|
id: voice_assistant_leds
|
|
effect: "Voice kit startup failed"
|
|
|
|
# Script executed during Improv BLE
|
|
# Warm White Twinkle
|
|
- id: control_leds_improv_ble_state
|
|
then:
|
|
- light.turn_on:
|
|
brightness: 66%
|
|
red: 100%
|
|
green: 89%
|
|
blue: 71%
|
|
id: voice_assistant_leds
|
|
effect: "Twinkle"
|
|
|
|
# Script executed during initialization
|
|
# Blue Twinkle if Wifi is connected, Else solid warm white
|
|
- id: control_leds_init_state
|
|
then:
|
|
- if:
|
|
condition:
|
|
wifi.connected:
|
|
then:
|
|
- light.turn_on:
|
|
brightness: 66%
|
|
red: 9.4%
|
|
green: 73.3%
|
|
blue: 94.9%
|
|
id: voice_assistant_leds
|
|
effect: "Twinkle"
|
|
else:
|
|
- light.turn_on:
|
|
brightness: 66%
|
|
red: 100%
|
|
green: 89%
|
|
blue: 71%
|
|
id: voice_assistant_leds
|
|
effect: "none"
|
|
|
|
# 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: 66%
|
|
red: 1
|
|
green: 0
|
|
blue: 0
|
|
id: voice_assistant_leds
|
|
effect: "Twinkle"
|
|
|
|
- id: activate_stop_word_once
|
|
then:
|
|
- delay: 1s
|
|
# Enable stop wake word
|
|
- if:
|
|
condition:
|
|
switch.is_off: timer_ringing
|
|
then:
|
|
- micro_wake_word.enable_model: stop
|
|
- wait_until:
|
|
not:
|
|
media_player.is_announcing:
|
|
- if:
|
|
condition:
|
|
switch.is_off: timer_ringing
|
|
then:
|
|
- micro_wake_word.disable_model: stop
|
|
|
|
voice_kit:
|
|
id: voice_kit_component
|
|
i2c_id: internal_i2c
|
|
reset_pin: GPIO4
|
|
firmware:
|
|
url: https://github.com/esphome/voice-kit-xmos-firmware/releases/download/v1.3.1/ffva_v1.3.1_upgrade.bin
|
|
version: "1.3.1"
|
|
md5: 964635c5bf125529dab14a2472a15401
|
|
|
|
external_components:
|
|
- source:
|
|
type: git
|
|
url: https://github.com/esphome/home-assistant-voice-pe
|
|
ref: dev
|
|
components:
|
|
- voice_kit
|
|
refresh: 0s
|
|
|
|
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: i2s_audio
|
|
id: i2s_mics
|
|
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: stereo
|
|
|
|
speaker:
|
|
- platform: i2s_audio
|
|
id: i2s_audio_speaker
|
|
sample_rate: 44100
|
|
i2s_mode: secondary
|
|
i2s_dout_pin: GPIO10
|
|
bits_per_sample: 16bit
|
|
i2s_audio_id: i2s_output
|
|
dac_type: external
|
|
channel: mono
|
|
timeout: never
|
|
buffer_duration: 100ms
|
|
|
|
# Virtual speakers to combine the announcement and media streams together into one output
|
|
- platform: mixer
|
|
id: mixing_speaker
|
|
output_speaker: i2s_audio_speaker
|
|
num_channels: 1
|
|
source_speakers:
|
|
- id: announcement_mixing_input
|
|
timeout: never
|
|
- id: media_mixing_input
|
|
timeout: never
|
|
|
|
# Vritual speakers to resample each pipelines' audio, if necessary, as the mixer speaker requires the same sample rate
|
|
- platform: resampler
|
|
id: announcement_resampling_speaker
|
|
output_speaker: announcement_mixing_input
|
|
sample_rate: 44100
|
|
bits_per_sample: 16
|
|
- platform: resampler
|
|
id: media_resampling_speaker
|
|
output_speaker: media_mixing_input
|
|
sample_rate: 44100
|
|
bits_per_sample: 16
|
|
|
|
media_player:
|
|
- platform: speaker
|
|
id: external_media_player
|
|
name: Media Player
|
|
internal: False
|
|
volume_increment: 0.05
|
|
volume_min: 0.4
|
|
volume_max: 0.85
|
|
announcement_pipeline:
|
|
speaker: announcement_resampling_speaker
|
|
format: FLAC # FLAC is the least processor intensive codec
|
|
num_channels: 1 # Stereo audio is unnecessary for announcements
|
|
sample_rate: 44100
|
|
media_pipeline:
|
|
speaker: media_resampling_speaker
|
|
format: FLAC # FLAC is the least processor intensive codec
|
|
num_channels: 1
|
|
sample_rate: 44100
|
|
on_mute:
|
|
- script.execute: control_leds
|
|
on_unmute:
|
|
- script.execute: control_leds
|
|
on_volume:
|
|
- script.execute: control_leds
|
|
on_announcement:
|
|
- mixer_speaker.apply_ducking:
|
|
id: media_mixing_input
|
|
decibel_reduction: 20
|
|
duration: 0.0s
|
|
on_state:
|
|
if:
|
|
condition:
|
|
and:
|
|
- switch.is_off: timer_ringing
|
|
- not:
|
|
voice_assistant.is_running:
|
|
- not:
|
|
media_player.is_announcing:
|
|
then:
|
|
- mixer_speaker.apply_ducking:
|
|
id: media_mixing_input
|
|
decibel_reduction: 0
|
|
duration: 1.0s
|
|
files:
|
|
- id: center_button_press_sound
|
|
file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/center_button_press.flac
|
|
- id: center_button_double_press_sound
|
|
file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/center_button_double_press.flac
|
|
- id: center_button_triple_press_sound
|
|
file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/center_button_triple_press.flac
|
|
- id: center_button_long_press_sound
|
|
file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/center_button_long_press.flac
|
|
- id: factory_reset_initiated_sound
|
|
file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/factory_reset_initiated.mp3
|
|
- id: factory_reset_cancelled_sound
|
|
file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/factory_reset_cancelled.mp3
|
|
- id: factory_reset_confirmed_sound
|
|
file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/factory_reset_confirmed.mp3
|
|
- id: jack_connected_sound
|
|
file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/jack_connected.flac
|
|
- id: jack_disconnected_sound
|
|
file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/jack_disconnected.flac
|
|
- id: mute_switch_on_sound
|
|
file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/mute_switch_on.flac
|
|
- id: mute_switch_off_sound
|
|
file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/mute_switch_off.flac
|
|
- id: timer_finished_sound
|
|
file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/timer_finished.flac
|
|
- id: wake_word_triggered_sound
|
|
file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/wake_word_triggered.flac
|
|
- id: easter_egg_tick_sound
|
|
file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/easter_egg_tick.mp3
|
|
- id: easter_egg_tada_sound
|
|
file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/easter_egg_tada.mp3
|
|
- id: error_cloud_expired
|
|
file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/error_cloud_expired.mp3
|
|
|
|
micro_wake_word:
|
|
id: mww
|
|
microphone:
|
|
microphone: i2s_mics
|
|
channels: 1
|
|
gain_factor: 4
|
|
stop_after_detection: false
|
|
models:
|
|
- model: https://github.com/kahrendt/microWakeWord/releases/download/okay_nabu_20241226.3/okay_nabu.json
|
|
id: okay_nabu
|
|
- model: hey_jarvis
|
|
id: hey_jarvis
|
|
- model: hey_mycroft
|
|
id: hey_mycroft
|
|
- model: https://github.com/kahrendt/microWakeWord/releases/download/stop/stop.json
|
|
id: stop
|
|
internal: true
|
|
vad:
|
|
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
|
|
# Stop voice assistant if running
|
|
else:
|
|
- if:
|
|
condition:
|
|
voice_assistant.is_running:
|
|
then:
|
|
voice_assistant.stop:
|
|
# Stop any other media player announcement
|
|
else:
|
|
- if:
|
|
condition:
|
|
media_player.is_announcing:
|
|
then:
|
|
- media_player.stop:
|
|
announcement: true
|
|
# Start the voice assistant and play the wake sound, if enabled
|
|
else:
|
|
- if:
|
|
condition:
|
|
switch.is_on: wake_sound
|
|
then:
|
|
- script.execute:
|
|
id: play_sound
|
|
priority: true
|
|
sound_file: !lambda return id(wake_word_triggered_sound);
|
|
- delay: 300ms
|
|
- voice_assistant.start:
|
|
wake_word: !lambda return wake_word;
|
|
|
|
select:
|
|
- platform: template
|
|
name: "Wake word sensitivity"
|
|
optimistic: true
|
|
initial_option: Slightly sensitive
|
|
restore_value: true
|
|
entity_category: config
|
|
options:
|
|
- Slightly sensitive
|
|
- Moderately sensitive
|
|
- Very sensitive
|
|
on_value:
|
|
# Sets specific wake word probabilities computed for each particular model
|
|
# Note probability cutoffs are set as a quantized uint8 value, each comment has the corresponding floating point cutoff
|
|
# False Accepts per Hour values are tested against all units and channels from the Dinner Party Corpus.
|
|
# These cutoffs apply only to the specific models included in the firmware: okay_nabu@20241226.3, hey_jarvis@v2, hey_mycroft@v2
|
|
lambda: |-
|
|
if (x == "Slightly sensitive") {
|
|
id(okay_nabu).set_probability_cutoff(217); // 0.85 -> 0.000 FAPH on DipCo (Manifest's default)
|
|
id(hey_jarvis).set_probability_cutoff(247); // 0.97 -> 0.563 FAPH on DipCo (Manifest's default)
|
|
id(hey_mycroft).set_probability_cutoff(253); // 0.99 -> 0.567 FAPH on DipCo
|
|
} else if (x == "Moderately sensitive") {
|
|
id(okay_nabu).set_probability_cutoff(176); // 0.69 -> 0.376 FAPH on DipCo
|
|
id(hey_jarvis).set_probability_cutoff(235); // 0.92 -> 0.939 FAPH on DipCo
|
|
id(hey_mycroft).set_probability_cutoff(242); // 0.95 -> 1.502 FAPH on DipCo (Manifest's default)
|
|
} else if (x == "Very sensitive") {
|
|
id(okay_nabu).set_probability_cutoff(143); // 0.56 -> 0.751 FAPH on DipCo
|
|
id(hey_jarvis).set_probability_cutoff(212); // 0.83 -> 1.502 FAPH on DipCo
|
|
id(hey_mycroft).set_probability_cutoff(237); // 0.93 -> 1.878 FAPH on DipCo
|
|
}
|
|
|
|
voice_assistant:
|
|
id: va
|
|
microphone:
|
|
microphone: i2s_mics
|
|
channels: 0
|
|
media_player: external_media_player
|
|
micro_wake_word: mww
|
|
use_wake_word: false
|
|
noise_suppression_level: 0
|
|
auto_gain: 0 dbfs
|
|
volume_multiplier: 1
|
|
on_client_connected:
|
|
- 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:
|
|
# Only set the error phase if the error code is different than duplicate_wake_up_detected or stt-no-text-recognized
|
|
# These two are ignored for a better user experience
|
|
- if:
|
|
condition:
|
|
and:
|
|
- lambda: return !id(init_in_progress);
|
|
- lambda: return code != "duplicate_wake_up_detected";
|
|
- lambda: return code != "stt-no-text-recognized";
|
|
then:
|
|
- lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id};
|
|
- script.execute: control_leds
|
|
# If the error code is cloud-auth-failed, serve a local audio file guiding the user.
|
|
- if:
|
|
condition:
|
|
- lambda: return code == "cloud-auth-failed";
|
|
then:
|
|
- script.execute:
|
|
id: play_sound
|
|
priority: true
|
|
sound_file: !lambda return id(error_cloud_expired);
|
|
# When the voice assistant starts: Play a wake up sound, duck audio.
|
|
on_start:
|
|
- mixer_speaker.apply_ducking:
|
|
id: media_mixing_input
|
|
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 no transition)
|
|
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_intent_progress:
|
|
- if:
|
|
condition:
|
|
# A nonempty x variable means a streaming TTS url was sent to the media player
|
|
lambda: 'return !x.empty();'
|
|
then:
|
|
- lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id};
|
|
- script.execute: control_leds
|
|
# Start a script that would potentially enable the stop word if the response is longer than a second
|
|
- script.execute: activate_stop_word_once
|
|
on_tts_start:
|
|
- if:
|
|
condition:
|
|
# The intent_progress trigger didn't start the TTS Reponse
|
|
lambda: 'return id(voice_assistant_phase) != ${voice_assist_replying_phase_id};'
|
|
then:
|
|
- lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id};
|
|
- script.execute: control_leds
|
|
# Start a script that would potentially enable the stop word if the response is longer than a second
|
|
- script.execute: activate_stop_word_once
|
|
# When the voice assistant ends ...
|
|
on_end:
|
|
- wait_until:
|
|
not:
|
|
voice_assistant.is_running:
|
|
# Stop ducking audio.
|
|
- mixer_speaker.apply_ducking:
|
|
id: media_mixing_input
|
|
decibel_reduction: 0
|
|
duration: 1.0s
|
|
# If the end happened because of an error, let the error phase on for a second
|
|
- if:
|
|
condition:
|
|
lambda: return id(voice_assistant_phase) == ${voice_assist_error_phase_id};
|
|
then:
|
|
- delay: 1s
|
|
# Reset the voice assistant phase id and reset the LED animations.
|
|
- lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
|
|
- script.execute: control_leds
|
|
on_timer_finished:
|
|
- switch.turn_on: timer_ringing
|
|
on_timer_started:
|
|
- script.execute: control_leds
|
|
on_timer_cancelled:
|
|
- script.execute: control_leds
|
|
on_timer_updated:
|
|
- script.execute: control_leds
|
|
on_timer_tick:
|
|
- script.execute: control_leds
|
|
|
|
|
|
|