substitutions: name: esphome-web-0bac48 friendly_name: "Xiaozhi Ball V2" ## v1.07 26-jul-2025 ############################################################################################################################# ## changed how to play startup sound to avoid double triggering ## v1.06 19-jul-2025 ############################################################################################################################# ## Added: Startup sound when connected to HA, optional with switch, and option to select other sounds in settings below. ## Added: Mute and Playing icon to all emos. ## v1.05 18-jul-2025 ############################################################################################################################# ## Fixes: minor bugfixes, sound level to max, show muted mic when playing music or playing TTS, not when playing internal sounds. ## v1.04 13-jul-2025 ############################################################################################################################# ## Added: optional wake sound, delays the time before listening, so optional switch in HA. ## Moved: show_text and show_battery_status to switches in HA. ## v1.03 08-jul-2025 ############################################################################################################################# ## Added: optional show text boxes ## v1.02 30-jun-2025 ############################################################################################################################# ## Added optional Battery Status ## SETTINGS ###################################################################################################################################### imagemodel: "Harley" # (options are: Alfred,Astrobot,Buzz,Casita,Cybergirl,Dory,EVE,Eyes,Eyes2,GLaDOS,Girl1,Guy1,Guy2,Gwen,HA-character,Harley,Jarvis,Luffy,Mario,Max,Prime,Robochibi,Robocop,Robot,Robotgirl,Shaun) startup_sound: "Home_Connected" # (options are: available,Home_Connected,Computer_Ready) imagewidth: "240" # GC9A01A (Ball v2 & Muma & Puck) "240" imageheight: "240" # GC9A01A (Ball v2 & Muma & Puck) "240" displaymodel: "GC9A01A" # GC9A01A (Ball v2 & Puck) or ST7789V (Muma) invertcolors: "true" # GC9A01A/ST7789V (Ball v2 & Muma & Puck) "true" ################################################################################################################################################## # Hardware v2 pin mappings sda_pin_bus_a: "15" # I2C Bus A SDA scl_pin_bus_a: "14" # I2C BUS A SCL sda_pin_bus_b: "11" # I2C Bus B SDA scl_pin_bus_b: "7" # I2C BUS B SCL i2s_lrclk_pin: "45" # I2S LRCLK (Word Select) i2s_bclk_pin: "9" # I2S BCLK (Bit Clock) i2s_mclk_pin: "16" # I2S MCLK (Master Clock) i2s_din_pin: "10" # I2S Data In (Mic) i2s_dout_pin: "8" # I2S Data Out (Speaker) speaker_enable_pin: "46" # Speaker Enable touch_input_pin: "12" # Touch interrupt touch_reset_pin: "6" # Touch Reset backlight_output_pin: "42" # Display Backlight lcd_cs_pin: "5" # Display CS (Chip Select) lcd_dc_pin: "47" # Display DC (Data/Command) lcd_reset_pin: "38" # Display Reset spi_clk_pin: "4" # SPI Clock spi_mosi_pin: "2" # SPI MOSI (Data Out) left_top_button_pin: "0" # Main Button led_pin: "48" # RGB LED (WS2812) battery_adc_pin: "1" # Battery Voltage ADC ################################################################################################################################################## loading_illustration_file: https://github.com/RealDeco/xiaozhi-esphome/raw/main/images/${imagemodel}/${imagewidth}x${imageheight}/loading.png idle_illustration_file: https://github.com/RealDeco/xiaozhi-esphome/raw/main/images/${imagemodel}/${imagewidth}x${imageheight}/idle.png listening_illustration_file: https://github.com/RealDeco/xiaozhi-esphome/raw/main/images/${imagemodel}/${imagewidth}x${imageheight}/listening.png thinking_illustration_file: https://github.com/RealDeco/xiaozhi-esphome/raw/main/images/${imagemodel}/${imagewidth}x${imageheight}/thinking.png replying_illustration_file: https://github.com/RealDeco/xiaozhi-esphome/raw/main/images/${imagemodel}/${imagewidth}x${imageheight}/replying.png error_illustration_file: https://github.com/RealDeco/xiaozhi-esphome/raw/main/images/${imagemodel}/${imagewidth}x${imageheight}/error.png timer_finished_illustration_file: https://github.com/RealDeco/xiaozhi-esphome/raw/main/images/${imagemodel}/${imagewidth}x${imageheight}/timer_finished.png mute_illustration_file: https://github.com/RealDeco/xiaozhi-esphome/raw/main/images/${imagemodel}/${imagewidth}x${imageheight}/mute.png startup_sound_file: https://github.com/RealDeco/xiaozhi-esphome/raw/main/sounds/${startup_sound}.flac loading_illustration_background_color: "000000" idle_illustration_background_color: "000000" listening_illustration_background_color: "000000" thinking_illustration_background_color: "000000" replying_illustration_background_color: "000000" error_illustration_background_color: "000000" 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" voice_assist_timer_finished_phase_id: "20" allowed_characters: " !#%'()+,-./0123456789:;<>?@ABCDEFGHIJKLMNOPQRSTUVWYZ[]_abcdefghijklmnopqrstuvwxyz{|}°²³µ¿ÁÂÄÅÉÖÚßàáâãäåæçèéêëìíîðñòóôõöøùúûüýþāăąćčďĐđēėęěğĮįıļľŁłńňőřśšťũūůűųźŻżŽžơưșțΆΈΌΐΑΒΓΔΕΖΗΘΚΜΝΠΡΣΤΥΦάέήίαβγδεζηθικλμνξοπρςστυφχψωϊόύώАБВГДЕЖЗИКЛМНОПРСТУХЦЧШЪЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяёђєіїјљњћאבגדהוזחטיכלםמןנסעפץצקרשת،ءآأإئابةتجحخدذرزسشصضطظعغفقكلمنهوىيٹپچڈکگںھہیےংকচতধনফবযরলশষস়ািু্చయలిెొ్ംഅആഇഈഉഎഓകഗങചജഞടഡണതദധനപഫബഭമയരറലളവശസഹാിീുൂെേൈ്ൺൻർൽൾაბგდევზთილმნოპრსტუფქყშჩცძჭხạảấầẩậắặẹẽếềểệỉịọỏốồổỗộớờởợụủứừửữựỳ—、一上不个中为主乾了些亮人任低佔何作供依侧係個側偵充光入全关冇冷几切到制前動區卧厅厨及口另右吊后吗启吸呀咗哪唔問啟嗎嘅嘛器圍在场執場外多大始安定客室家密寵对將小少左已帘常幫幾库度庫廊廚廳开式後恆感態成我戲戶户房所扇手打执把拔换掉控插摄整斯新明是景暗更最會有未本模機檯櫃欄次正氏水沒没洗活派温測源溫漏潮激濕灯為無煙照熱燈燥物狀玄现現瓦用發的盞目着睡私空窗立笛管節簾籬紅線红罐置聚聲脚腦腳臥色节著行衣解設調請謝警设调走路車车运連遊運過道邊部都量鎖锁門閂閉開關门闭除隱離電震霧面音頂題顏颜風风食餅餵가간감갔강개거게겨결경고공과관그금급기길깥꺼껐꼽나난내네놀누는능니다닫담대더데도동됐되된됨둡드든등디때떤뜨라래러렇렌려로료른를리림링마많명몇모무문물뭐바밝방배변보부불블빨뽑사산상색서설성세센션소쇼수스습시신실싱아안않알았애야어얼업없었에여연열옆오온완외왼요운움워원위으은을음의이인일임입있작잠장재전절정제져조족종주줄중줘지직진짐쪽차창천최추출충치침커컴켜켰쿠크키탁탄태탬터텔통트튼티파팬퍼폰표퓨플핑한함해했행혀현화활후휴힘,?" font_glyphsets: "GF_Latin_Core" font_family: Figtree esphome: name: ${name} friendly_name: ${friendly_name} min_version: 2025.5.0 name_add_mac_suffix: false on_boot: priority: 600 then: - script.execute: draw_display - component.update: battery_voltage - component.update: battery_percentage - delay: 30s - if: condition: lambda: return id(init_in_progress); then: - lambda: id(init_in_progress) = false; - script.execute: draw_display esp32: board: esp32-s3-devkitc-1 flash_size: 16MB cpu_frequency: 240MHz framework: type: esp-idf sdkconfig_options: CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y" CONFIG_ESP32S3_DATA_CACHE_64KB: "y" CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y" psram: mode: octal speed: 80MHz api: on_client_connected: - script.execute: draw_display - lambda: |- if (!id(boot_sound_played)) { id(boot_sound_played) = true; if (id(startup_sound_switch).state) { id(play_sound)->execute(true, id(ready_sound)); } } on_client_disconnected: - script.execute: draw_display ota: - platform: esphome id: ota_esphome logger: hardware_uart: USB_SERIAL_JTAG wifi: ssid: !secret wifi_ssid password: !secret wifi_password ap: ssid: "Ball v2 Hotspot" password: "Rijnstraat214" on_connect: - script.execute: draw_display on_disconnect: - script.execute: draw_display captive_portal: button: - platform: factory_reset id: factory_reset_btn internal: true sensor: - platform: adc pin: GPIO${battery_adc_pin} name: "Battery Voltage" id: battery_voltage attenuation: 12db accuracy_decimals: 2 update_interval: 1s unit_of_measurement: "V" icon: mdi:battery-medium filters: - multiply: 2.0 - median: window_size: 7 send_every: 7 send_first_at: 7 - throttle: 1min on_value: then: - component.update: battery_percentage - platform: template id: battery_percentage name: "Battery Percentage" lambda: return id(battery_voltage).state; accuracy_decimals: 0 unit_of_measurement: "%" icon: mdi:battery-medium filters: - calibrate_linear: method: exact datapoints: - 2.80 -> 0.0 - 3.10 -> 10.0 - 3.30 -> 20.0 - 3.45 -> 30.0 - 3.60 -> 40.0 - 3.70 -> 50.0 - 3.75 -> 60.0 - 3.80 -> 70.0 - 3.90 -> 80.0 - 4.00 -> 90.0 - 4.20 -> 100.0 - lambda: |- if (x > 100) return 100; if (x < 0) return 0; return x; on_value: then: - lambda: |- int current = (int)x; if (current != id(last_battery_percent)) { id(last_battery_percent) = current; id(draw_display).execute(); } touchscreen: - platform: cst816 i2c_id: bus_b interrupt_pin: ${touch_input_pin} reset_pin: ${touch_reset_pin} id: touch_dp on_touch: then: - binary_sensor.template.publish: id: touch_input state: ON on_release: then: - binary_sensor.template.publish: id: touch_input state: OFF binary_sensor: - platform: template name: "Touch Button" id: touch_input on_multi_click: # Single tap - timing: - ON for 40ms to 400ms - OFF for at least 330ms then: - if: condition: switch.is_on: timer_ringing then: - switch.turn_off: timer_ringing else: - if: condition: lambda: return id(wake_word_engine_location).state == "On device"; then: - if: condition: not: voice_assistant.is_running: then: - voice_assistant.start: else: - voice_assistant.stop: - timing: - ON for at least 1s then: - switch.toggle: show_battery_status - script.execute: draw_display - platform: gpio pin: number: GPIO${left_top_button_pin} mode: INPUT_PULLUP ignore_strapping_warning: true inverted: true id: left_top_button internal: true on_multi_click: - timing: - ON for at least 50ms - OFF for at least 50ms then: - if: condition: switch.is_on: timer_ringing then: - switch.turn_off: timer_ringing else: - if: condition: lambda: return id(wake_word_engine_location).state == "On device"; then: - if: condition: not: voice_assistant.is_running: then: - voice_assistant.start: else: - voice_assistant.stop: - timing: - ON for at least 10s then: - button.press: factory_reset_btn output: - platform: ledc pin: GPIO${backlight_output_pin} id: backlight_output inverted: true light: - platform: monochromatic id: Sled name: Screen icon: "mdi:television" entity_category: config output: backlight_output restore_mode: ALWAYS_ON default_transition_length: 250ms - platform: esp32_rmt_led_strip id: led name: none disabled_by_default: false entity_category: config pin: GPIO${led_pin} default_transition_length: 0s chipset: WS2812 num_leds: 1 rgb_order: grb effects: - pulse: name: "Slow Pulse" transition_length: 250ms update_interval: 250ms min_brightness: 50% max_brightness: 100% - pulse: name: "Fast Pulse" transition_length: 100ms update_interval: 100ms min_brightness: 50% max_brightness: 100% i2c: - id: bus_a sda: GPIO${sda_pin_bus_a} scl: GPIO${scl_pin_bus_a} scan: true - id: bus_b sda: GPIO${sda_pin_bus_b} scl: GPIO${scl_pin_bus_b} scan: true i2s_audio: - id: i2s_audio_bus i2s_lrclk_pin: GPIO${i2s_lrclk_pin} i2s_bclk_pin: GPIO${i2s_bclk_pin} i2s_mclk_pin: GPIO${i2s_mclk_pin} audio_dac: - platform: es8311 i2c_id: bus_a id: es8311_dac bits_per_sample: 16bit sample_rate: 16000 microphone: - platform: i2s_audio id: i2s_mics sample_rate: 16000 i2s_din_pin: GPIO${i2s_din_pin} bits_per_sample: 16bit adc_type: external channel: left speaker: - platform: i2s_audio id: i2s_audio_speaker i2s_dout_pin: GPIO${i2s_dout_pin} dac_type: external sample_rate: 16000 bits_per_sample: 16bit channel: left audio_dac: es8311_dac buffer_duration: 100ms media_player: - platform: speaker name: None id: external_media_player announcement_pipeline: speaker: i2s_audio_speaker format: FLAC sample_rate: 16000 num_channels: 1 # S3 Box only has one output channel files: - id: timer_finished_sound file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/timer_finished.flac - id: wake_word_triggered_sound file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/wake_word_triggered.flac - id: ready_sound file: ${startup_sound_file} on_announcement: - if: condition: - microphone.is_capturing: then: - script.execute: stop_wake_word - if: condition: - lambda: return id(wake_word_engine_location).state == "In Home Assistant"; then: - wait_until: - not: voice_assistant.is_running: - if: condition: not: voice_assistant.is_running: then: - if: condition: lambda: 'return id(playing_internal_sound);' then: - lambda: 'id(playing_internal_sound) = false;' else: - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; - script.execute: draw_display on_idle: - if: condition: not: voice_assistant.is_running: then: - script.execute: start_wake_word - script.execute: set_idle_or_mute_phase - script.execute: draw_display micro_wake_word: id: mww models: - okay_nabu - hey_jarvis - alexa on_wake_word_detected: - if: condition: switch.is_on: wake_sound then: - script.execute: id: play_sound priority: true sound_file: !lambda return id(wake_word_triggered_sound); - delay: 300ms - voice_assistant.start: wake_word: !lambda return wake_word; voice_assistant: id: va microphone: i2s_mics media_player: external_media_player micro_wake_word: mww noise_suppression_level: 2 auto_gain: 31dBFS volume_multiplier: 2.0 on_listening: - lambda: id(voice_assistant_phase) = ${voice_assist_listening_phase_id}; - text_sensor.template.publish: id: text_request state: "..." - text_sensor.template.publish: id: text_response state: "..." - script.execute: draw_display on_stt_vad_end: - lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id}; - script.execute: draw_display on_stt_end: - text_sensor.template.publish: id: text_request state: !lambda return x; - script.execute: draw_display on_tts_start: - text_sensor.template.publish: id: text_response state: !lambda return x; - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; - script.execute: draw_display on_end: - wait_until: condition: - media_player.is_announcing: timeout: 0.5s - wait_until: - and: - not: media_player.is_announcing: - not: speaker.is_playing: - if: condition: - lambda: return id(wake_word_engine_location).state == "On device"; then: - lambda: id(va).set_use_wake_word(false); - micro_wake_word.start: - script.execute: set_idle_or_mute_phase - script.execute: draw_display - text_sensor.template.publish: id: text_request state: "" - text_sensor.template.publish: id: text_response state: "" on_error: - if: condition: lambda: return !id(init_in_progress); then: - lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id}; - script.execute: draw_display - delay: 1s - 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: draw_display on_client_connected: - lambda: id(init_in_progress) = false; - script.execute: start_wake_word - script.execute: set_idle_or_mute_phase - script.execute: draw_display on_client_disconnected: - script.execute: stop_wake_word - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id}; - script.execute: draw_display on_timer_started: - script.execute: draw_display on_timer_cancelled: - script.execute: draw_display on_timer_updated: - script.execute: draw_display on_timer_tick: - script.execute: draw_display on_timer_finished: - switch.turn_on: timer_ringing - wait_until: media_player.is_announcing: - lambda: id(voice_assistant_phase) = ${voice_assist_timer_finished_phase_id}; - script.execute: draw_display script: - id: draw_display then: - if: condition: lambda: return !id(init_in_progress); then: - if: condition: wifi.connected: then: - if: condition: api.connected: then: - lambda: | switch(id(voice_assistant_phase)) { case ${voice_assist_listening_phase_id}: id(s3_box_lcd).show_page(listening_page); id(s3_box_lcd).update(); break; case ${voice_assist_thinking_phase_id}: id(s3_box_lcd).show_page(thinking_page); id(s3_box_lcd).update(); break; case ${voice_assist_replying_phase_id}: id(s3_box_lcd).show_page(replying_page); id(s3_box_lcd).update(); break; case ${voice_assist_error_phase_id}: id(s3_box_lcd).show_page(error_page); id(s3_box_lcd).update(); break; case ${voice_assist_muted_phase_id}: id(s3_box_lcd).show_page(muted_page); id(s3_box_lcd).update(); break; case ${voice_assist_not_ready_phase_id}: id(s3_box_lcd).show_page(no_ha_page); id(s3_box_lcd).update(); break; case ${voice_assist_timer_finished_phase_id}: id(s3_box_lcd).show_page(timer_finished_page); id(s3_box_lcd).update(); break; default: id(s3_box_lcd).show_page(idle_page); id(s3_box_lcd).update(); } else: - display.page.show: no_ha_page - component.update: s3_box_lcd else: - display.page.show: no_wifi_page - component.update: s3_box_lcd else: - display.page.show: initializing_page - component.update: s3_box_lcd - id: fetch_first_active_timer then: - lambda: | const auto timers = id(va).get_timers(); auto output_timer = timers.begin()->second; for (auto &iterable_timer : timers) { if (iterable_timer.second.is_active && iterable_timer.second.seconds_left <= output_timer.seconds_left) { output_timer = iterable_timer.second; } } id(global_first_active_timer) = output_timer; - id: check_if_timers_active then: - lambda: | const auto timers = id(va).get_timers(); bool output = false; if (timers.size() > 0) { for (auto &iterable_timer : timers) { if(iterable_timer.second.is_active) { output = true; } } } id(global_is_timer_active) = output; - id: fetch_first_timer then: - lambda: | const auto timers = id(va).get_timers(); auto output_timer = timers.begin()->second; for (auto &iterable_timer : timers) { if (iterable_timer.second.seconds_left <= output_timer.seconds_left) { output_timer = iterable_timer.second; } } id(global_first_timer) = output_timer; - id: check_if_timers then: - lambda: | const auto timers = id(va).get_timers(); bool output = false; if (timers.size() > 0) { output = true; } id(global_is_timer) = output; - id: draw_timer_timeline then: - lambda: | id(check_if_timers_active).execute(); id(check_if_timers).execute(); if (id(global_is_timer_active)){ id(fetch_first_active_timer).execute(); int active_pixels = round( 320 * id(global_first_active_timer).seconds_left / max(id(global_first_active_timer).total_seconds , static_cast(1)) ); if (active_pixels > 0){ id(s3_box_lcd).filled_rectangle(0 , 225 , 240 , 15 , Color::WHITE ); id(s3_box_lcd).filled_rectangle(0 , 226 , active_pixels , 13 , id(active_timer_color) ); } } else if (id(global_is_timer)){ id(fetch_first_timer).execute(); int active_pixels = round( 320 * id(global_first_timer).seconds_left / max(id(global_first_timer).total_seconds , static_cast(1))); if (active_pixels > 0){ id(s3_box_lcd).filled_rectangle(0 , 225 , 240 , 15 , Color::WHITE ); id(s3_box_lcd).filled_rectangle(0 , 226 , active_pixels , 13 , id(paused_timer_color) ); } } - id: draw_active_timer_widget then: - lambda: | id(check_if_timers_active).execute(); if (id(global_is_timer_active)){ id(s3_box_lcd).filled_rectangle(80 , 40 , 160 , 50 , Color::WHITE ); id(s3_box_lcd).rectangle(80 , 40 , 160 , 50 , Color::BLACK ); id(fetch_first_active_timer).execute(); int hours_left = floor(id(global_first_active_timer).seconds_left / 3600); int minutes_left = floor((id(global_first_active_timer).seconds_left - hours_left * 3600) / 60); int seconds_left = id(global_first_active_timer).seconds_left - hours_left * 3600 - minutes_left * 60 ; auto display_hours = (hours_left < 10 ? "0" : "") + std::to_string(hours_left); auto display_minute = (minutes_left < 10 ? "0" : "") + std::to_string(minutes_left); auto display_seconds = (seconds_left < 10 ? "0" : "") + std::to_string(seconds_left) ; std::string display_string = ""; if (hours_left > 0) { display_string = display_hours + ":" + display_minute; } else { display_string = display_minute + ":" + display_seconds; } id(s3_box_lcd).printf(120, 47, id(font_timer), Color::BLACK, "%s", display_string.c_str()); } - id: start_wake_word then: - if: condition: and: - not: - voice_assistant.is_running: - lambda: return id(wake_word_engine_location).state == "On device"; then: - lambda: id(va).set_use_wake_word(false); - micro_wake_word.start: - if: condition: and: - not: - voice_assistant.is_running: - lambda: return id(wake_word_engine_location).state == "In Home Assistant"; then: - lambda: id(va).set_use_wake_word(true); - voice_assistant.start_continuous: - id: stop_wake_word then: - if: condition: lambda: return id(wake_word_engine_location).state == "In Home Assistant"; then: - lambda: id(va).set_use_wake_word(false); - voice_assistant.stop: - if: condition: lambda: return id(wake_word_engine_location).state == "On device"; then: - micro_wake_word.stop: - id: set_idle_or_mute_phase then: - 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 executed when we want to play sounds on the device. - id: play_sound parameters: priority: bool sound_file: "audio::AudioFile*" then: - lambda: |- id(playing_internal_sound) = true; if (priority) { id(external_media_player) ->make_call() .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP) .set_announcement(true) .perform(); } if ( (id(external_media_player).state != media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING ) || priority) { id(external_media_player) ->play_file(sound_file, true, false); } switch: - platform: template name: Mute id: mute icon: "mdi:microphone-off" optimistic: true restore_mode: RESTORE_DEFAULT_OFF entity_category: config on_turn_off: - microphone.unmute: - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; - script.execute: draw_display on_turn_on: - microphone.mute: - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; - script.execute: draw_display - platform: template id: timer_ringing optimistic: true internal: true restore_mode: ALWAYS_OFF on_turn_off: - lambda: |- id(external_media_player) ->make_call() .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_OFF) .set_announcement(true) .perform(); id(external_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 0); - media_player.stop: announcement: true on_turn_on: - lambda: |- id(external_media_player) ->make_call() .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ONE) .set_announcement(true) .perform(); id(external_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 1000); - media_player.speaker.play_on_device_media_file: media_file: timer_finished_sound announcement: true - delay: 15min - switch.turn_off: timer_ringing - platform: gpio name: Speaker Enable icon: "mdi:speaker" entity_category: config pin: GPIO${speaker_enable_pin} restore_mode: RESTORE_DEFAULT_ON - platform: template id: wake_sound name: Wake sound icon: "mdi:bullhorn" entity_category: config optimistic: true restore_mode: RESTORE_DEFAULT_ON - platform: template id: startup_sound_switch name: Startup sound icon: "mdi:card-text-outline" entity_category: config optimistic: true restore_mode: RESTORE_DEFAULT_ON - platform: template id: show_text name: Show Text icon: "mdi:card-text-outline" entity_category: config optimistic: true restore_mode: RESTORE_DEFAULT_ON - platform: template id: show_battery_status name: Show Battery Status icon: "mdi:card-text-outline" entity_category: config optimistic: true restore_mode: RESTORE_DEFAULT_ON on_turn_on: - script.execute: draw_display on_turn_off: - script.execute: draw_display select: - platform: template entity_category: config name: Wake word engine location id: wake_word_engine_location icon: "mdi:account-voice" optimistic: true restore_value: true options: - In Home Assistant - On device initial_option: On device on_value: - if: condition: lambda: return !id(init_in_progress); then: - 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: - micro_wake_word.stop - delay: 500ms - if: condition: switch.is_off: mute then: - lambda: id(va).set_use_wake_word(true); - voice_assistant.start_continuous: - if: condition: lambda: return x == "On device"; then: - lambda: id(va).set_use_wake_word(false); - voice_assistant.stop - delay: 500ms - if: condition: switch.is_off: mute then: - micro_wake_word.start 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: global_first_active_timer type: voice_assistant::Timer restore_value: false - id: global_is_timer_active type: bool restore_value: false - id: global_first_timer type: voice_assistant::Timer restore_value: false - id: global_is_timer type: bool restore_value: false - id: last_battery_percent type: int restore_value: no initial_value: '-1' - id: playing_internal_sound type: bool restore_value: no initial_value: 'false' - id: boot_sound_played type: bool restore_value: no initial_value: "false" image: - file: ${error_illustration_file} id: casita_error resize: ${imagewidth}x${imageheight} type: RGB565 transparency: alpha_channel - file: ${idle_illustration_file} id: casita_idle resize: ${imagewidth}x${imageheight} type: RGB565 transparency: alpha_channel - file: ${listening_illustration_file} id: casita_listening resize: ${imagewidth}x${imageheight} type: RGB565 transparency: alpha_channel - file: ${thinking_illustration_file} id: casita_thinking resize: ${imagewidth}x${imageheight} type: RGB565 transparency: alpha_channel - file: ${replying_illustration_file} id: casita_replying resize: ${imagewidth}x${imageheight} type: RGB565 transparency: alpha_channel - file: ${timer_finished_illustration_file} id: casita_timer_finished resize: ${imagewidth}x${imageheight} type: RGB565 transparency: alpha_channel - file: ${loading_illustration_file} id: casita_initializing resize: ${imagewidth}x${imageheight} type: RGB565 transparency: alpha_channel - file: ${mute_illustration_file} id: casita_muted resize: ${imagewidth}x${imageheight} type: RGB565 transparency: alpha_channel - file: https://github.com/esphome/wake-word-voice-assistants/raw/main/error_box_illustrations/error-no-wifi.png id: error_no_wifi resize: ${imagewidth}x${imageheight} type: RGB565 transparency: alpha_channel - file: https://github.com/esphome/wake-word-voice-assistants/raw/main/error_box_illustrations/error-no-ha.png id: error_no_ha resize: ${imagewidth}x${imageheight} type: RGB565 transparency: alpha_channel font: - file: type: gfonts family: ${font_family} weight: 300 italic: true id: font_request size: 15 glyphsets: - ${font_glyphsets} - file: type: gfonts family: ${font_family} weight: 300 id: font_response size: 15 glyphsets: - ${font_glyphsets} - file: type: gfonts family: ${font_family} weight: 300 id: font_battery size: 24 glyphsets: - ${font_glyphsets} - file: type: gfonts family: ${font_family} weight: 300 id: font_timer size: 30 glyphsets: - ${font_glyphsets} text_sensor: - id: text_request platform: template on_value: lambda: |- if(id(text_request).state.length()>32) { std::string name = id(text_request).state.c_str(); std::string truncated = esphome::str_truncate(name.c_str(),31); id(text_request).state = (truncated+"...").c_str(); } - id: text_response platform: template on_value: lambda: |- if(id(text_response).state.length()>32) { std::string name = id(text_response).state.c_str(); std::string truncated = esphome::str_truncate(name.c_str(),31); id(text_response).state = (truncated+"...").c_str(); } color: - id: idle_color hex: ${idle_illustration_background_color} - id: listening_color hex: ${listening_illustration_background_color} - id: thinking_color hex: ${thinking_illustration_background_color} - id: replying_color hex: ${replying_illustration_background_color} - id: loading_color hex: ${loading_illustration_background_color} - id: error_color hex: ${error_illustration_background_color} - id: active_timer_color hex: "26ed3a" - id: paused_timer_color hex: "3b89e3" spi: - id: spi_bus clk_pin: GPIO${spi_clk_pin} mosi_pin: GPIO${spi_mosi_pin} display: - platform: ili9xxx id: s3_box_lcd model: ${displaymodel} invert_colors: ${invertcolors} data_rate: 40MHz cs_pin: GPIO${lcd_cs_pin} dc_pin: GPIO${lcd_dc_pin} reset_pin: number: GPIO${lcd_reset_pin} # inverted: false update_interval: never dimensions: height: ${imageheight} width: ${imagewidth} pages: - id: idle_page lambda: |- it.fill(id(idle_color)); it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_idle), ImageAlign::CENTER); if (id(show_battery_status).state) { const int box_top = 2; const int box_height = 28; it.filled_rectangle((it.get_width()/2)-40, box_top, 80, box_height, Color::WHITE); it.rectangle((it.get_width()/2)-40, box_top, 80, box_height, Color::BLACK); char battery_text[16]; snprintf(battery_text, sizeof(battery_text), "%.0f%%", id(battery_percentage).state); it.printf(it.get_width() / 2, 4, id(font_battery), Color::BLACK, TextAlign::TOP_CENTER, "%s", battery_text); } id(draw_timer_timeline).execute(); id(draw_active_timer_widget).execute(); - id: listening_page lambda: |- it.fill(id(listening_color)); it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_listening), ImageAlign::CENTER); id(draw_timer_timeline).execute(); - id: thinking_page lambda: |- it.fill(id(thinking_color)); it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_thinking), ImageAlign::CENTER); if (id(show_text).state) { it.filled_rectangle(20 , 20 , 200 , 30 , Color::WHITE ); it.rectangle(20 , 20 , 200 , 30 , Color::BLACK ); it.printf(30, 25, id(font_request), Color::BLACK, "%s", id(text_request).state.c_str()); } id(draw_timer_timeline).execute(); - id: replying_page lambda: |- it.fill(id(replying_color)); it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_replying), ImageAlign::CENTER); int y_offset = 190; // default for 240px display if (it.get_height() == 320) { y_offset = 270; // Move box lower for 320px tall screens } if (id(show_text).state) { it.filled_rectangle(20, y_offset, 200, 30, Color::WHITE); it.rectangle(20, y_offset, 200, 30, Color::BLACK); it.printf(30, y_offset + 5, id(font_response), Color::BLACK, "%s", id(text_response).state.c_str()); } id(draw_timer_timeline).execute(); - id: timer_finished_page lambda: |- it.fill(id(idle_color)); it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_timer_finished), ImageAlign::CENTER); - id: error_page lambda: |- it.fill(id(error_color)); it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_error), ImageAlign::CENTER); - id: no_ha_page lambda: |- it.image((it.get_width() / 2), (it.get_height() / 2), id(error_no_ha), ImageAlign::CENTER); - id: no_wifi_page lambda: |- it.image((it.get_width() / 2), (it.get_height() / 2), id(error_no_wifi), ImageAlign::CENTER); - id: initializing_page lambda: |- it.fill(id(loading_color)); it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_initializing), ImageAlign::CENTER); - id: muted_page lambda: |- it.fill(Color::BLACK); it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_muted), ImageAlign::CENTER); id(draw_timer_timeline).execute(); id(draw_active_timer_widget).execute();