Skip to content

Instantly share code, notes, and snippets.

@aalaei
Last active May 10, 2025 19:57
Show Gist options
  • Select an option

  • Save aalaei/515d900e1cf5be352d4f8d775e6f8678 to your computer and use it in GitHub Desktop.

Select an option

Save aalaei/515d900e1cf5be352d4f8d775e6f8678 to your computer and use it in GitHub Desktop.
ESPHome ESP32-S3-LCD-1.85
substitutions:
name: esp32-s3-round
friendly_name: esp32-s3-round
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
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"
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"
# These unique characters have been extracted from every test file of every language available on https://github.com/home-assistant/intents (14 March 2024)
# However, the Figtree font only contains Latin characters, so there is no point using this... unlessyou change the font configuration accordingly.
allowed_characters: " !#%'()+,-./0123456789:;<>?@ABCDEFGHIJKLMNOPQRSTUVWYZ[]_abcdefghijklmnopqrstuvwxyz{|}°²³µ¿ÁÂÄÅÉÖÚßàáâãäåæçèéêëìíîðñòóôõöøùúûüýþāăąćčďĐđēėęěğĮįıļľŁłńňőřśšťũūůűųźŻżŽžơưșțΆΈΌΐΑΒΓΔΕΖΗΘΚΜΝΠΡΣΤΥΦάέήίαβγδεζηθικλμνξοπρςστυφχψωϊόύώАБВГДЕЖЗИКЛМНОПРСТУХЦЧШЪЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяёђєіїјљњћאבגדהוזחטיכלםמןנסעפץצקרשת،ءآأإئابةتجحخدذرزسشصضطظعغفقكلمنهوىيٹپچڈکگںھہیےংকচতধনফবযরলশষস়ািু্చయలిెొ్ംഅആഇഈഉഎഓകഗങചജഞടഡണതദധനപഫബഭമയരറലളവശസഹാിീുൂെേൈ്ൺൻർൽൾაბგდევზთილმნოპრსტუფქყშჩცძჭხạảấầẩậắặẹẽếềểệỉịọỏốồổỗộớờởợụủứừửữựỳ—、一上不个中为主乾了些亮人任低佔何作供依侧係個側偵充光入全关冇冷几切到制前動區卧厅厨及口另右吊后吗启吸呀咗哪唔問啟嗎嘅嘛器圍在场執場外多大始安定客室家密寵对將小少左已帘常幫幾库度庫廊廚廳开式後恆感態成我戲戶户房所扇手打执把拔换掉控插摄整斯新明是景暗更最會有未本模機檯櫃欄次正氏水沒没洗活派温測源溫漏潮激濕灯為無煙照熱燈燥物狀玄现現瓦用發的盞目着睡私空窗立笛管節簾籬紅線红罐置聚聲脚腦腳臥色节著行衣解設調請謝警设调走路車车运連遊運過道邊部都量鎖锁門閂閉開關门闭除隱離電震霧面音頂題顏颜風风食餅餵가간감갔강개거게겨결경고공과관그금급기길깥꺼껐꼽나난내네놀누는능니다닫담대더데도동됐되된됨둡드든등디때떤뜨라래러렇렌려로료른를리림링마많명몇모무문물뭐바밝방배변보부불블빨뽑사산상색서설성세센션소쇼수스습시신실싱아안않알았애야어얼업없었에여연열옆오온완외왼요운움워원위으은을음의이인일임입있작잠장재전절정제져조족종주줄중줘지직진짐쪽차창천최추출충치침커컴켜켰쿠크키탁탄태탬터텔통트튼티파팬퍼폰표퓨플핑한함해했행혀현화활후휴힘,?"
# Add support for non-unicode characters by using better glyphset
font_glyphsets: "GF_Latin_Core"
# for Greek use "Noto Sans" for other languages use a compatible font family
font_family: Figtree
micro_wake_word_model: okay_nabu
esphome:
name: ${name}
friendly_name: ${friendly_name}
min_version: 2025.2.0
on_boot:
- priority: 600
then:
- delay: 0.4s
- script.execute: draw_display
- component.update: batpercent
- delay: 30s
- if:
condition:
lambda: return id(init_in_progress);
then:
- lambda: id(init_in_progress) = false;
- script.execute: draw_display
- pcf85063.read_time
- component.update: batpercent
esp32:
board: esp32-s3-devkitc-1
flash_size: 16MB
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"
CONFIG_SPIRAM_FETCH_INSTRUCTIONS: y
CONFIG_SPIRAM_RODATA: y
CONFIG_FATFS_LFN_STACK: "y"
advanced:
enable_idf_experimental_features: true
# external_components:
# source: github://n-serrette/esphome_sd_card
# sd_mmc_card:
# id: main_sd_mmc_card
# mode_1bit: true
# clk_pin: GPIO14
# cmd_pin: GPIO17
# data0_pin: GPIO16
# sd_mmc_card.create_directory:
# path: "/test"
time:
- platform: pcf85063
id: pcf85063_time
# repeated synchronization is not necessary unless the external RTC
# is much more accurate than the internal clock
update_interval: never
- platform: homeassistant
# instead try to synchronize via network repeatedly ...
on_time_sync:
then:
# ... and update the RTC when the synchronization was successful
pcf85063.write_time:
psram:
mode: octal
speed: 80MHz # 120MHz
sensor:
- platform: adc
name: "Battery Voltage"
attenuation: auto
id: batvolt
pin: GPIO8
accuracy_decimals: 3
device_class: "voltage"
entity_category: "diagnostic"
update_interval: 1s
unit_of_measurement: "V"
icon: mdi:battery-medium
filters:
- multiply: 3.09
- median:
window_size: 7
send_every: 7
send_first_at: 7
- throttle: 15min
on_value:
then:
- component.update: batpercent
- platform: template
name: "Battery level"
id: batpercent
lambda: return id(batvolt).state;
accuracy_decimals: 0
unit_of_measurement: "%"
icon: mdi:battery-medium
device_class: "battery"
entity_category: "diagnostic"
filters:
- calibrate_linear:
method: exact
datapoints:
- 0.00 -> 0.0
- 3.30 -> 1.0
- 3.39 -> 10.0
- 3.75 -> 50.0
- 4.11 -> 90.0
- 4.20 -> 100.0
- clamp:
min_value: 0
max_value: 100
ignore_out_of_range: false
logger:
api:
encryption:
key: !secret encryption_key
on_client_connected:
- script.execute: draw_display
on_client_disconnected:
- script.execute: draw_display
ota:
- platform: esphome
id: ota_esphome
password: !secret ota_password
on_begin:
then:
- logger.log: "OTA started!"
- lambda: |-
id(ota_active) = true;
- script.execute: draw_display
- lambda: |-
id(main_display).update();
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
on_connect:
- script.execute: draw_display
on_disconnect:
- script.execute: draw_display
captive_portal:
button:
- platform: factory_reset
id: factory_reset_btn
internal: true
web_server:
port: 80
version: 2
include_internal: true
i2s_audio:
- id: i2s_in
i2s_lrclk_pin: GPIO2
i2s_bclk_pin: GPIO15
- id: i2s_out
i2s_lrclk_pin: GPIO38
i2s_bclk_pin: GPIO48
speaker:
- platform: i2s_audio
id: i2s_speaker
i2s_audio_id: i2s_out
i2s_dout_pin: GPIO47
dac_type: external
channel: stereo
microphone:
- platform: i2s_audio
id: i2s_microphone
i2s_audio_id: i2s_in
i2s_din_pin: GPIO39
adc_type: external
pdm: false
channel: right
sample_rate: 16000
bits_per_sample: 16bit
binary_sensor:
- platform: gpio
pin:
number: GPIO0
mode: INPUT_PULLUP
inverted: true
name: "Boot Button"
on_multi_click:
- timing:
- ON for at most 500ms
- OFF for at least 200ms
then:
- if:
condition:
- not:
- microphone.is_muted:
then:
- if:
condition:
- voice_assistant.is_running
then:
- voice_assistant.stop:
else:
- voice_assistant.start:
- timing:
- ON for at most 500ms
- OFF for at most 400ms
- ON for at most 500ms
then:
- if:
condition:
- microphone.is_muted:
then:
- microphone.unmute:
- globals.set:
id: mic_muted
value: 'false'
else:
- microphone.mute:
- globals.set:
id: mic_muted
value: 'true'
- script.execute: draw_display
- timing:
- ON for at least 10s
then:
- button.press: factory_reset_btn
i2c:
sda: GPIO11
scl: GPIO10
pca9554:
- id: 'pca9554a_device'
output:
- platform: ledc
pin: GPIO5
id: backlight_output
light:
- platform: monochromatic
name: "backlight Light"
icon: "mdi:television"
entity_category: config
output: backlight_output
restore_mode: RESTORE_DEFAULT_ON
default_transition_length: 250ms
spi:
id: display_qspi
type: quad
clk_pin: GPIO40
data_pins: [GPIO46, GPIO45, GPIO42, GPIO41]
touchscreen:
platform: cst816
id: my_touchscreen
interrupt_pin: GPIO4
reset_pin:
pca9554: pca9554a_device
number: 0
display: main_display
on_touch:
- lambda: |-
ESP_LOGI("cal", "x=%d, y=%d, x_raw=%d, y_raw=%0d",
touch.x,
touch.y,
touch.x_raw,
touch.y_raw
);
display:
- platform: qspi_dbi
model: CUSTOM
data_rate: 40MHz
invert_colors: true
id: main_display
spi_id: display_qspi
color_order: rgb
dimensions:
height: 360
width: 360
cs_pin: GPIO21
reset_pin:
pca9554: pca9554a_device
number: 1
auto_clear_enabled: true #set to false for LVGL
update_interval: never
init_sequence:
- [ 0xF0, 0x08 ]
- [ 0xF2, 0x08 ]
- [ 0x9B, 0x51 ]
- [ 0x86, 0x53 ]
- [ 0xF2, 0x80 ]
- [ 0xF0, 0x00 ]
- [ 0xF0, 0x01 ]
- [ 0xF1, 0x01 ]
- [ 0xB0, 0x54 ]
- [ 0xB1, 0x3F ]
- [ 0xB2, 0x2A ]
- [ 0xB4, 0x46 ]
- [ 0xB5, 0x34 ]
- [ 0xB6, 0xD5 ]
- [ 0xB7, 0x30 ]
- [ 0xBA, 0x00 ]
- [ 0xBB, 0x08 ]
- [ 0xBC, 0x08 ]
- [ 0xBD, 0x00 ]
- [ 0xC0, 0x80 ]
- [ 0xC1, 0x10 ]
- [ 0xC2, 0x37 ]
- [ 0xC3, 0x80 ]
- [ 0xC4, 0x10 ]
- [ 0xC5, 0x37 ]
- [ 0xC6, 0xA9 ]
- [ 0xC7, 0x41 ]
- [ 0xC8, 0x51 ]
- [ 0xC9, 0xA9 ]
- [ 0xCA, 0x41 ]
- [ 0xCB, 0x51 ]
- [ 0xD0, 0x91 ]
- [ 0xD1, 0x68 ]
- [ 0xD2, 0x69 ]
- [ 0xF5, 0x00, 0xA5 ]
- [ 0xDD, 0x3F ]
- [ 0xDE, 0x3F ]
- [ 0xF1, 0x10 ]
- [ 0xF0, 0x00 ]
- [ 0xF0, 0x02 ]
- [ 0xE0, 0xF0, 0x06, 0x0B, 0x09, 0x09, 0x16, 0x32, 0x44, 0x4A, 0x37, 0x13, 0x13, 0x2E, 0x34 ]
- [ 0xE1, 0xF0, 0x06, 0x0B, 0x09, 0x08, 0x05, 0x32, 0x33, 0x49, 0x17, 0x13, 0x13, 0x2E, 0x34 ]
- [ 0xF0, 0x10 ]
- [ 0xF3, 0x10 ]
- [ 0xE0, 0x0A ]
- [ 0xE1, 0x00 ]
- [ 0xE2, 0x00 ]
- [ 0xE3, 0x00 ]
- [ 0xE4, 0xE0 ]
- [ 0xE5, 0x06 ]
- [ 0xE6, 0x21 ]
- [ 0xE7, 0x00 ]
- [ 0xE8, 0x05 ]
- [ 0xE9, 0x82 ]
- [ 0xEA, 0xDF ]
- [ 0xEB, 0x89 ]
- [ 0xEC, 0x20 ]
- [ 0xED, 0x14 ]
- [ 0xEE, 0xFF ]
- [ 0xEF, 0x00 ]
- [ 0xF8, 0xFF ]
- [ 0xF9, 0x00 ]
- [ 0xFA, 0x00 ]
- [ 0xFB, 0x30 ]
- [ 0xFC, 0x00 ]
- [ 0xFD, 0x00 ]
- [ 0xFE, 0x00 ]
- [ 0xFF, 0x00 ]
- [ 0x60, 0x42 ]
- [ 0x61, 0xE0 ]
- [ 0x62, 0x40 ]
- [ 0x63, 0x40 ]
- [ 0x64, 0x02 ]
- [ 0x65, 0x00 ]
- [ 0x66, 0x40 ]
- [ 0x67, 0x03 ]
- [ 0x68, 0x00 ]
- [ 0x69, 0x00 ]
- [ 0x6A, 0x00 ]
- [ 0x6B, 0x00 ]
- [ 0x70, 0x42 ]
- [ 0x71, 0xE0 ]
- [ 0x72, 0x40 ]
- [ 0x73, 0x40 ]
- [ 0x74, 0x02 ]
- [ 0x75, 0x00 ]
- [ 0x76, 0x40 ]
- [ 0x77, 0x03 ]
- [ 0x78, 0x00 ]
- [ 0x79, 0x00 ]
- [ 0x7A, 0x00 ]
- [ 0x7B, 0x00 ]
- [ 0x80, 0x48 ]
- [ 0x81, 0x00 ]
- [ 0x82, 0x05 ]
- [ 0x83, 0x02 ]
- [ 0x84, 0xDD ]
- [ 0x85, 0x00 ]
- [ 0x86, 0x00 ]
- [ 0x87, 0x00 ]
- [ 0x88, 0x48 ]
- [ 0x89, 0x00 ]
- [ 0x8A, 0x07 ]
- [ 0x8B, 0x02 ]
- [ 0x8C, 0xDF ]
- [ 0x8D, 0x00 ]
- [ 0x8E, 0x00 ]
- [ 0x8F, 0x00 ]
- [ 0x90, 0x48 ]
- [ 0x91, 0x00 ]
- [ 0x92, 0x09 ]
- [ 0x93, 0x02 ]
- [ 0x94, 0xE1 ]
- [ 0x95, 0x00 ]
- [ 0x96, 0x00 ]
- [ 0x97, 0x00 ]
- [ 0x98, 0x48 ]
- [ 0x99, 0x00 ]
- [ 0x9A, 0x0B ]
- [ 0x9B, 0x02 ]
- [ 0x9C, 0xE3 ]
- [ 0x9D, 0x00 ]
- [ 0x9E, 0x00 ]
- [ 0x9F, 0x00 ]
- [ 0xA0, 0x48 ]
- [ 0xA1, 0x00 ]
- [ 0xA2, 0x04 ]
- [ 0xA3, 0x02 ]
- [ 0xA4, 0xDC ]
- [ 0xA5, 0x00 ]
- [ 0xA6, 0x00 ]
- [ 0xA7, 0x00 ]
- [ 0xA8, 0x48 ]
- [ 0xA9, 0x00 ]
- [ 0xAA, 0x06 ]
- [ 0xAB, 0x02 ]
- [ 0xAC, 0xDE ]
- [ 0xAD, 0x00 ]
- [ 0xAE, 0x00 ]
- [ 0xAF, 0x00 ]
- [ 0xB0, 0x48 ]
- [ 0xB1, 0x00 ]
- [ 0xB2, 0x08 ]
- [ 0xB3, 0x02 ]
- [ 0xB4, 0xE0 ]
- [ 0xB5, 0x00 ]
- [ 0xB6, 0x00 ]
- [ 0xB7, 0x00 ]
- [ 0xB8, 0x48 ]
- [ 0xB9, 0x00 ]
- [ 0xBA, 0x0A ]
- [ 0xBB, 0x02 ]
- [ 0xBC, 0xE2 ]
- [ 0xBD, 0x00 ]
- [ 0xBE, 0x00 ]
- [ 0xBF, 0x00 ]
- [ 0xC0, 0x12 ]
- [ 0xC1, 0xAA ]
- [ 0xC2, 0x65 ]
- [ 0xC3, 0x74 ]
- [ 0xC4, 0x47 ]
- [ 0xC5, 0x56 ]
- [ 0xC6, 0x00 ]
- [ 0xC7, 0x88 ]
- [ 0xC8, 0x99 ]
- [ 0xC9, 0x33 ]
- [ 0xD0, 0x21 ]
- [ 0xD1, 0xAA ]
- [ 0xD2, 0x65 ]
- [ 0xD3, 0x74 ]
- [ 0xD4, 0x47 ]
- [ 0xD5, 0x56 ]
- [ 0xD6, 0x00 ]
- [ 0xD7, 0x88 ]
- [ 0xD8, 0x99 ]
- [ 0xD9, 0x33 ]
- [ 0xF3, 0x01 ]
- [ 0xF0, 0x00 ]
- [ 0xF0, 0x01 ]
- [ 0xF1, 0x01 ]
- [ 0xA0, 0x0B ]
- [ 0xA3, 0x2A ]
- [ 0xA5, 0xC3 ]
- delay 1ms
- [ 0xA3, 0x2B ]
- [ 0xA5, 0xC3 ]
- delay 1ms
- [ 0xA3, 0x2C ]
- [ 0xA5, 0xC3 ]
- delay 1ms
- [ 0xA3, 0x2D ]
- [ 0xA5, 0xC3 ]
- delay 1ms
- [ 0xA3, 0x2E ]
- [ 0xA5, 0xC3 ]
- delay 1ms
- [ 0xA3, 0x2F ]
- [ 0xA5, 0xC3 ]
- delay 1ms
- [ 0xA3, 0x30 ]
- [ 0xA5, 0xC3 ]
- delay 1ms
- [ 0xA3, 0x31 ]
- [ 0xA5, 0xC3 ]
- delay 1ms
- [ 0xA3, 0x32 ]
- [ 0xA5, 0xC3 ]
- delay 1ms
- [ 0xA3, 0x33 ]
- [ 0xA5, 0xC3 ]
- delay 1ms
- [ 0xA0, 0x09 ]
- [ 0xF1, 0x10 ]
- [ 0xF0, 0x00 ]
- [ 0x2A, 0x00, 0x00, 0x01, 0x67 ]
- [ 0x2B, 0x01, 0x68, 0x01, 0x68 ]
- [ 0x4D, 0x00 ]
- [ 0x4E, 0x00 ]
- [ 0x4F, 0x00 ]
- [ 0x4C, 0x01 ]
- delay 10ms
- [ 0x4C, 0x00 ]
- [ 0x2A, 0x00, 0x00, 0x01, 0x67 ]
- [ 0x2B, 0x00, 0x00, 0x01, 0x67 ]
- [ 0x3A, 0x55 ]
- [ 0x21, 0x00 ]
- [ 0x11, 0x00 ]
- delay 120ms
- [ 0x29, 0x00 ]
pages:
- id: idle_page
lambda: |-
it.fill(id(idle_color));
if (id(ota_active)) {
it.filled_circle(70, 50, 3, Color(255, 255, 0));
}
if (id(mic_muted)) {
it.filled_circle(290, 50, 3, Color(255, 0, 0));
}
it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_idle), ImageAlign::CENTER);
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);
it.filled_rectangle(55 , 60 , 250 , 30 , Color::WHITE );
it.rectangle(55 , 60 , 250 , 30 , Color::BLACK );
it.printf(65, 65, 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);
it.filled_rectangle(55 , 60 , 250 , 30 , Color::WHITE );
it.rectangle(55 , 60 , 250 , 30 , Color::BLACK );
it.filled_rectangle(90 , 290 , 180 , 30 , Color::WHITE );
it.rectangle(90 , 290 , 180 , 30 , Color::BLACK );
it.printf(65, 65, id(font_request), Color::BLACK, "%s", id(text_request).state.c_str());
it.printf(100, 295, 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);
id(draw_timer_timeline).execute();
id(draw_active_timer_widget).execute();
media_player:
- platform: speaker
name: None
id: speaker_media_player
volume_min: 0.5
volume_max: 0.9
announcement_pipeline:
speaker: i2s_speaker
format: FLAC
sample_rate: 48000
files:
- id: timer_finished_sound
file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/timer_finished.flac
on_announcement:
- if:
condition:
microphone.is_capturing:
then:
- script.execute: stop_voice_assistant
- if:
condition:
not:
voice_assistant.is_running:
then:
- lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
- script.execute: draw_display
on_idle:
- script.execute: start_voice_assistant
- script.execute: draw_display
micro_wake_word:
models:
- ${micro_wake_word_model}
on_wake_word_detected:
- voice_assistant.start:
wake_word: !lambda return wake_word;
voice_assistant:
id: va
microphone: i2s_microphone
use_wake_word: true
noise_suppression_level: 2 #4
auto_gain: 31dBFS
media_player: speaker_media_player
volume_multiplier: 2.0 # 8.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:
and:
- not:
media_player.is_announcing:
- not:
voice_assistant.is_running:
- 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
- if:
condition:
and:
- switch.is_off: mute
- lambda: return id(wake_word_engine_location).state == "On device";
- lambda: return id(voice_assistant_phase) != ${voice_assist_timer_finished_phase_id};
then:
- micro_wake_word.start:
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_voice_assistant
- script.execute: draw_display
on_client_disconnected:
- script.execute: stop_voice_assistant
- 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
switch:
- platform: gpio
pin: GPIO18
id: lcd_TE
- platform: template
name: "Speaker Mute"
id: speaker_mute_switch
entity_category: config
disabled_by_default: true
turn_on_action:
- speaker.mute_on:
- globals.set:
id: speaker_muted
value: 'true'
- logger.log: "Speaker muted"
turn_off_action:
- speaker.mute_off:
- globals.set:
id: speaker_muted
value: 'false'
- logger.log: "Speaker unmuted"
restore_mode: RESTORE_DEFAULT_OFF
- platform: template
name: "Microphone Mute"
id: mute
icon: "mdi:microphone-off"
optimistic: true
restore_mode: RESTORE_DEFAULT_OFF
entity_category: config
on_turn_off:
- if:
condition:
lambda: return !id(init_in_progress);
then:
- lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
- if:
condition:
not:
- voice_assistant.is_running
then:
- if:
condition:
lambda: return id(wake_word_engine_location).state == "In Home Assistant";
then:
- lambda: id(va).set_use_wake_word(true);
- voice_assistant.start_continuous
- 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: draw_display
on_turn_on:
- if:
condition:
lambda: return !id(init_in_progress);
then:
- lambda: id(va).set_use_wake_word(false);
- voice_assistant.stop
- micro_wake_word.stop
- 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:
# Turn off the repeat mode and disable the pause between playlist items
- 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);
# Stop playing the alarm
- media_player.stop:
announcement: true
on_turn_on:
# Turn on the repeat mode and pause for 1000 ms between playlist items/repeats
- 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
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: ota_active
type: bool
initial_value: 'false'
- id: mic_muted
type: bool
restore_value: no
initial_value: 'false'
- id: speaker_muted
type: bool
restore_value: no
initial_value: 'false'
- 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
image:
- file: ${error_illustration_file}
id: casita_error
resize: 320x240
type: RGB
transparency: alpha_channel
- file: ${idle_illustration_file}
id: casita_idle
resize: 320x240
type: RGB
transparency: alpha_channel
- file: ${listening_illustration_file}
id: casita_listening
resize: 320x240
type: RGB
transparency: alpha_channel
- file: ${thinking_illustration_file}
id: casita_thinking
resize: 320x240
type: RGB
transparency: alpha_channel
- file: ${replying_illustration_file}
id: casita_replying
resize: 320x240
type: RGB
transparency: alpha_channel
- file: ${timer_finished_illustration_file}
id: casita_timer_finished
resize: 320x240
type: RGB
transparency: alpha_channel
- file: ${loading_illustration_file}
id: casita_initializing
resize: 320x240
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: 320x240
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: 320x240
type: RGB
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_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"
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(main_display).show_page(listening_page);
id(main_display).update();
break;
case ${voice_assist_thinking_phase_id}:
id(main_display).show_page(thinking_page);
id(main_display).update();
break;
case ${voice_assist_replying_phase_id}:
id(main_display).show_page(replying_page);
id(main_display).update();
break;
case ${voice_assist_error_phase_id}:
id(main_display).show_page(error_page);
id(main_display).update();
break;
case ${voice_assist_muted_phase_id}:
id(main_display).show_page(muted_page);
id(main_display).update();
break;
case ${voice_assist_not_ready_phase_id}:
id(main_display).show_page(no_ha_page);
id(main_display).update();
break;
case ${voice_assist_timer_finished_phase_id}:
id(main_display).show_page(timer_finished_page);
id(main_display).update();
break;
default:
id(main_display).show_page(idle_page);
id(main_display).update();
}
else:
- display.page.show: no_ha_page
- component.update: main_display
else:
- display.page.show: no_wifi_page
- component.update: main_display
else:
- display.page.show: initializing_page
- component.update: main_display
- 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<uint32_t>(1)) );
if (active_pixels > 0){
id(main_display).filled_rectangle(0 , 225 , 320 , 15 , Color::WHITE );
id(main_display).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<uint32_t>(1)));
if (active_pixels > 0){
id(main_display).filled_rectangle(0 , 225 , 320 , 15 , Color::WHITE );
id(main_display).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(main_display).filled_rectangle(80 , 40 , 160 , 50 , Color::WHITE );
id(main_display).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(main_display).printf(120, 47, id(font_timer), Color::BLACK, "%s", display_string.c_str());
}
- id: start_voice_assistant
then:
- if:
condition:
switch.is_off: mute
then:
- if:
condition:
lambda: return id(wake_word_engine_location).state == "In Home Assistant";
then:
- lambda: id(va).set_use_wake_word(true);
- voice_assistant.start_continuous:
- 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
- lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
else:
- lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
- id: stop_voice_assistant
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:
- voice_assistant.stop:
- micro_wake_word.stop:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment