Created
March 10, 2025 09:02
-
-
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: 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 or 24 bit | |
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? | |
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 | |
- 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 # NOTE: 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 # NOTE: 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: | |
- 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: dev | |
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: | |
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_end: | |
- light.turn_on: | |
id: top_led | |
blue: 0% | |
red: 20% | |
green: 100% | |
effect: speaking | |
on_end: | |
- wait_until: | |
not: | |
voice_assistant.is_running | |
- 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" | |
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, 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 | |
- 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: 125ms | |
- lambda: id(i2s_audio_speaker).stop(); | |
- delay: 125ms | |
- 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