Skip to content

Instantly share code, notes, and snippets.

@martin-kokos
Last active January 24, 2026 22:46
Show Gist options
  • Select an option

  • Save martin-kokos/e9767c016c9112530644fd2be2911545 to your computer and use it in GitHub Desktop.

Select an option

Save martin-kokos/e9767c016c9112530644fd2be2911545 to your computer and use it in GitHub Desktop.
ESP32-Korvo V1.1 ESPhome Voice assistant
# https://github.com/espressif/esp-skainet/blob/master/docs/en/hw-reference/esp32/user-guide-esp32-korvo-v1.1.md
# Works:
# - shows up in HomeAssistant
# - leds, although flicker sometimes
# - buttons
# - wake word, audio sending, audio receive
# Doesn't work:
# - speaker playback: I2S_MCLK problem
substitutions:
name: homeassist03
friendly_name: HomeAssist Speaker 3
voice_assist_idle_phase_id: "1"
voice_assist_listening_phase_id: "2"
voice_assist_thinking_phase_id: "3"
voice_assist_replying_phase_id: "4"
voice_assist_not_ready_phase_id: "10"
voice_assist_error_phase_id: "11"
voice_assist_muted_phase_id: "12"
micro_wake_word_model: hey_jarvis # current esphome wake word models are alexa,okay_nabu,hey_jarvis use one of these for micro_wake_word_model
esphome:
name: "${name}"
friendly_name: "${friendly_name}"
min_version: 2024.12.3
platformio_options:
board_build.flash_mode: dio
board_build.flash: 8MB
on_boot:
- priority: -100
then:
- light.turn_on:
id: led_ring
blue: 0%
red: 100%
green: 0%
effect: Fast Pulse192.168.1.10
- delay: 1s
- wait_until:
condition:
wifi.connected:
- light.turn_on:
id: led_ring
blue: 0%
red: 100%
green: 50%
effect: Slow Pulse
- wait_until:
condition:
api.connected
- lambda: id(init_in_progress) = false;
- lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
- script.execute: reset_led
esp32:
board: esp-wrover-kit
flash_size: 8MB
framework:
type: esp-idf
version: recommended
sdkconfig_options:
CONFIG_ESP32_SPIRAM_SUPPORT: y
CONFIG_SPIRAM_SPEED_80M: y
CONFIG_SPIRAM_MODE_QUAD: y
CONFIG_ESP32_DEFAULT_CPU_FREQ_240: y
CONFIG_ESP32_DATA_CACHE_64KB: y
CONFIG_ESP32_DATA_CACHE_LINE_64B: y
CONFIG_AUDIO_BOARD_CUSTOM: y
micro_wake_word:
id: wake_word
microphone: external_mic
models:
- model: github://esphome/micro-wake-word-models/models/v2/${micro_wake_word_model}.json
on_wake_word_detected:
then:
- lambda: ESP_LOGW("DEBUG", "Wake word detected, starting voice assistant...");
- delay: 300ms
- lambda: ESP_LOGW("DEBUG", "About to call voice_assistant.start");
- voice_assistant.start:
id: voice_asst
wake_word: !lambda return wake_word;
- lambda: ESP_LOGW("DEBUG", "voice_assistant.start called");
text_sensor:
- platform: wifi_info
ip_address:
name: "${friendly_name} IP Address"
time:
platform: homeassistant
id: homeassistant_time
i2c:
sda: GPIO19
scl: GPIO32
scan: true
frequency: 400kHz
audio_dac:
- id: es8311_dac
platform: es8311
use_mclk: true
audio_adc:
- platform: es7210
id: es7210_adc
address: 0x40
sample_rate: 16000
i2s_audio:
- id: i2s_out
i2s_lrclk_pin: GPIO22
i2s_bclk_pin: GPIO25
i2s_mclk_pin:
number: GPIO0
allow_other_uses: true
- id: i2s_in
i2s_lrclk_pin: GPIO26
i2s_bclk_pin: GPIO27
i2s_mclk_pin:
number: GPIO0
allow_other_uses: true
speaker:
- id: external_speaker
i2s_audio_id: i2s_out
platform: i2s_audio
dac_type: external
i2s_dout_pin: GPIO13
audio_dac: es8311_dac
sample_rate: 16000
bits_per_sample: 16bit
channel: mono
microphone:
- platform: i2s_audio
id: external_mic
adc_type: external
i2s_audio_id: i2s_in
i2s_din_pin: GPIO36
channel: left
on_data:
- lambda: |-
static int count = 0;
count++;
if (count >= 100) {
ESP_LOGD("mic", "Received %d bytes", x.size());
count = 0;
}
media_player:
- platform: speaker
name: "Test Media Player"
id: speaker_media_player
codec_support_enabled: true # Enable FLAC/MP3 decoders
announcement_pipeline:
speaker: external_speaker
format: FLAC
num_channels: 1
on_announcement:
- micro_wake_word.stop:
- delay: 50ms
on_idle:
- micro_wake_word.start:
number:
- platform: template
name: "DAC Volume"
id: dac_volume
min_value: 0
max_value: 100
step: 1
initial_value: 80
optimistic: true
set_action:
- audio_dac.set_volume:
id: es8311_dac
volume: !lambda 'return x / 100.0;'
voice_assistant:
id: voice_asst
microphone: external_mic
speaker: external_speaker
noise_suppression_level: 1
auto_gain: 31dBFS
volume_multiplier: 6.0
use_wake_word: false
on_listening:
- lambda: id(voice_assistant_phase) = ${voice_assist_listening_phase_id};
- script.execute: reset_led
on_stt_vad_end:
- lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id};
- script.execute: reset_led
on_tts_start:
- light.turn_on:
id: led_ring
blue: 0%
red: 0%
green: 100%
brightness: 50%
effect: pulse
- micro_wake_word.stop: # Stop wake word detection
on_stt_end:
- homeassistant.service:
service: media_player.play_media
data:
entity_id: media_player.ke_ting
media_content_id: !lambda return x;
media_content_type: music
announce: "true"
on_tts_stream_start:
- delay: 100ms
- lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id};
- script.execute: reset_led
on_end:
- wait_until:
not:
speaker.is_playing:
- lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
- script.execute: reset_led
- if:
condition:
and:
- switch.is_off: mute
then:
- wait_until:
lambda: return !id(wake_word).is_running() && !id(voice_asst).is_running();
- if:
condition:
lambda: return id(wake_word_engine_location).state == "On device";
then:
- lambda: ESP_LOGD("micro_wake_word", "Resetting micro wake word pipeline (On device)...");
- micro_wake_word.stop
- delay: 500ms
- micro_wake_word.start
on_error:
- if:
condition:
lambda: return !id(init_in_progress);
then:
- lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id};
- script.execute: reset_led
- delay: 2s
- if:
condition:
switch.is_off: mute
then:
- lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
else:
- lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
- script.execute: reset_led
on_client_connected:
- if:
condition:
switch.is_off: mute
then:
- if:
condition:
lambda: return id(wake_word_engine_location).state == "In Home Assistant";
then:
- lambda: id(voice_asst).set_use_wake_word(true);
- voice_assistant.start_continuous:
- lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
else:
- lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
- lambda: id(init_in_progress) = false;
- script.execute: reset_led
on_client_disconnected:
- if:
condition:
lambda: return id(wake_word_engine_location).state == "In Home Assistant";
then:
- lambda: id(voice_asst).set_use_wake_word(false);
- voice_assistant.stop:
- lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id};
- script.execute: reset_led
on_tts_end:
- micro_wake_word.start:
select:
- platform: template
entity_category: config
name: Wake word engine location
id: wake_word_engine_location
optimistic: true
restore_value: true
options:
- In Home Assistant
- On device
initial_option: On device
on_value:
- wait_until:
lambda: return id(voice_assistant_phase) == ${voice_assist_muted_phase_id} || id(voice_assistant_phase) == ${voice_assist_idle_phase_id};
- if:
condition:
lambda: return x == "In Home Assistant";
then:
- wait_until:
lambda: return !id(voice_asst).is_running();
- micro_wake_word.stop
- delay: 300ms
- if:
condition:
switch.is_off: mute
then:
- lambda: id(voice_asst).set_use_wake_word(true);
- voice_assistant.start_continuous:
else:
- wait_until:
lambda: return !id(wake_word).is_running();
- voice_assistant.stop
- delay: 300ms
- lambda: id(voice_asst).set_use_wake_word(false);
- micro_wake_word.start
script:
- id: reset_led
then:
- if:
condition:
lambda: return !id(init_in_progress);
then:
- if:
condition:
lambda: return id(voice_assistant_phase) == ${voice_assist_listening_phase_id};
then:
- light.turn_on:
id: led_ring
blue: 100%
red: 0%
green: 0%
brightness: 100%
effect: wakeword
- if:
condition:
lambda: return id(voice_assistant_phase) == ${voice_assist_thinking_phase_id};
then:
- light.turn_on:
id: led_ring
blue: 100%
red: 100%
green: 0%
brightness: 100%
effect: Working
- delay: 100ms
- if:
condition:
lambda: return id(voice_assistant_phase) == ${voice_assist_replying_phase_id};
then:
- light.turn_on:
id: led_ring
blue: 100%
red: 0%
green: 0%
brightness: 100%
effect: Working
- if:
condition:
lambda: return id(voice_assistant_phase) == ${voice_assist_idle_phase_id};
then:
- light.turn_on:
id: led_ring
blue: 100%
red: 0%
green: 0%
brightness: 40%
effect: none
- delay: 200ms
- if:
condition:
lambda: return id(voice_assistant_phase) == ${voice_assist_not_ready_phase_id};
then:
- light.turn_on:
id: led_ring
blue: 40%
red: 100%
green: 0%
effect: Slow Pulse
- if:
condition:
lambda: return id(voice_assistant_phase) == ${voice_assist_error_phase_id};
then:
- light.turn_on:
id: led_ring
blue: 0%
red: 100%
green: 0%
brightness: 100%
effect: none
- if:
condition:
lambda: return id(voice_assistant_phase) == ${voice_assist_muted_phase_id};
then:
- light.turn_off: led_ring
else:
- light.turn_on:
id: led_ring
blue: 0%
red: 100%
green: 0%
effect: Fast Pulse
switch:
- platform: template
name: Mute
id: mute
optimistic: true
restore_mode: RESTORE_DEFAULT_OFF
entity_category: config
on_turn_off:
- if:
condition:
lambda: return !id(init_in_progress);
then:
- lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
- if:
condition:
not:
- voice_assistant.is_running
then:
- if:
condition:
lambda: return id(wake_word_engine_location).state == "In Home Assistant";
then:
- lambda: id(voice_asst).set_use_wake_word(true);
- voice_assistant.start_continuous
- if:
condition:
lambda: return id(wake_word_engine_location).state == "On device";
then:
- micro_wake_word.start
- script.execute: reset_led
on_turn_on:
- if:
condition:
lambda: return !id(init_in_progress);
then:
- lambda: id(voice_asst).set_use_wake_word(false);
- voice_assistant.stop
- micro_wake_word.stop
- lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
- script.execute: reset_led
- platform: restart
name: "${name} Restart"
- platform: gpio
pin: GPIO12
id: pa_ctrl
name: "${name} power amplifier"
restore_mode: ALWAYS_ON
light:
- platform: esp32_rmt_led_strip
id: led_ring
name: "${friendly_name} Light"
pin: GPIO33 # GPIO33 for esp32-korvo-1.1 GPIO19 for esp32-s3-korvo-1
num_leds: 12
rmt_symbols: 96
rgb_order: GRB
chipset: ws2812
default_transition_length: 0s
effects:
- pulse:
name: "Pulse"
transition_length: 0.5s
update_interval: 0.5s
- addressable_twinkle:
name: "Working"
twinkle_probability: 5%
progress_interval: 4ms
- addressable_color_wipe:
name: "Wakeword"
colors:
- red: 28%
green: 100%
blue: 90%
num_leds: 12
add_led_interval: 40ms
reverse: false
- addressable_color_wipe:
name: "Connecting"
colors:
- red: 60%
green: 60%
blue: 60%
num_leds: 12
- red: 60%
green: 60%
blue: 0%
num_leds: 12
add_led_interval: 100ms
reverse: true
- addressable_color_wipe:
name: "Thinking"
colors:
- red: 1%
green: 90%
blue: 99%
num_leds: 2
- red: 13%
green: 17%
blue: 87%
num_leds: 6
add_led_interval: 75ms
reverse: true
- pulse:
name: "Slow Pulse"
transition_length: 0.5s
update_interval: 1s
min_brightness: 0%
max_brightness: 100%
- pulse:
name: "Fast Pulse"
transition_length: 50ms
update_interval: 100ms
min_brightness: 50%
max_brightness: 100%
globals:
- id: init_in_progress
type: bool
restore_value: false
initial_value: "true"
- id: voice_assistant_phase
type: int
restore_value: false
initial_value: ${voice_assist_not_ready_phase_id}
- id: speaker_volume
type: int
restore_value: yes
initial_value: '50' # Initial volume level (0-100)
button:
- platform: template
name: "DAC Unmute"
on_press:
- audio_dac.mute_off: es8311_dac
- platform: template
name: "DAC Mute"
on_press:
- audio_dac.mute_on: es8311_dac
- platform: template
name: "${friendly_name} Volume Up"
id: btn_volume_up
on_press:
then:
- lambda: |-
id(speaker_volume) = max(0.0f, id(speaker_volume) + 0.05f);
- platform: template
name: "${friendly_name} Volume Down"
id: btn_volume_down
on_press:
then:
- lambda: |-
id(speaker_volume) = max(0.0f, id(speaker_volume) - 0.05f);
- platform: template
name: "Test Tone"
on_press:
- lambda: |-
// Simple square wave test
const int samples = 16000; // 1 second at 16kHz
std::vector<int16_t> buffer(samples);
for (int i = 0; i < samples; i++) {
buffer[i] = (i % 100 < 50) ? 16000 : -16000;
}
id(external_speaker).play((const uint8_t*)buffer.data(), samples * 2);
- delay: 1s
- lambda: id(external_speaker).stop();
binary_sensor:
- platform: template
name: "${friendly_name} Volume Up"
id: btn_vol_up
trigger_on_initial_state : True
on_press:
then:
- lambda: |-
id(speaker_volume) = max(0.0f, id(speaker_volume) + 0.05f);
- platform: template
name: "${friendly_name} Volume Down"
id: btn_vol_down
trigger_on_initial_state : True
on_press:
then:
- lambda: |-
id(speaker_volume) = max(0.0f, id(speaker_volume) - 0.05f);
- platform: template
name: "${friendly_name} Set"
id: btn_set
trigger_on_initial_state : True
- platform: template
name: "${friendly_name} Play"
id: btn_play
trigger_on_initial_state : True
- platform: template
name: "${friendly_name} Mode"
id: btn_mode
trigger_on_initial_state : True
- platform: template
name: "${friendly_name} Record"
id: btn_record
trigger_on_initial_state : True
on_press:
- voice_assistant.start:
- light.turn_on:
id: led_ring
blue: 0%
red: 0%
green: 100%
brightness: 100%
effect: "Wakeword"
# Connection status
- platform: status
name: "${friendly_name} Status"
sensor:
- id: button_adc
platform: adc
internal: true
pin: GPIO39 # pin GPIO39 for esp32-korvo-1.1 GPIO8 for esp32-s3-korvo-1
attenuation: auto
update_interval: 15ms
filters:
- median:
window_size: 5
send_every: 5
send_first_at: 1
- delta: 0.1
on_value_range:
- below: 0.55
then:
- binary_sensor.template.publish:
id: btn_vol_up
state: ON
- above: 0.65
below: 0.92
then:
- binary_sensor.template.publish:
id: btn_vol_down
state: ON
- above: 1.02
below: 1.33
then:
- binary_sensor.template.publish:
id: btn_set
state: ON
- above: 1.43
below: 1.77
then:
- binary_sensor.template.publish:
id: btn_play
state: ON
- above: 1.87
below: 2.15
then:
- binary_sensor.template.publish:
id: btn_mode
state: ON
- above: 2.25
below: 2.56
then:
- binary_sensor.template.publish:
id: btn_record
state: ON
- above: 2.8
then:
- binary_sensor.template.publish:
id: btn_vol_up
state: OFF
- binary_sensor.template.publish:
id: btn_vol_down
state: OFF
- binary_sensor.template.publish:
id: btn_set
state: OFF
- binary_sensor.template.publish:
id: btn_play
state: OFF
- binary_sensor.template.publish:
id: btn_mode
state: OFF
- binary_sensor.template.publish:
id: btn_record
state: OFF
# Wifi signal
- platform: wifi_signal
name: "${friendly_name} WiFi Signal"
update_interval: 60s
# Generic volume sensor was used so it can be used in the esp_adf_speaker to get volume into HA
- platform: template
id: generic_volume_sensor
internal: true
update_interval: never
- platform: template
name: "${friendly_name} Volume"
accuracy_decimals: 0
id: scaled_volume_sensor
lambda: |-
static float last_volume_value = -1; // Initialize with a value outside valid range
float current_value = id(generic_volume_sensor).state / 10.0; // Scale the value
if (current_value != last_volume_value) {
last_volume_value = current_value; // Update the last value
ESP_LOGI("Volume Sensor", "Volume changed to %.1f", current_value);
return current_value; // Publish the updated value
}
return {}; // Do not publish if there's no change
update_interval: 1s
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: !secret home_assistant_api_key
ota:
- platform: !secret ota_platform
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: !secret pairing_hotspot_ssid
password: !secret pairing_hotspot_password
captive_portal:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment