# ============================================================================= # Auto-generated from literate documentation # Do not edit directly - modify the source markdown instead # ============================================================================= # --- substitutions --- substitutions: device_name: "${timer_area}-voice-assistant" friendly_name: "${timer_area} Voice Assistant" device_description: "Waveshare ESP32-S3-Touch-LCD-1.83" # REQUIRED: Set this to match your HA area timer_area: "bedroom" # Generic voice assistant images (240x240 for this display) loading_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/loading_320_240.png idle_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/idle_320_240.png listening_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/listening_320_240.png thinking_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/thinking_320_240.png replying_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/replying_320_240.png error_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/error_320_240.png timer_finished_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/timer_finished_320_240.png # Background colors loading_illustration_background_color: "000000" idle_illustration_background_color: "000000" listening_illustration_background_color: "FFFFFF" thinking_illustration_background_color: "FFFFFF" replying_illustration_background_color: "FFFFFF" error_illustration_background_color: "000000" timer_finished_illustration_background_color: "FFFFFF" # Voice assistant phase IDs 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" voice_assist_ota_phase_id: "30" # Display dimensions for Waveshare 1.83" LCD display_width: "240" display_height: "284" # Font configuration font_glyphsets: "GF_Latin_Core" font_family: Figtree # Hardware pin definitions i2s_mclk_pin: GPIO16 i2s_bclk_pin: GPIO9 i2s_lrclk_pin: GPIO45 i2s_din_pin: GPIO10 i2s_dout_pin: GPIO8 audio_i2c_sda: GPIO15 audio_i2c_scl: GPIO14 pa_enable_pin: GPIO46 display_clk_pin: GPIO6 display_mosi_pin: GPIO7 display_cs_pin: GPIO5 display_dc_pin: GPIO4 display_rst_pin: GPIO38 display_backlight_pin: GPIO40 touch_int_pin: GPIO13 touch_rst_pin: GPIO39 boot_button_pin: GPIO0 power_button_pin: GPIO41 # --- esphome --- esphome: name: ${device_name} friendly_name: ${friendly_name} comment: ${device_description} min_version: 2025.5.0 name_add_mac_suffix: false on_boot: priority: 600 then: - script.execute: draw_display - delay: 30s - if: condition: lambda: return id(init_in_progress); then: - lambda: id(init_in_progress) = false; - script.execute: draw_display # --- esp32 --- esp32: board: esp32-s3-devkitc-1 variant: esp32s3 flash_size: 16MB cpu_frequency: 240MHz 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_SPIRAM_RODATA: "y" CONFIG_SPIRAM_FETCH_INSTRUCTIONS: "y" external_components: - source: github://Djelibeybi/esphome-qmi8658@main components: [qmi8658] refresh: 10min psram: mode: octal speed: 80MHz # --- api --- api: on_client_connected: - script.execute: draw_display on_client_disconnected: - script.execute: draw_display services: - service: timer_finished then: - logger.log: "Timer finished! Playing alarm..." - switch.turn_on: timer_ringing - service: timer_started variables: duration: int then: - logger.log: format: "Timer started with duration: %d seconds" args: ["duration"] - script.execute: draw_display - service: timer_cancelled then: - logger.log: "Timer cancelled" - switch.turn_off: timer_ringing - script.execute: draw_display - service: stop_alarm then: - switch.turn_off: timer_ringing # --- ota --- ota: - platform: esphome id: ota_esphome on_begin: - script.execute: stop_wake_word - lambda: |- id(voice_assistant_phase) = ${voice_assist_ota_phase_id}; id(ota_progress) = 0; - display.page.show: ota_page - component.update: lcd_display on_progress: - lambda: id(ota_progress) = (int)x; - component.update: lcd_display on_end: - lambda: id(ota_progress) = 100; - component.update: lcd_display on_error: - lambda: id(ota_progress) = -1; - display.page.show: error_page - component.update: lcd_display - delay: 5s - script.execute: draw_display logger: level: INFO hardware_uart: USB_SERIAL_JTAG logs: text_sensor: WARN sensor: WARN component: ERROR wifi: ssid: !secret wifi_ssid password: !secret wifi_password on_connect: - script.execute: draw_display on_disconnect: - script.execute: draw_display time: - platform: sntp id: sntp_time servers: !secret ntp_servers timezone: !secret timezone # --- interval --- interval: # 30-second sync ensures display matches timer state - interval: 30s then: - lambda: |- if (id(voice_assistant_phase) == ${voice_assist_idle_phase_id} || id(voice_assistant_phase) == ${voice_assist_muted_phase_id}) { std::string state = id(timer_state).state; if (state == "active" || state == "paused") { ESP_LOGD("timer_sync", "Timer is %s but display is idle - triggering redraw", state.c_str()); id(draw_display).execute(); } } # Fast display update for final 60 seconds - interval: 1s then: - lambda: |- std::string state = id(timer_state).state; float remaining = id(timer_remaining).state; if ((state == "active" || state == "paused") && !std::isnan(remaining) && remaining <= 60) { id(draw_display).execute(); } # --- button --- button: - platform: restart id: restart_btn name: Restart - platform: factory_reset id: factory_reset_btn internal: true # --- i2c --- i2c: - id: internal_i2c sda: ${audio_i2c_sda} scl: ${audio_i2c_scl} scan: true frequency: 400kHz qmi8658: id: imu_sensor address: 0x6B update_interval: 100ms on_orientation_change: then: - lambda: |- ESP_LOGD("orientation", "Orientation changed to: %s", orientation.c_str()); if (orientation == "portrait_inverted") { id(landscape_mode) = false; id(lcd_display).set_rotation(esphome::display::DISPLAY_ROTATION_180_DEGREES); id(draw_display).execute(); } else if (orientation == "portrait") { id(landscape_mode) = false; id(lcd_display).set_rotation(esphome::display::DISPLAY_ROTATION_0_DEGREES); id(draw_display).execute(); } else if (orientation == "landscape_left" || orientation == "landscape_right") { id(landscape_mode) = true; id(lcd_display).show_page(landscape_warning_page); id(lcd_display).update(); } # --- spi --- spi: - id: spi_bus clk_pin: ${display_clk_pin} mosi_pin: ${display_mosi_pin} # --- audio --- i2s_audio: - id: i2s_audio_bus i2s_lrclk_pin: ${i2s_lrclk_pin} i2s_bclk_pin: ${i2s_bclk_pin} i2s_mclk_pin: ${i2s_mclk_pin} audio_adc: - platform: es7210 id: es7210_adc bits_per_sample: 16bit sample_rate: 16000 i2c_id: internal_i2c audio_dac: - platform: es8311 id: es8311_dac bits_per_sample: 16bit sample_rate: 48000 i2c_id: internal_i2c microphone: - platform: i2s_audio id: box_mic sample_rate: 16000 i2s_din_pin: ${i2s_din_pin} bits_per_sample: 16bit adc_type: external speaker: - id: i2s_audio_speaker platform: i2s_audio i2s_audio_id: i2s_audio_bus i2s_dout_pin: ${i2s_dout_pin} dac_type: external sample_rate: 48000 bits_per_sample: 16bit channel: left audio_dac: es8311_dac buffer_duration: 100ms media_player: - platform: speaker name: None id: speaker_media_player volume_min: 0.5 volume_max: 0.8 task_stack_in_psram: true announcement_pipeline: speaker: i2s_audio_speaker format: FLAC sample_rate: 48000 num_channels: 1 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 file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/wake_word_triggered.flac on_announcement: - lambda: id(announcement_in_progress) = true; - script.execute: track_announcement_lifecycle - if: condition: - microphone.is_capturing: then: - script.execute: stop_wake_word - if: condition: and: - not: voice_assistant.is_running: - switch.is_off: timer_ringing then: - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; - script.execute: draw_display on_idle: - delay: 100ms - if: condition: and: - not: voice_assistant.is_running: - switch.is_off: timer_ringing - not: media_player.is_announcing: - lambda: return !id(announcement_in_progress); then: - script.execute: start_wake_word - script.execute: set_idle_or_mute_phase - script.execute: draw_display # --- wake_word --- micro_wake_word: id: mww models: - model: okay_nabu id: okay_nabu vad: model: github://esphome/micro-wake-word-models/models/v2/vad.json 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: - 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: - if: condition: - lambda: return id(announcement_in_progress); then: - logger.log: "on_end: Skipping - announcement in progress" else: - wait_until: condition: - media_player.is_announcing: timeout: 3s - wait_until: - and: - not: media_player.is_announcing: - not: speaker.is_playing: - lambda: id(va).set_use_wake_word(false); - micro_wake_word.start: - script.execute: set_idle_or_mute_phase - text_sensor.template.publish: id: text_request state: "" - text_sensor.template.publish: id: text_response state: "" - script.execute: draw_display 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 # Timer Event Stubs - HA handles actual timer logic on_timer_started: - logger.log: format: "Timer started (handled by HA): %s" args: ["timer.id.c_str()"] on_timer_finished: - logger.log: "Timer finished event received (handled by HA automation)" on_timer_cancelled: - logger.log: "Timer cancelled (handled by HA)" on_timer_updated: - logger.log: "Timer updated (handled by HA)" on_timer_tick: - lambda: return; # --- sensor --- sensor: - platform: template name: "Voice Assistant Phase" id: voice_assistant_phase_sensor lambda: |- return (float)id(voice_assistant_phase); update_interval: 500ms - platform: homeassistant id: timer_remaining name: "Timer remaining" entity_id: sensor.${timer_area}_timer_remaining_seconds unit_of_measurement: "s" device_class: "duration" on_value: then: - script.execute: draw_display - platform: homeassistant id: timer_duration name: "Timer duration" entity_id: sensor.${timer_area}_timer_remaining_seconds attribute: duration_seconds unit_of_measurement: "s" device_class: "duration" - platform: homeassistant id: timer_progress entity_id: sensor.${timer_area}_timer_remaining_seconds attribute: progress_percent internal: true - platform: wifi_signal name: "WiFi db" id: wifi_signal_db update_interval: 30s - platform: copy source_id: wifi_signal_db name: "WiFi Signal" id: wifi_percent filters: - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0); unit_of_measurement: "%" entity_category: "diagnostic" # --- text_sensor --- text_sensor: - platform: template name: "Voice Assistant State" id: voice_assistant_state_sensor lambda: |- int phase = id(voice_assistant_phase); switch(phase) { case ${voice_assist_idle_phase_id}: return {"idle"}; case ${voice_assist_listening_phase_id}: return {"listening"}; case ${voice_assist_thinking_phase_id}: return {"thinking"}; case ${voice_assist_replying_phase_id}: return {"replying"}; case ${voice_assist_error_phase_id}: return {"error"}; case ${voice_assist_not_ready_phase_id}: return {"not_ready"}; case ${voice_assist_muted_phase_id}: return {"muted"}; case ${voice_assist_timer_finished_phase_id}: return {"timer_finished"}; default: return {"unknown"}; } update_interval: 500ms - platform: homeassistant id: timer_state entity_id: sensor.${timer_area}_timer_remaining_seconds attribute: timer_state internal: true on_value: then: - script.execute: draw_display - 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(); } # --- touchscreen --- touchscreen: - platform: cst816 i2c_id: internal_i2c id: cst816_touchscreen interrupt_pin: ${touch_int_pin} reset_pin: ${touch_rst_pin} # --- binary_sensor --- binary_sensor: - platform: touchscreen touchscreen_id: cst816_touchscreen id: touch_area x_min: 0 x_max: ${display_width} y_min: 0 y_max: ${display_height} on_press: then: - if: condition: lambda: return !id(init_in_progress); then: - if: condition: switch.is_on: timer_ringing then: - switch.turn_off: timer_ringing else: - if: condition: voice_assistant.is_running: then: - voice_assistant.stop: else: - if: condition: media_player.is_announcing: then: media_player.stop: announcement: true else: - if: condition: media_player.is_playing: then: - media_player.pause: else: - if: condition: and: - switch.is_off: mute - not: voice_assistant.is_running then: - media_player.speaker.play_on_device_media_file: media_file: wake_word_triggered_sound_file announcement: true - wait_until: - not: - media_player.is_announcing: - voice_assistant.start: - platform: gpio pin: number: ${power_button_pin} mode: INPUT_PULLUP inverted: true id: power_button internal: true on_multi_click: - timing: - ON for at least 50ms - OFF for at least 50ms then: - switch.toggle: mute - platform: gpio pin: number: ${boot_button_pin} mode: INPUT_PULLUP inverted: true id: boot_button internal: true on_multi_click: - timing: - ON for at least 50ms - OFF for at least 50ms then: - switch.turn_off: timer_ringing - timing: - ON for at least 10s then: - button.press: factory_reset_btn # --- switch --- output: - platform: ledc pin: ${display_backlight_pin} id: backlight_output light: - platform: monochromatic id: led name: Screen icon: "mdi:television" entity_category: config output: backlight_output restore_mode: RESTORE_DEFAULT_ON default_transition_length: 250ms switch: - platform: gpio name: Speaker Enable pin: ${pa_enable_pin} restore_mode: RESTORE_DEFAULT_ON entity_category: config disabled_by_default: true - 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 name: "Timer Ringing" optimistic: true restore_mode: ALWAYS_OFF icon: "mdi:bell-ring-outline" on_turn_off: - lambda: |- id(speaker_media_player) ->make_call() .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_OFF) .set_announcement(true) .perform(); id(speaker_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 0); - media_player.stop: announcement: true - script.execute: set_idle_or_mute_phase - script.execute: draw_display on_turn_on: - lambda: id(voice_assistant_phase) = ${voice_assist_timer_finished_phase_id}; - script.execute: draw_display - lambda: |- id(speaker_media_player) ->make_call() .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ONE) .set_announcement(true) .perform(); id(speaker_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 # --- globals --- 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: ota_progress type: int restore_value: false initial_value: "0" - id: announcement_in_progress type: bool restore_value: false initial_value: "false" - id: landscape_mode type: bool restore_value: false initial_value: "false" # --- script --- script: - id: draw_display then: - if: condition: lambda: return !id(init_in_progress); then: - if: condition: lambda: return id(landscape_mode); then: - display.page.show: landscape_warning_page - component.update: lcd_display else: - if: condition: wifi.connected: then: - if: condition: api.connected: then: - lambda: | switch(id(voice_assistant_phase)) { case ${voice_assist_listening_phase_id}: id(lcd_display).show_page(listening_page); id(lcd_display).update(); break; case ${voice_assist_thinking_phase_id}: id(lcd_display).show_page(thinking_page); id(lcd_display).update(); break; case ${voice_assist_replying_phase_id}: id(lcd_display).show_page(replying_page); id(lcd_display).update(); break; case ${voice_assist_error_phase_id}: id(lcd_display).show_page(error_page); id(lcd_display).update(); break; case ${voice_assist_muted_phase_id}: id(lcd_display).show_page(muted_page); id(lcd_display).update(); break; case ${voice_assist_not_ready_phase_id}: id(lcd_display).show_page(no_ha_page); id(lcd_display).update(); break; case ${voice_assist_timer_finished_phase_id}: id(lcd_display).show_page(timer_finished_page); id(lcd_display).update(); break; case ${voice_assist_ota_phase_id}: id(lcd_display).show_page(ota_page); id(lcd_display).update(); break; default: id(lcd_display).show_page(idle_page); id(lcd_display).update(); } else: - display.page.show: no_ha_page - component.update: lcd_display else: - display.page.show: no_wifi_page - component.update: lcd_display else: - display.page.show: initializing_page - component.update: lcd_display # Number-to-words status bar for timer display - id: draw_status_bar then: - lambda: | const char* ones[] = {"", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"}; const char* tens[] = {"", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"}; auto num_to_words = [&](int n, char* buf, size_t buf_size) { if (n == 0) { snprintf(buf, buf_size, "zero"); return; } std::string result; if (n >= 100) { int hundreds = n / 100; result += ones[hundreds]; result += " hundred"; n %= 100; if (n > 0) result += " "; } if (n >= 20) { result += tens[n / 10]; if (n % 10 != 0) { result += "-"; result += ones[n % 10]; } } else if (n > 0) { result += ones[n]; } snprintf(buf, buf_size, "%s", result.c_str()); }; std::string state = id(timer_state).state; int remaining = (int)id(timer_remaining).state; int duration = (int)id(timer_duration).state; if ((state == "active" || state == "paused") && remaining > 0) { id(lcd_display).filled_rectangle(0, 200, 240, 84, Color::BLACK); char num_word[32]; char unit_text[32]; if (remaining >= 60) { int mins = (remaining + 59) / 60; num_to_words(mins, num_word, sizeof(num_word)); snprintf(unit_text, sizeof(unit_text), "minute%s remaining", (mins == 1) ? "" : "s"); } else { num_to_words(remaining, num_word, sizeof(num_word)); snprintf(unit_text, sizeof(unit_text), "second%s remaining", (remaining == 1) ? "" : "s"); } id(lcd_display).printf(120, 207, id(font_timer_large), Color::WHITE, TextAlign::TOP_CENTER, "%s", num_word); id(lcd_display).printf(120, 235, id(font_timer_large), Color::WHITE, TextAlign::TOP_CENTER, "%s", unit_text); if (state == "paused") { id(lcd_display).printf(120, 263, id(font_status), Color::WHITE, TextAlign::TOP_CENTER, "(paused)"); } } - id: start_wake_word then: - if: condition: not: - voice_assistant.is_running: then: - lambda: id(va).set_use_wake_word(false); - micro_wake_word.start: - id: stop_wake_word 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}; - id: track_announcement_lifecycle mode: restart then: - logger.log: "Announcement lifecycle: waiting for audio to start..." - wait_until: condition: - media_player.is_announcing: timeout: 30s - if: condition: - not: media_player.is_announcing: then: - logger.log: "Announcement lifecycle: timed out waiting for audio" - lambda: id(announcement_in_progress) = false; else: - logger.log: "Announcement lifecycle: audio playing, waiting for completion..." - wait_until: condition: - not: media_player.is_announcing: timeout: 10min - logger.log: "Announcement lifecycle: complete" - lambda: id(announcement_in_progress) = false; # --- image --- image: - file: ${error_illustration_file} id: casita_error resize: 240x240 type: RGB transparency: alpha_channel - file: ${idle_illustration_file} id: casita_idle resize: 240x240 type: RGB transparency: alpha_channel - file: ${listening_illustration_file} id: casita_listening resize: 240x240 type: RGB transparency: alpha_channel - file: ${thinking_illustration_file} id: casita_thinking resize: 240x240 type: RGB transparency: alpha_channel - file: ${replying_illustration_file} id: casita_replying resize: 240x240 type: RGB transparency: alpha_channel - file: ${timer_finished_illustration_file} id: casita_timer_finished resize: 240x240 type: RGB transparency: alpha_channel - file: ${loading_illustration_file} id: casita_initializing resize: 240x240 type: RGB 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: 240x240 type: RGB 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: 240x240 type: RGB transparency: alpha_channel # --- font --- font: - file: type: gfonts family: ${font_family} weight: 300 italic: true id: font_request size: 13 glyphsets: - ${font_glyphsets} - file: type: gfonts family: ${font_family} weight: 300 id: font_response size: 13 glyphsets: - ${font_glyphsets} - file: type: gfonts family: ${font_family} weight: 300 id: font_timer size: 26 glyphsets: - ${font_glyphsets} - file: type: gfonts family: ${font_family} weight: 700 id: font_ota size: 20 glyphsets: - ${font_glyphsets} - file: type: gfonts family: ${font_family} weight: 400 id: font_status size: 14 glyphsets: - ${font_glyphsets} - file: type: gfonts family: ${font_family} weight: 400 id: font_timer_large size: 24 glyphsets: - ${font_glyphsets} - file: type: gfonts family: ${font_family} weight: 700 id: font_warning_title size: 28 glyphsets: - ${font_glyphsets} - file: type: gfonts family: ${font_family} weight: 400 id: font_warning_body size: 16 glyphsets: - ${font_glyphsets} # --- color --- 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: timer_finished_color hex: ${timer_finished_illustration_background_color} - id: muted_color hex: "000000" - id: ota_progress_color hex: "ff6600" - id: warning_red hex: "FF3030" - id: warning_orange hex: "FF6A00" # --- display --- display: - platform: st7789v id: lcd_display model: Custom height: ${display_height} width: ${display_width} rotation: 180 offset_height: 0 offset_width: 0 cs_pin: ${display_cs_pin} dc_pin: ${display_dc_pin} reset_pin: ${display_rst_pin} eightbitcolor: false update_interval: never pages: - id: idle_page lambda: |- it.fill(id(idle_color)); it.image(120, 0, id(casita_idle), ImageAlign::TOP_CENTER); std::string state = id(timer_state).state; bool timer_active = (state == "active" || state == "paused"); if (timer_active) { id(draw_status_bar).execute(); } else { it.printf(120, 230, id(font_status), Color::WHITE, TextAlign::TOP_CENTER, "Say 'Okay Nabu'"); it.printf(120, 250, id(font_status), Color::WHITE, TextAlign::TOP_CENTER, "to get started"); } - id: listening_page lambda: |- it.fill(id(listening_color)); it.image(120, 0, id(casita_listening), ImageAlign::TOP_CENTER); std::string state = id(timer_state).state; bool timer_active = (state == "active" || state == "paused"); if (timer_active) { id(draw_status_bar).execute(); } else { it.printf(120, 240, id(font_status), Color::BLACK, TextAlign::TOP_CENTER, "Listening..."); } - id: thinking_page lambda: |- it.fill(id(thinking_color)); it.image(120, 0, id(casita_thinking), ImageAlign::TOP_CENTER); it.filled_rectangle(10, 15, 220, 25, Color::WHITE); it.rectangle(10, 15, 220, 25, Color::BLACK); it.printf(15, 18, id(font_request), Color::BLACK, "%s", id(text_request).state.c_str()); std::string state = id(timer_state).state; bool timer_active = (state == "active" || state == "paused"); if (timer_active) { id(draw_status_bar).execute(); } else { it.printf(120, 240, id(font_status), Color::BLACK, TextAlign::TOP_CENTER, "Thinking..."); } - id: replying_page lambda: |- it.fill(id(replying_color)); it.image(120, 0, id(casita_replying), ImageAlign::TOP_CENTER); it.filled_rectangle(10, 15, 220, 25, Color::WHITE); it.rectangle(10, 15, 220, 25, Color::BLACK); it.printf(15, 18, id(font_request), Color::BLACK, "%s", id(text_request).state.c_str()); std::string state = id(timer_state).state; bool timer_active = (state == "active" || state == "paused"); if (timer_active) { id(draw_status_bar).execute(); } - id: timer_finished_page lambda: |- it.fill(id(timer_finished_color)); it.image(120, 0, id(casita_timer_finished), ImageAlign::TOP_CENTER); it.printf(120, 240, id(font_timer_large), Color::BLACK, TextAlign::TOP_CENTER, "Timer finished!"); it.printf(120, 265, id(font_status), Color::BLACK, TextAlign::TOP_CENTER, "Tap to dismiss"); - id: error_page lambda: |- it.fill(id(error_color)); it.image(120, 0, id(casita_error), ImageAlign::TOP_CENTER); std::string state = id(timer_state).state; bool timer_active = (state == "active" || state == "paused"); if (timer_active) { id(draw_status_bar).execute(); } else { it.printf(120, 240, id(font_status), Color::WHITE, TextAlign::TOP_CENTER, "An error occurred"); } - id: no_ha_page lambda: |- it.fill(Color::BLACK); it.image(120, 0, id(error_no_ha), ImageAlign::TOP_CENTER); id(draw_status_bar).execute(); - id: no_wifi_page lambda: |- it.fill(Color::BLACK); it.image(120, 0, id(error_no_wifi), ImageAlign::TOP_CENTER); id(draw_status_bar).execute(); - id: initializing_page lambda: |- it.fill(id(loading_color)); it.image(120, 0, id(casita_initializing), ImageAlign::TOP_CENTER); it.printf(120, 240, id(font_status), Color::WHITE, TextAlign::TOP_CENTER, "Initializing..."); - id: muted_page lambda: |- it.fill(id(muted_color)); std::string state = id(timer_state).state; bool timer_active = (state == "active" || state == "paused"); if (timer_active) { id(draw_status_bar).execute(); } else { it.printf(120, 142, id(font_status), Color::WHITE, TextAlign::CENTER, "Microphone muted"); } - id: ota_page lambda: |- it.fill(id(error_color)); it.image(120, 0, id(casita_error), ImageAlign::TOP_CENTER); it.filled_rectangle(0, 200, 240, 84, Color::BLACK); it.filled_rectangle(10, 208, 220, 16, Color(0x202020)); it.rectangle(10, 208, 220, 16, Color::WHITE); int progress_width = (id(ota_progress) * 216) / 100; if (progress_width > 0) { it.filled_rectangle(12, 210, progress_width, 12, id(ota_progress_color)); } if (id(ota_progress) >= 0) { it.printf(120, 232, id(font_status), Color::WHITE, TextAlign::TOP_CENTER, "Upgrading: %d%%", id(ota_progress)); } else { it.printf(120, 232, id(font_status), Color::WHITE, TextAlign::TOP_CENTER, "Update Failed!"); } - id: landscape_warning_page lambda: |- int w = it.get_width(); int h = it.get_height(); int cx = w / 2; it.fill(Color::BLACK); // Hazard stripes for (int i = 0; i < w; i += 20) { it.filled_rectangle(i, 0, 10, 8, id(warning_orange)); it.filled_rectangle(i, h - 8, 10, 8, id(warning_orange)); } it.printf(cx, 40, id(font_warning_title), id(warning_red), TextAlign::TOP_CENTER, "WARNING"); it.horizontal_line(20, 75, w - 40, id(warning_orange)); it.printf(cx, 90, id(font_warning_body), Color::WHITE, TextAlign::TOP_CENTER, "Device orientation"); it.printf(cx, 115, id(font_warning_body), Color::WHITE, TextAlign::TOP_CENTER, "not supported"); it.horizontal_line(20, 150, w - 40, id(warning_orange)); it.printf(cx, 170, id(font_status), id(warning_orange), TextAlign::TOP_CENTER, "Please rotate to"); it.printf(cx, 190, id(font_status), id(warning_orange), TextAlign::TOP_CENTER, "portrait mode");