Skip to content

Instantly share code, notes, and snippets.

@rmeissn
Last active August 16, 2025 07:25
Show Gist options
  • Save rmeissn/a6bc1c91f65a47cb5e37d6e2fcfa8849 to your computer and use it in GitHub Desktop.
Save rmeissn/a6bc1c91f65a47cb5e37d6e2fcfa8849 to your computer and use it in GitHub Desktop.
Onju Voice with esphome 2025.2
substitutions:
name: "onju"
friendly_name: "Onju Voice PE"
project_version: "1.1.0"
device_description: "Onju Voice Satellite with ESPHome software and microWakeWord"
wakeup_sound_url: "http://192.168.0.202:8123/local/wakeup.flac" # New Notification #7 by UNIVERSFIELD https://freesound.org/people/UNIVERSFIELD/sounds/736267/
error_sound_url: "http://192.168.0.202:8123/local/error.flac" # Error #8 by UNIVERSFIELD https://freesound.org/people/UNIVERSFIELD/sounds/734442/
timer_finished_sound_url: "http://192.168.0.202:8123/local/timer_finished.flac" # New Notification #6 by UNIVERSFIELD https://freesound.org/people/UNIVERSFIELD/sounds/734445/
mute_sound_url: "http://192.168.0.202:8123/local/mute.flac" # https://github.com/esphome/home-assistant-voice-pe/blob/dev/sounds/jack_disconnected.flac
unmute_sound_url: "http://192.168.0.202:8123/local/unmute.flac" # https://github.com/esphome/home-assistant-voice-pe/blob/dev/sounds/jack_connected.flac
knock_sound_url: "http://192.168.0.202:8123/local/knock.flac" # https://freesound.org/people/UberBosser/sounds/421585/
click_sound_url: "http://192.168.0.202:8123/local/tongue-click.flac" # https://freesound.org/people/MichellePamelaLyons/sounds/135515/
# NOTE for sounds: all sound were converted to flac, mono, 48khz (match the speaker sample_rate!)
esphome:
name: "${name}"
friendly_name: "${friendly_name}"
comment: "${device_description}"
#name_add_mac_suffix: true
project:
name: tetele.onju_voice_satellite
version: "${project_version}"
min_version: 2025.2.0
platformio_options:
board_build.flash_mode: dio
board_build.arduino.memory_type: qio_opi
on_boot:
then:
- light.turn_on:
id: top_led
effect: slow_pulse
red: 100%
green: 60%
blue: 0%
- wait_until:
condition:
wifi.connected
- light.turn_on:
id: top_led
effect: pulse
red: 0%
green: 100%
blue: 0%
- wait_until:
condition:
api.connected
- light.turn_on:
id: top_led
effect: none
red: 0%
green: 100%
blue: 0%
- delay: 1s
- script.execute: reset_led
- media_player.volume_set:
id: onju_out
volume: !lambda "return id(volume_percent);"
- lambda: id(booted) = true;
dashboard_import: # not sure this is needed at all
package_import_url: github://tetele/onju-voice-satellite/esphome/onju-voice-microwakeword.yaml@main
esp32:
board: esp32-s3-devkitc-1
variant: esp32s3
flash_size: 16MB
framework:
type: esp-idf
version: recommended
sdkconfig_options:
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y"
CONFIG_ESP32S3_DATA_CACHE_64KB: "y"
CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y"
CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB: "y"
CONFIG_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
psram:
mode: octal
speed: 80MHz
# Enable logging
logger:
#level: debug
#initial_level: debug
#logs:
# sensor: WARN
# Allow OTA updates
ota:
platform: esphome
password: "" # ADD YOUR PASSWORD HERE
# Allow provisioning Wi-Fi via serial
improv_serial:
wifi:
ssid: !secret wifi_ssid # ADD YOUR SSID HERE
password: !secret wifi_password # ADD YOUR PASSWORD HERE
#fast_connect: True # activate if you only got one access point (no mesh or similar)
enable_rrm: True
enable_btm: True
#power_save_mode: NONE
#domain: .local
ap:
ssid: "Onju"
password: !secret fallback_ap_password # ADD YOUR PASSWORD HERE
# In combination with the `ap` this allows the user
# to provision wifi credentials to the device via WiFi AP.
captive_portal:
api:
services:
- service: start_va
then:
voice_assistant.start
- service: start_va_continuous
then:
voice_assistant.start_continuous
- service: stop_va
then:
voice_assistant.stop
- service: notification_on
then:
- script.execute: turn_on_notification
- service: notification_clear
then:
- script.execute: clear_notification
globals:
- 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: booted # new
type: bool
restore_value: false
- id: mic_off # new
type: bool
restore_value: false
- id: internal_flicker # new
type: bool
restore_value: false
- id: volume_change # new
type: bool
restore_value: false
- id: need_reply
type: bool
restore_value: no
initial_value: 'false'
- id: volume_percent
type: float
initial_value: "0.5"
restore_value: true
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
i2s_audio:
#- id: i2s_in
# i2s_lrclk_pin:
# number: GPIO13 # WS / LRCLK
# allow_other_uses: true
# i2s_bclk_pin:
# number: GPIO18 # SCK / BCLK
# allow_other_uses: true
#- id: i2s_out
# i2s_lrclk_pin:
# number: GPIO13 # WS / LRCLK
# allow_other_uses: true
# i2s_bclk_pin:
# number: GPIO18 # SCK / BCLK
# allow_other_uses: true
- id: i2s_shared
i2s_lrclk_pin: GPIO13 # WS / LRCLK
i2s_bclk_pin: GPIO18 # SCK / BCLK
microphone:
- platform: nabu_microphone
id: nabu_mic
i2s_din_pin: GPIO17 # SDI
adc_type: external
use_apll: true
pdm: false
sample_rate: 16000 # mic supports 16kHz to 64kHz, captures approx. ~45Hz to ~15kHz -> 16kHz to 32kHz is sufficient, mww and va need 16kHz
bits_per_sample: 32bit # mic only supports 24 bits, TODO: test 16 (implemented) or 24 bit (not implemented?)
i2s_mode: primary
#i2s_audio_id: i2s_in
i2s_audio_id: i2s_shared
channel_0: # e.g. left
id: onju_microphone
amplify_shift: 3 # 0 to 8, higher is better, because it will produce louder signals, that might clip, 3 seems the highest possible value
channel_1: # e.g. right
id: mww_microphone
amplify_shift: 3 # 0 to 8, higher is better, because it will produce louder signals, that might clip, 3 seems the highest possible value
speaker:
# Hardware speaker output
- platform: i2s_audio
id: i2s_audio_speaker
#i2s_mode: primary
sample_rate: 48000 # DAC supports 8kHz to 96kHz, TODO: test 44.1kHz
bits_per_sample: 32bit # DAC supports 16/24/32 bit, TODO: set to 16 or 24? (resampler outputs 16 bit)
use_apll: true
#i2s_audio_id: i2s_out
i2s_audio_id: i2s_shared
dac_type: external
i2s_dout_pin: GPIO12 # SDO / Din
channel: left
timeout: never
buffer_duration: 100ms # default 500ms
- platform: mixer
id: mixing_speaker
output_speaker: i2s_audio_speaker
num_channels: 1
#task_stack_in_psram: true
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: 48000 # NOTE: must be same as for speaker
bits_per_sample: 16 # NOTE: there will never arrive 32 bit at the speaker itself
#task_stack_in_psram: true
- platform: resampler
id: media_resampling_speaker
output_speaker: media_mixing_input
sample_rate: 48000 # NOTE: must be same as for speaker
bits_per_sample: 16 # NOTE: there will never arrive 32 bit at the speaker itself
#task_stack_in_psram: true
media_player:
- platform: speaker
id: onju_out
name: Media Player
internal: False
volume_increment: 0.05
volume_min: 0.2
volume_max: 0.85
#task_stack_in_psram: true
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: 48000 # 48kHz for audio quality. TODO: 44.1khz is also sufficient? Must be same as for speaker
media_pipeline:
speaker: media_resampling_speaker
format: FLAC # FLAC is the least processor intensive codec
num_channels: 1 # Onju only got one speaker
sample_rate: 48000 # 48kHz for audio quality. TODO: 44.1khz is also sufficient? Must be same as for speaker
on_announcement:
- lambda: id(nabu_mic).stop();
- mixer_speaker.apply_ducking:
id: media_mixing_input
decibel_reduction: 20
duration: 0.0s # duck now
on_state:
then:
- lambda: |-
static float old_volume = -1;
float new_volume = id(onju_out).volume;
if(abs(new_volume-old_volume) > 0.0001) {
if(old_volume != -1) {
id(volume_change) = true;
id(show_volume)->execute();
}
}
old_volume = new_volume;
id(volume_percent) = old_volume;
- if: # reset ducking only of va is not active
condition:
and:
- not:
voice_assistant.is_running:
- not:
media_player.is_announcing:
then:
- mixer_speaker.apply_ducking:
id: media_mixing_input
decibel_reduction: 0 # stop ducking
duration: 1.0s # over 1s
on_play: # not called for announcements
- lambda: id(internal_flicker) = false; # needed to deactivate flicker on audio playback
- script.execute: reset_led # TODO: causes the volume_show to "fail" (not shown)
- lambda: id(nabu_mic).stop();
on_pause: # speaker is auto-restarted if something is paused, causing the mic to fail -> stop on wakeword detection
- lambda: id(internal_flicker) = true; # needed to activate flicker on pause
- script.execute: reset_led # TODO: causes the volume_show to "fail" (not shown)
- script.execute: stop_speaker_start_microphone
- script.wait: stop_speaker_start_microphone
on_idle: # also called after announcement finished, is triggered on volume change
- if: # TODO: should also be included at pause and play. Is there a better alternative?
condition:
not:
lambda: return id(volume_change);
then:
- lambda: id(internal_flicker) = true; # needed to activate flicker on idle
- script.execute: reset_led
- if: # stop destroying speaker if media_player plays music
condition:
- not:
media_player.is_playing
then:
# TODO if use ww and not muted
- script.execute: stop_speaker_start_microphone
- script.wait: stop_speaker_start_microphone
files:
- id: wakeup
file: "${wakeup_sound_url}"
- id: error
file: "${error_sound_url}"
- id: mute
file: "${mute_sound_url}"
- id: unmute
file: "${unmute_sound_url}"
- id: knock
file: "${knock_sound_url}"
#- id: click # TODO: Adding this causes the ota partition to fail (too small)
# file: "${click_sound_url}"
- id: timer_finished
file: "${timer_finished_sound_url}"
external_components:
# https://github.com/esphome/esphome/pull/7802 might be interesting
- source:
type: git
url: https://github.com/formatBCE/home-assistant-voice-pe
ref: 817efa65bdaa407050830c54a3161021628b8560 # before continue conversation commit
components:
- micro_wake_word
- microphone
- nabu_microphone
- voice_assistant
refresh: 0s
#- source: # only needed if mic is on 48kHz
# type: git
# url: https://github.com/formatBCE/home-assistant-voice-pe
# ref: 48kHz_mic_support
# components:
# - nabu_microphone
# refresh: 0s
micro_wake_word: # requires 16khz input currently
id: mww
models:
- model: https://github.com/kahrendt/microWakeWord/releases/download/okay_nabu_20241226.3/okay_nabu.json
id: okay_nabu
#probability_cutoff: 0.8 # TODO tune cutoff to the onju
vad:
microphone: mww_microphone
on_wake_word_detected:
- if:
condition:
- switch.is_on: use_wake_word # ignore detection if switch is on
then:
- if: # media_player needs to be stopped to not trigger on_idle too often
condition:
media_player.is_paused
then:
- media_player.stop
- delay: 300ms
- media_player.speaker.play_on_device_media_file: # when MA is paused, any play event keeps the speaker active -> stop media player if it is paused (see above)
media_file: wakeup
announcement: true
- wait_until: media_player.is_announcing
- wait_until:
or:
- media_player.is_idle
- media_player.is_paused
- voice_assistant.start:
wake_word: !lambda return wake_word;
voice_assistant: # requires 16khz input currently (for vad?)
id: va
microphone: onju_microphone
media_player: onju_out
micro_wake_word: mww
use_wake_word: false
noise_suppression_level: 0 # this is done at the ha side and for recorded audio (activate debug mode to see settings), maybe buggy
auto_gain: 31 dbfs # this is done at the ha side and for recorded audio (activate debug mode to see settings), maybe buggy
volume_multiplier: 3 # this is done at the ha side and for recorded audio (activate debug mode to see settings), maybe buggy
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) -> duck now
on_listening:
- light.turn_on:
id: top_led
blue: 100%
red: 100%
green: 100%
brightness: 100%
effect: listening
on_stt_vad_end:
- light.turn_on:
id: top_led
blue: 100%
red: 0%
green: 20%
brightness: 70%
effect: processing
on_tts_start:
- lambda: if(x.ends_with("?")){ id(need_reply) = true; } # Check if va needs a reply
on_tts_end:
- light.turn_on:
id: top_led
blue: 0%
red: 20%
green: 100%
effect: speaking
on_end:
- wait_until:
not:
voice_assistant.is_running
- if:
condition:
lambda: return id(need_reply);
then:
- lambda: id(need_reply) = false; # reset
- voice_assistant.stop
- media_player.speaker.play_on_device_media_file:
media_file: wakeup
announcement: true
- wait_until: media_player.is_announcing
- wait_until:
or:
- media_player.is_idle
- media_player.is_paused
- voice_assistant.start
else:
- mixer_speaker.apply_ducking: # Stop ducking audio.
id: media_mixing_input
decibel_reduction: 0
duration: 1.0s # duck over 1s
- script.execute: reset_led
on_timer_started:
- light.turn_on:
id: top_led
effect: random_twinkle
on_timer_finished:
- media_player.speaker.play_on_device_media_file:
media_file: timer_finished
announcement: true
- light.turn_on:
id: top_led
blue: 0%
red: 100%
green: 80%
effect: slow_pulse
- delay: 5s
- script.execute: reset_led
on_client_connected:
- if:
condition:
and:
- switch.is_on: use_wake_word
- binary_sensor.is_off: mute_switch
then:
#- lambda: id(nabu_mic).start();
- micro_wake_word.start
on_client_disconnected:
- if:
condition:
and:
- switch.is_on: use_wake_word
- binary_sensor.is_off: mute_switch
then:
- voice_assistant.stop
- micro_wake_word.stop
#- lambda: id(nabu_mic).stop();
on_error:
- media_player.speaker.play_on_device_media_file:
media_file: error
announcement: true
- light.turn_on:
id: top_led
blue: 0%
red: 100%
green: 0%
effect: none
- delay: 1s
- script.execute: reset_speaker_microphone
- script.execute: reset_led
number:
- platform: template
name: "Touch threshold percentage"
id: touch_threshold_percentage
icon: mdi:gesture-tap
update_interval: never
entity_category: config
initial_value: 0.75
min_value: 0.25
max_value: 5
step: 0.05
optimistic: true
on_value:
then:
- lambda: !lambda |-
id(thresh_percent) = 0.01 * x;
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
button:
- platform: restart
id: restart_button
name: "Restart"
entity_category: config
disabled_by_default: true
icon: "mdi:restart"
#sensor: # TODO: causes the esp to crash on boot
# - platform: internal_temperature
# name: "ESP Temperature"
# entity_category: "diagnostic"
# update_interval: 60s
binary_sensor:
- platform: template
id: conversation_mode
name: "Conversation Mode"
icon: mdi:forum
disabled_by_default: true
- platform: esp32_touch
id: volume_down
name: "VOL-"
icon: mdi:volume-minus
disabled_by_default: true
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
name: "VOL+"
icon: mdi:volume-plus
disabled_by_default: true
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_multi_click:
- timing: # double click
- ON for at most 0.5s
- OFF for at most 0.5s
- ON for at most 0.5s
- OFF for at least 0.25s
then:
- if:
condition:
media_player.is_playing
then:
- media_player.pause
- media_player.speaker.play_on_device_media_file:
media_file: knock
announcement: true
- wait_until: media_player.is_announcing
- wait_until: media_player.is_idle
- micro_wake_word.stop
- binary_sensor.template.publish:
id: conversation_mode
state: ON
- delay: 50ms
- voice_assistant.start_continuous # TODO causes the mic to stop for some reason after mode was ended by user, buggy!
- timing: # single click
- ON for at most 1s
- OFF for at least 0.5s
then:
- if:
condition:
media_player.is_announcing
then:
- media_player.stop:
announcement: true
- if:
condition:
voice_assistant.is_running
then:
- voice_assistant.stop
- binary_sensor.template.publish:
id: conversation_mode
state: OFF
else:
- if: # switch between pause/play
condition:
media_player.is_playing
then:
- media_player.pause:
else:
- if:
condition:
media_player.is_paused
then:
- media_player.play
else:
- if: # if not paused, activate va
condition:
and:
- not:
voice_assistant.is_running
- lambda: return id(booted);
- not:
binary_sensor.is_on: mute_switch
then:
- media_player.speaker.play_on_device_media_file:
media_file: wakeup
announcement: true
- wait_until: media_player.is_announcing
- wait_until: media_player.is_idle
- delay: 50ms
- voice_assistant.start
- timing: # long press, reset everything TODO: still a bit buggy
- ON for 1s to 3s
- OFF for at least 0.25s
then:
- voice_assistant.stop
- micro_wake_word.stop
- media_player.stop
- mixer_speaker.apply_ducking: # Stop ducking audio.
id: media_mixing_input
decibel_reduction: 0
duration: 0.0s # duck over 1s
- lambda: id(mic_off) = true;
- media_player.speaker.play_on_device_media_file:
media_file: knock
announcement: true
- wait_until: media_player.is_announcing
- wait_until: media_player.is_idle
- media_player.speaker.play_on_device_media_file:
media_file: knock
announcement: true
- wait_until: media_player.is_announcing
- lambda: id(mic_off) = false;
- wait_until: media_player.is_idle
- binary_sensor.template.publish:
id: conversation_mode
state: OFF
- script.execute: reset_led
- script.wait: reset_led
- script.execute: reset_speaker_microphone
- script.wait: reset_speaker_microphone
#- lambda: id(nabu_mic).start();
- micro_wake_word.start
- platform: gpio
id: mute_switch
icon: mdi:microphone-message-off
pin:
number: GPIO38
mode: INPUT_PULLUP
name: "Muted (Hardware Switch)"
on_press:
- media_player.speaker.play_on_device_media_file:
media_file: mute
announcement: true
- wait_until: media_player.is_announcing
- wait_until: media_player.is_idle
- script.execute: turn_off_wake_word
on_release:
- media_player.speaker.play_on_device_media_file:
media_file: unmute
announcement: true
- wait_until: media_player.is_announcing
- wait_until: media_player.is_idle
- script.execute: turn_on_wake_word
light:
- platform: esp32_rmt_led_strip
id: leds
pin: GPIO11
chipset: SK6812
num_leds: 6
rgb_order: GRB
default_transition_length: 0s
gamma_correct: 2.8
- platform: partition
id: left_led
segments:
- id: leds
from: 0
to: 0
default_transition_length: 100ms
- platform: partition
id: top_led
segments:
- id: leds
from: 1
to: 4
default_transition_length: 100ms
effects:
- pulse:
name: pulse
transition_length: 250ms
update_interval: 250ms
- pulse:
name: slow_pulse
transition_length: 1s
update_interval: 2s
- addressable_lambda:
name: show_volume
update_interval: 50ms
lambda: |-
int int_volume = int(id(onju_out).volume * 100.0f * it.size());
int full_leds = int_volume / 100;
int last_brightness = int_volume % 100;
int i = 0;
for(; i < full_leds; i++) {
it[i] = Color::WHITE;
}
if(i < 4) {
it[i++] = Color(64, 64, 64).fade_to_white(last_brightness*256/100);
}
for(; i < it.size(); i++) {
it[i] = Color(64, 64, 64);
}
- addressable_twinkle:
name: listening_ww
twinkle_probability: 1%
- addressable_twinkle:
name: listening
twinkle_probability: 45%
- addressable_scan:
name: processing
move_interval: 80ms
- addressable_twinkle: # changed from default onju
name: speaking
twinkle_probability: 45%
- addressable_random_twinkle:
name: random_twinkle
twinkle_probability: 45%
- platform: partition
id: right_led
segments:
- id: leds
from: 5
to: 5
default_transition_length: 100ms
script:
- id: reset_led
then:
- if:
condition:
- lambda: return id(notification);
then:
- light.turn_on:
id: top_led
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
- lambda: return id(internal_flicker);
- binary_sensor.is_off: mute_switch
then:
- if:
condition:
- binary_sensor.is_off: conversation_mode
then:
- light.turn_on:
id: top_led
blue: 100%
red: 0%
green: 100%
brightness: 60%
effect: listening_ww
else:
- light.turn_on:
id: top_led
blue: 0%
red: 100%
green: 100%
brightness: 60%
effect: listening_ww
else:
- light.turn_off: top_led
- 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: reset_speaker_microphone # new
then:
- lambda: id(i2s_audio_speaker).stop();
- lambda: id(nabu_mic).stop();
- delay: 250ms
- lambda: id(nabu_mic).start();
- id: stop_speaker_start_microphone # new, tuned timings
then:
- if:
condition:
not:
- lambda: return id(mic_off);
then:
- delay: 150ms
- lambda: id(i2s_audio_speaker).stop();
# TODO if use ww and not muted
- delay: 150ms
- lambda: id(nabu_mic).start();
- id: set_volume
mode: restart
parameters:
volume: float
then:
- media_player.volume_set:
id: onju_out
volume: !lambda return clamp(id(onju_out).volume+volume, 0.0f, 1.0f);
- id: show_volume
mode: restart
then:
- light.turn_on:
id: top_led
effect: show_volume
- delay: 1s
- lambda: id(volume_change) = false;
- script.execute: reset_led
- id: turn_on_wake_word
then:
- if:
condition:
and:
- binary_sensor.is_off: mute_switch
- switch.is_on: use_wake_word
then:
#- lambda: id(nabu_mic).start();
- micro_wake_word.start
- lambda: id(internal_flicker) = true;
- delay: 50ms
- script.execute: reset_led
else:
- logger.log:
tag: "turn_on_wake_word"
format: "Trying to start listening for wake word, but %s"
args:
[
'id(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
- delay: 250ms
#- lambda: id(nabu_mic).stop();
- lambda: id(internal_flicker) = false;
- 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;
}
switch:
- platform: template
name: Use Wake Word
id: use_wake_word # TODO: if reenabled from off, the mic crahses, buggy!
icon: mdi:microphone-message
entity_category: config
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
icon: mdi:microphone-settings
entity_category: config
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- lambda: id(internal_flicker) = true;
- script.execute: reset_led
on_turn_off:
- lambda: id(internal_flicker) = false;
- script.execute: reset_led
- platform: gpio
id: dac_mute
icon: mdi:volume-off
restore_mode: ALWAYS_OFF
pin:
number: GPIO21
inverted: True
@GegoSK
Copy link

GegoSK commented Aug 14, 2025

Hi,
I was suprised when I found new yaml for onju-voice. My old code (from last year) is not working properly anymore. But in yours, there is a lot of errors. I tried to solve it, but no luck. Any chance to check the yaml, please? Thank you!
Gego

@rmeissn
Copy link
Author

rmeissn commented Aug 14, 2025

My config is for ESPHome 2025.02.x . More up to date versions of ESPHome changed some of these components, which is probably giving you these errors. Currently, I got no time to update my yaml to the latest ESPHome version.
You can try https://github.com/s00500/onjuconfig/blob/master/nabu_magic.yml with ESPHome 2025.05.x. At least I read that this yaml is supposed to work.

@GegoSK
Copy link

GegoSK commented Aug 16, 2025

Thank you for link! Your yaml looks very useful. Hope you will find time in future!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment