nest-mini-drop-in-pcb/MiciMike.yaml
2025-07-18 16:49:19 +01:00

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