################################### # _ __ _ _ _ # # | |/ / | | | | | | # # | ' / ___ | |__ ___ | | __| | # # | < / _ \| '_ \ / _ \| |/ _` | # # | . \ (_) | |_) | (_) | | (_| | # # |_|\_\___/|_.__/ \___/|_|\__,_| # ################################### # Voice assistant based on # ESP32-S3-Touch-AMOLED-1.75 # Incorporates the following: # - Display -- CO5300 # - Microphone -- ES7210 # - Speaker -- ES8311 ################################## esphome: name: kobold friendly_name: Kobold esp32: board: esp32-s3-devkitc-1 flash_size: 16MB cpu_frequency: 240MHZ framework: type: esp-idf ################################### # _____ __ _ # # / ____| / _(_) # # | | ___ _ __ | |_ _ __ _ # # | | / _ \| '_ \| _| |/ _` | # # | |___| (_) | | | | | | | (_| | # # \_____\___/|_| |_|_| |_|\__, | # # __/ | # # |___/ # #Config############################ substitutions: 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" i2s_bps_spk: 16bit i2s_bps_mic: 16bit i2s_sample_rate_spk: 44100 i2s_sample_rate_mic: 16000 loading_illustration_file: kobold/loading.png listening_illustration_file: kobold/listening.png thinking_illustration_file: kobold/thinking.png replying_illustration_file: kobold/responding.png error_illustration_file: kobold/error.png logger: level: DEBUG 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 font: - file: gfonts://Nanum+Gothic+Coding id: clock_font size: 160 color: - id: black hex: "000000" - id: white hex: "FFFFFF" image: - file: ${error_illustration_file} id: kobold_error type: RGB565 - file: ${listening_illustration_file} id: kobold_listening type: RGB565 - file: ${thinking_illustration_file} id: kobold_thinking type: RGB565 - file: ${replying_illustration_file} id: kobold_replying type: RGB565 - file: ${loading_illustration_file} id: kobold_initializing type: RGB565 external_components: - source: type: git url: https://github.com/shelson/esphome-cst9217 ########################## # ____ # # | _ \ # # | |_) | __ _ ___ ___ # # | _ < / _` / __|/ _ \ # # | |_) | (_| \__ \ __/ # # |____/ \__,_|___/\___| # #Base##################### #time: # - platform: homeassistant # id: esptime # on_time_sync: # - script.execute: time_update # on_time: # - minutes: '*' # seconds: 0 # then: # - script.execute: time_update api: encryption: key: "DxR50LKDN8Msy5qOfP5e9C5xStJhLfxbTp+6rVMmfUQ=" ota: - platform: esphome password: "bd5f419dd73f955f31027dd98a607df4" wifi: ssid: !secret wifi_ssid password: !secret wifi_password # Enable fallback hotspot (captive portal) in case wifi connection fails ap: ssid: "Kobold-Test Fallback Hotspot" password: "o58qyk1mgr0b" psram: mode: octal speed: 80MHz i2c: sda: GPIO15 scl: GPIO14 scan: true id: bus_a pca9554: - id: 'pca9554a_device' address: 0x18 # Example configuration entry spi: - id: spi_bus clk_pin: GPIO2 mosi_pin: GPIO1 miso_pin: number: GPIO3 ignore_strapping_warning: true - id: quad_spi_bus type: quad clk_pin: GPIO38 data_pins: - GPIO4 - GPIO5 - GPIO6 - GPIO7 # Example configuration entry uart: tx_pin: GPIO43 rx_pin: GPIO44 baud_rate: 256000 rx_buffer_size: 1024 parity: NONE stop_bits: 1 # Example configuration entry ld2410: binary_sensor: - platform: ld2410 has_target: name: Presence has_moving_target: name: Moving Target has_still_target: name: Still Target out_pin_presence_status: name: Out pin presence status sensor: - platform: ld2410 light: name: Light moving_distance: name: Moving Distance still_distance: name: Still Distance moving_energy: name: Move Energy still_energy: name: Still Energy detection_distance: name: Detection Distance g0: move_energy: name: G0 move energy still_energy: name: G0 still energy g1: move_energy: name: G1 move energy still_energy: name: G1 still energy g2: move_energy: name: G2 move energy still_energy: name: G2 still energy g3: move_energy: name: G3 move energy still_energy: name: G3 still energy g4: move_energy: name: G4 move energy still_energy: name: G4 still energy g5: move_energy: name: G5 move energy still_energy: name: G5 still energy g6: move_energy: name: G6 move energy still_energy: name: G6 still energy g7: move_energy: name: G7 move energy still_energy: name: G7 still energy g8: move_energy: name: G8 move energy still_energy: name: G8 still energy - platform: apds9960 type: CLEAR name: "APDS9960 Clear Channel" number: - platform: ld2410 timeout: name: Timeout light_threshold: name: Light threshold max_move_distance_gate: name: Max move distance gate max_still_distance_gate: name: Max still distance gate g0: move_threshold: name: G0 move threshold still_threshold: name: G0 still threshold g1: move_threshold: name: G1 move threshold still_threshold: name: G1 still threshold g2: move_threshold: name: G2 move threshold still_threshold: name: G2 still threshold g3: move_threshold: name: G3 move threshold still_threshold: name: G3 still threshold g4: move_threshold: name: G4 move threshold still_threshold: name: G4 still threshold g5: move_threshold: name: G5 move threshold still_threshold: name: G5 still threshold g6: move_threshold: name: G6 move threshold still_threshold: name: G6 still threshold g7: move_threshold: name: G7 move threshold still_threshold: name: G7 still threshold g8: move_threshold: name: G8 move threshold still_threshold: name: G8 still threshold apds9960: address: 0x39 update_interval: 10s ambient_light_gain: 64x gesture_gain: 1x gesture_led_drive: 100ma gesture_wait_time: 39.2ms 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 ################################# # _ _ # # /\ | (_) # # / \ _ _ __| |_ ___ # # / /\ \| | | |/ _` | |/ _ \ # # / ____ \ |_| | (_| | | (_) | # # /_/ \_\__,_|\__,_|_|\___/ # #Audio########################### i2s_audio: - id: i2s_audio_bus i2s_mclk_pin: GPIO42 i2s_bclk_pin: GPIO9 i2s_lrclk_pin: number: GPIO45 ignore_strapping_warning: true audio_adc: - platform: es7210 id: es7210_adc bits_per_sample: $i2s_bps_mic sample_rate: $i2s_sample_rate_mic audio_dac: - platform: es8311 id: es8311_dac bits_per_sample: $i2s_bps_spk sample_rate: $i2s_sample_rate_spk microphone: - platform: i2s_audio id: box_mic sample_rate: $i2s_sample_rate_mic i2s_din_pin: GPIO10 bits_per_sample: $i2s_bps_mic adc_type: external speaker: - platform: i2s_audio id: box_speaker i2s_dout_pin: GPIO8 dac_type: external sample_rate: $i2s_sample_rate_spk bits_per_sample: $i2s_bps_spk audio_dac: es8311_dac buffer_duration: 90ms use_apll: true media_player: - platform: speaker name: player id: speaker_media_player volume_max: 80% announcement_pipeline: speaker: box_speaker format: FLAC on_announcement: - if: condition: - microphone.is_capturing: then: - script.execute: stop_wake_word # Ensure VA stops before moving on - 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: - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; - logger.log: "WEH WEH!" on_idle: # Since VA isn't running, this is the end of user-intiated media playback. Restart the wake word. - if: condition: - and: - not: voice_assistant.is_running: - lambda: return id(voice_assistant_phase) != ${voice_assist_idle_phase_id}; then: - script.execute: start_wake_word - script.execute: set_idle_or_mute_phase ###################################### # _____ _ _ # # | __ \(_) | | # # | | | |_ ___ _ __ | | __ _ _ _ # # | | | | / __| '_ \| |/ _` | | | | # # | |__| | \__ \ |_) | | (_| | |_| | # # |_____/|_|___/ .__/|_|\__,_|\__, | # # | | __/ | # # |_| |___/ # #Display############################## lvgl: id: voice_assistant_display buffer_size: 25% # animations: # - # - id: text_fade_out # duration: 1s # timing: # - type: ease_in_out # weight: 1.0 # widgets: # - id: lbl_time # text_color: # from: white # to: black # - id: text_fade_in # duration: 1s # timing: # - type: ease_in_out # weight: 1.0 # widgets: # - id: lbl_time # text_color: # from: black # to: white pages: - id: image_page bg_color: black widgets: - image: id: current_image align: CENTER src: kobold_initializing - id: clock_page bg_color: black # widgets: # - label: # id: lbl_time # text: "00:00" # align: CENTER # text_align: CENTER # text_font: clock_font # text_color: 0xffffff # pad_left: 22 # outline_color: black display: - platform: mipi_spi id: disp1 model: CO5300 bus_mode: quad reset_pin: GPIO39 cs_pin: GPIO12 data_rate: 80MHz dimensions: height: 466 width: 466 offset_width: 6 light: - platform: monochromatic id: display_backlight name: "Backlight" output: backlight_brightness default_transition_length: milliseconds: 0 initial_state: brightness: 81% restore_mode: ALWAYS_ON output: - platform: template id: backlight_brightness type: float write_action: then: - lambda: |- id(disp1).set_brightness(state*255); touchscreen: - platform: cst9217 display: disp1 id: ts_disp1 interrupt_pin: GPIO11 reset_pin: GPIO40 transform: mirror_x: true mirror_y: true on_update: - lambda: |- for (auto touch: touches) { if (touch.state <= 2) { ESP_LOGI("Touch points:", "id=%d x=%d, y=%d", touch.id, touch.x, touch.y); } } ############################################ # _____ _ _ # # |_ _| | | | | # # | | _ __ | |_ ___ _ __ __ _ ___| |_ # # | | | '_ \| __/ _ \ '__/ _` |/ __| __| # # _| |_| | | | || __/ | | (_| | (__| |_ # # |_____|_| |_|\__\___|_| \__,_|\___|\__| # #Interact################################### button: - platform: restart name: "Living Room Restart" switch: - platform: ld2410 engineering_mode: name: Engineering mode bluetooth: name: Control bluetooth - platform: gpio name: "Speaker Enable" pin: number: GPIO46 ignore_strapping_warning: true restore_mode: RESTORE_DEFAULT_ON - 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}; - logger.log: "Calling draw_display from switch on_turn_off" on_turn_on: - microphone.mute: - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; - logger.log: "Calling draw_display from switch on_turn_on" micro_wake_word: id: mww models: - model: https://raw.githubusercontent.com/YOUR-WORST-TACO/CustomWakeupWords/refs/heads/main/models/hey_nokari/hey_nokari.json id: hey_nokari # probability_cutoff: 0.98 # sliding_window_size: 5 - model: https://raw.githubusercontent.com/YOUR-WORST-TACO/CustomWakeupWords/refs/heads/main/models/nokari/nokari.json id: nokari probability_cutoff: 0.98 sliding_window_size: 5 on_wake_word_detected: - voice_assistant.start: wake_word: !lambda return wake_word; voice_assistant: id: va microphone: box_mic media_player: speaker_media_player micro_wake_word: mww noise_suppression_level: 2 auto_gain: 31dBFS volume_multiplier: 2.0 on_listening: - if: condition: lambda: return id(voice_assistant_phase) != ${voice_assist_listening_phase_id}; then: - lambda: id(voice_assistant_phase) = ${voice_assist_listening_phase_id}; - logger.log: "Calling draw_display from voice_assistant on_listening" - script.execute: draw_display - text_sensor.template.publish: id: text_request state: "..." - text_sensor.template.publish: id: text_response state: "..." on_stt_vad_end: - lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id}; - logger.log: "Calling draw_display from voice_assistant on_stt_vad_end" - script.execute: draw_display on_stt_end: - text_sensor.template.publish: id: text_request state: !lambda return x; - logger.log: "Calling draw_display from voice_assistant on_stt_end" - 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}; - logger.log: "Calling draw_display from voice_assistant on_tts_start" - script.execute: draw_display on_end: # Wait a short amount of time to see if an announcement starts - wait_until: condition: - media_player.is_announcing: timeout: 0.5s # Announcement is finished and the I2S bus is free - wait_until: - and: - not: media_player.is_announcing: - not: speaker.is_playing: # Restart only mWW if enabled; streaming wake words automatically restart - 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 - logger.log: "Calling draw_display from voice_assistant on_end" - script.execute: draw_display # Clear text sensors - 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}; - logger.log: "Calling draw_display from voice_assistant on_error" - 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}; - logger.log: "Calling draw_display from voice_assistant on_error" - 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 - logger.log: "Calling draw_display from voice_assistant on_client_connected" - script.execute: draw_display on_client_disconnected: - script.execute: stop_wake_word - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id}; - logger.log: "Calling draw_display from voice_assistant on_client_disconnected" - script.execute: draw_display 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(); } mapping: - id: image_mapping from: int to: image entries: ${voice_assist_listening_phase_id}: kobold_listening ${voice_assist_thinking_phase_id}: kobold_thinking ${voice_assist_replying_phase_id}: kobold_replying ${voice_assist_not_ready_phase_id}: kobold_error ${voice_assist_error_phase_id}: kobold_error ${voice_assist_muted_phase_id}: kobold_error script: # - id: time_update # then: # - lvgl.animation.start: text_fade_out # - delay: 1s # - lvgl.label.update: # id: lbl_time # text: !lambda |- # return id(esptime).now().strftime("%H:%M"); # - lvgl.animation.start: text_fade_in # - logger.log: "update Time script ran" - id: draw_display then: - logger.log: "Drawing display" - if: condition: lambda: return !id(init_in_progress); then: - if: condition: wifi.connected: then: - if: condition: api.connected: then: - if: condition: lambda: return id(voice_assistant_phase) == ${voice_assist_idle_phase_id}; then: - lvgl.page.show: id: clock_page animation: OUT_BOTTOM time: 500ms else: - lvgl.page.show: id: image_page animation: OUT_TOP time: 500ms - lvgl.image.update: id: current_image src: !lambda return id(image_mapping)[id(voice_assistant_phase)]; else: - lvgl.page.show: id: image_page animation: OUT_TOP time: 500ms - lvgl.image.update: id: current_image src: kobold_error else: - lvgl.page.show: id: image_page animation: OUT_TOP time: 500ms - lvgl.image.update: id: current_image src: kobold_error else: - lvgl.page.show: id: image_page animation: OUT_TOP time: 500ms - lvgl.image.update: id: current_image src: kobold_initializing # Starts either mWW or the streaming wake word, depending on the configured location - 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: # Stops either mWW or the streaming wake word, depending on the configured location - 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: # Set the voice assistant phase to idle or muted, depending on if the software mute switch is activated - 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};