Last active
July 10, 2025 07:48
-
-
Save rmeissn/a6bc1c91f65a47cb5e37d6e2fcfa8849 to your computer and use it in GitHub Desktop.
Onju Voice with esphome 2025.2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment