865 lines
24 KiB
YAML
865 lines
24 KiB
YAML
# Waveshare ESP32-S3-Touch-LCD-1.85C-V2 — voice satellite + status display.
|
|
#
|
|
# See firmware/esp32-s3-touch-lcd-1.85c/README.md before flashing this: it needs
|
|
# the V2 hardware revision specifically (V1 has no echo-cancellation circuit and
|
|
# different audio pins), and two entity IDs in secrets.yaml filled in before the
|
|
# display shows anything real.
|
|
#
|
|
# Hardware bring-up (esp32/psram/i2c/pca9554/spi/display/touchscreen/i2s_audio
|
|
# sections) is adapted from the community-verified config at
|
|
# https://github.com/ulsmith/home-assistant-esphome-esp32-s3-touch-lcd-185c —
|
|
# the register-level display init sequence in particular is copied close to
|
|
# verbatim rather than re-derived, since getting it wrong just shows static.
|
|
# Everything voice/UI-behaviour related below that is new, built for this
|
|
# project's specific ask: media-with-cover-art takes priority over an idle
|
|
# weather/time/date cycle, with a voice-state visualizer overlaid on top of
|
|
# whichever of those is showing.
|
|
|
|
substitutions:
|
|
# REQUIRED per physical unit — see README.md's "Multiple rooms" section.
|
|
# This is what makes the device identifiable as belonging to one specific
|
|
# room in both Home Assistant's device list and this file's own hostname;
|
|
# it does NOT by itself make media_player_entity_id (a secret, below)
|
|
# point at the right room's media — that's a separate, equally required
|
|
# step covered in the same README section.
|
|
room: "living-room" # lowercase, hyphens only — becomes part of the hostname
|
|
friendly_name: "Voice display (Living room)"
|
|
# Substitution (not a plain !secret) specifically so it can be interpolated
|
|
# inside larger strings and lambda string literals below, e.g.
|
|
# "${ha_base_url}/local/no_art.png" — !secret alone can't be embedded that way.
|
|
ha_base_url: !secret ha_base_url
|
|
|
|
esphome:
|
|
name: voice-display-${room}
|
|
friendly_name: ${friendly_name}
|
|
|
|
# ESP-IDF (not Arduino) is required for octal PSRAM + this QSPI display driver.
|
|
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"
|
|
|
|
psram:
|
|
mode: octal
|
|
speed: 80MHz
|
|
|
|
logger:
|
|
level: INFO
|
|
|
|
api:
|
|
encryption:
|
|
key: !secret api_encryption_key
|
|
|
|
ota:
|
|
- platform: esphome
|
|
password: !secret ota_password
|
|
|
|
wifi:
|
|
ssid: !secret wifi_ssid
|
|
password: !secret wifi_password
|
|
ap:
|
|
ssid: "voice-display-${room}"
|
|
password: !secret ap_password
|
|
|
|
captive_portal:
|
|
|
|
# Required by online_image below (cover-art fetch) — LAN-only in practice,
|
|
# since ha_base_url always points at the household's own Home Assistant.
|
|
http_request:
|
|
useragent: esphome/voice-display-${room}
|
|
timeout: 10s
|
|
|
|
# Home Assistant's own clock — this device never needs to be right on its own.
|
|
time:
|
|
- platform: homeassistant
|
|
id: ha_time
|
|
on_time:
|
|
- seconds: 0
|
|
then:
|
|
- lvgl.label.update:
|
|
id: lbl_time
|
|
text: !lambda "return id(ha_time).now().strftime(\"%H:%M\");"
|
|
- lvgl.label.update:
|
|
id: lbl_date
|
|
text: !lambda "return id(ha_time).now().strftime(\"%a %d %b\");"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Display + touch (ST77916 360x360 round QSPI, CST816 touch) — Phase 13-style
|
|
# "copied from a verified source, not re-derived" note applies to the whole
|
|
# i2c/pca9554/spi/display block below.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
i2c:
|
|
sda: GPIO11
|
|
scl: GPIO10
|
|
scan: true
|
|
|
|
# I/O expander that carries the display's reset line — the V2 variant
|
|
# specifically resets via EXIO2 (pca9554 pin 2), not a plain GPIO.
|
|
pca9554:
|
|
- id: pca9554_device
|
|
address: 0x20
|
|
|
|
output:
|
|
- platform: ledc
|
|
pin: GPIO5
|
|
id: backlight_pwm
|
|
|
|
light:
|
|
- platform: monochromatic
|
|
output: backlight_pwm
|
|
id: display_backlight
|
|
name: "Display backlight"
|
|
restore_mode: ALWAYS_ON
|
|
|
|
spi:
|
|
id: display_qspi
|
|
type: quad
|
|
clk_pin: GPIO40
|
|
data_pins: [GPIO46, GPIO45, GPIO42, GPIO41]
|
|
|
|
display:
|
|
- platform: qspi_dbi
|
|
model: CUSTOM
|
|
id: main_display
|
|
spi_id: display_qspi
|
|
cs_pin: GPIO21
|
|
reset_pin:
|
|
pca9554: pca9554_device
|
|
number: 2 # EXIO2 — V2-variant-specific, see README
|
|
data_rate: 80MHz
|
|
dimensions:
|
|
width: 360
|
|
height: 360
|
|
color_order: rgb
|
|
invert_colors: true
|
|
auto_clear_enabled: false
|
|
update_interval: never
|
|
# Two lines to change if the unit needs mounting upside down (see README):
|
|
# rotation: 180° and touchscreen.transform.mirror_x/mirror_y: true below.
|
|
rotation: 0°
|
|
# Official Waveshare V2 init sequence — do not reorder or trim these lines.
|
|
init_sequence:
|
|
- [0xF0, 0x28]
|
|
- [0xF2, 0x28]
|
|
- [0x73, 0xF0]
|
|
- [0x7C, 0xD1]
|
|
- [0x83, 0xE0]
|
|
- [0x84, 0x61]
|
|
- [0xF2, 0x82]
|
|
- [0xF0, 0x00]
|
|
- [0xF0, 0x01]
|
|
- [0xF1, 0x01]
|
|
- [0xB0, 0x56]
|
|
- [0xB1, 0x4D]
|
|
- [0xB2, 0x24]
|
|
- [0xB4, 0x87]
|
|
- [0xB5, 0x44]
|
|
- [0xB6, 0x8B]
|
|
- [0xB7, 0x40]
|
|
- [0xB8, 0x86]
|
|
- [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, 0x01]
|
|
- [0xC9, 0xA9]
|
|
- [0xCA, 0x41]
|
|
- [0xCB, 0x01]
|
|
- [0xD0, 0x91]
|
|
- [0xD1, 0x68]
|
|
- [0xD2, 0x68]
|
|
- [0xF5, 0x00, 0xA5]
|
|
- [0xDD, 0x4F]
|
|
- [0xDE, 0x4F]
|
|
- [0xF1, 0x10]
|
|
- [0xF0, 0x00]
|
|
- [0xF0, 0x02]
|
|
- [0xE0, 0xF0, 0x0A, 0x10, 0x09, 0x09, 0x36, 0x35, 0x33, 0x4A, 0x29, 0x15, 0x15, 0x2E, 0x34]
|
|
- [0xE1, 0xF0, 0x0A, 0x0F, 0x08, 0x08, 0x05, 0x34, 0x33, 0x4A, 0x39, 0x15, 0x15, 0x2D, 0x33]
|
|
- [0xF0, 0x10]
|
|
- [0xF3, 0x10]
|
|
- [0xE0, 0x07]
|
|
- [0xE1, 0x00]
|
|
- [0xE2, 0x00]
|
|
- [0xE3, 0x00]
|
|
- [0xE4, 0xE0]
|
|
- [0xE5, 0x06]
|
|
- [0xE6, 0x21]
|
|
- [0xE7, 0x01]
|
|
- [0xE8, 0x05]
|
|
- [0xE9, 0x02]
|
|
- [0xEA, 0xDA]
|
|
- [0xEB, 0x00]
|
|
- [0xEC, 0x00]
|
|
- [0xED, 0x0F]
|
|
- [0xEE, 0x00]
|
|
- [0xEF, 0x00]
|
|
- [0xF8, 0x00]
|
|
- [0xF9, 0x00]
|
|
- [0xFA, 0x00]
|
|
- [0xFB, 0x00]
|
|
- [0xFC, 0x00]
|
|
- [0xFD, 0x00]
|
|
- [0xFE, 0x00]
|
|
- [0xFF, 0x00]
|
|
- [0x60, 0x40]
|
|
- [0x61, 0x04]
|
|
- [0x62, 0x00]
|
|
- [0x63, 0x42]
|
|
- [0x64, 0xD9]
|
|
- [0x65, 0x00]
|
|
- [0x66, 0x00]
|
|
- [0x67, 0x00]
|
|
- [0x68, 0x00]
|
|
- [0x69, 0x00]
|
|
- [0x6A, 0x00]
|
|
- [0x6B, 0x00]
|
|
- [0x70, 0x40]
|
|
- [0x71, 0x03]
|
|
- [0x72, 0x00]
|
|
- [0x73, 0x42]
|
|
- [0x74, 0xD8]
|
|
- [0x75, 0x00]
|
|
- [0x76, 0x00]
|
|
- [0x77, 0x00]
|
|
- [0x78, 0x00]
|
|
- [0x79, 0x00]
|
|
- [0x7A, 0x00]
|
|
- [0x7B, 0x00]
|
|
- [0x80, 0x48]
|
|
- [0x81, 0x00]
|
|
- [0x82, 0x06]
|
|
- [0x83, 0x02]
|
|
- [0x84, 0xD6]
|
|
- [0x85, 0x04]
|
|
- [0x86, 0x00]
|
|
- [0x87, 0x00]
|
|
- [0x88, 0x48]
|
|
- [0x89, 0x00]
|
|
- [0x8A, 0x08]
|
|
- [0x8B, 0x02]
|
|
- [0x8C, 0xD8]
|
|
- [0x8D, 0x04]
|
|
- [0x8E, 0x00]
|
|
- [0x8F, 0x00]
|
|
- [0x90, 0x48]
|
|
- [0x91, 0x00]
|
|
- [0x92, 0x0A]
|
|
- [0x93, 0x02]
|
|
- [0x94, 0xDA]
|
|
- [0x95, 0x04]
|
|
- [0x96, 0x00]
|
|
- [0x97, 0x00]
|
|
- [0x98, 0x48]
|
|
- [0x99, 0x00]
|
|
- [0x9A, 0x0C]
|
|
- [0x9B, 0x02]
|
|
- [0x9C, 0xDC]
|
|
- [0x9D, 0x04]
|
|
- [0x9E, 0x00]
|
|
- [0x9F, 0x00]
|
|
- [0xA0, 0x48]
|
|
- [0xA1, 0x00]
|
|
- [0xA2, 0x05]
|
|
- [0xA3, 0x02]
|
|
- [0xA4, 0xD5]
|
|
- [0xA5, 0x04]
|
|
- [0xA6, 0x00]
|
|
- [0xA7, 0x00]
|
|
- [0xA8, 0x48]
|
|
- [0xA9, 0x00]
|
|
- [0xAA, 0x07]
|
|
- [0xAB, 0x02]
|
|
- [0xAC, 0xD7]
|
|
- [0xAD, 0x04]
|
|
- [0xAE, 0x00]
|
|
- [0xAF, 0x00]
|
|
- [0xB0, 0x48]
|
|
- [0xB1, 0x00]
|
|
- [0xB2, 0x09]
|
|
- [0xB3, 0x02]
|
|
- [0xB4, 0xD9]
|
|
- [0xB5, 0x04]
|
|
- [0xB6, 0x00]
|
|
- [0xB7, 0x00]
|
|
- [0xB8, 0x48]
|
|
- [0xB9, 0x00]
|
|
- [0xBA, 0x0B]
|
|
- [0xBB, 0x02]
|
|
- [0xBC, 0xDB]
|
|
- [0xBD, 0x04]
|
|
- [0xBE, 0x00]
|
|
- [0xBF, 0x00]
|
|
- [0xC0, 0x10]
|
|
- [0xC1, 0x47]
|
|
- [0xC2, 0x56]
|
|
- [0xC3, 0x65]
|
|
- [0xC4, 0x74]
|
|
- [0xC5, 0x88]
|
|
- [0xC6, 0x99]
|
|
- [0xC7, 0x01]
|
|
- [0xC8, 0xBB]
|
|
- [0xC9, 0xAA]
|
|
- [0xD0, 0x10]
|
|
- [0xD1, 0x47]
|
|
- [0xD2, 0x56]
|
|
- [0xD3, 0x65]
|
|
- [0xD4, 0x74]
|
|
- [0xD5, 0x88]
|
|
- [0xD6, 0x99]
|
|
- [0xD7, 0x01]
|
|
- [0xD8, 0xBB]
|
|
- [0xD9, 0xAA]
|
|
- [0xF3, 0x01]
|
|
- [0xF0, 0x00]
|
|
- [0x3A, 0x55] # RGB565
|
|
- [0x36, 0x00] # MADCTL
|
|
- [0x2A, 0x00, 0x00, 0x01, 0x67] # column address 0-359
|
|
- [0x2B, 0x00, 0x00, 0x01, 0x67] # row address 0-359
|
|
- [0x21, 0x00] # display inversion on
|
|
- [0x11, 0x00] # sleep out
|
|
- delay 120ms
|
|
- [0x29, 0x00] # display on
|
|
- delay 20ms
|
|
|
|
touchscreen:
|
|
platform: cst816
|
|
id: my_touchscreen
|
|
address: 0x15
|
|
interrupt_pin: GPIO4
|
|
transform:
|
|
mirror_x: false
|
|
mirror_y: false
|
|
on_touch:
|
|
then:
|
|
- light.turn_on:
|
|
id: display_backlight
|
|
brightness: 100%
|
|
- lvgl.resume:
|
|
|
|
# Narrow edge strips for swipe-to-navigate between the auto-managed idle/media
|
|
# pages and the two manual pages (settings, device info) — same shape as the
|
|
# reference config's own swipe zones.
|
|
binary_sensor:
|
|
- platform: touchscreen
|
|
touchscreen_id: my_touchscreen
|
|
id: swipe_left
|
|
internal: true
|
|
x_min: 0
|
|
x_max: 60
|
|
y_min: 100
|
|
y_max: 260
|
|
on_press:
|
|
then:
|
|
- lvgl.page.previous:
|
|
- platform: touchscreen
|
|
touchscreen_id: my_touchscreen
|
|
id: swipe_right
|
|
internal: true
|
|
x_min: 300
|
|
x_max: 360
|
|
y_min: 100
|
|
y_max: 260
|
|
on_press:
|
|
then:
|
|
- lvgl.page.next:
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Audio: ES7210 mic ADC (with the board's echo-cancellation reference path)
|
|
# + ES8311 speaker DAC, wired for on-device wake word (micro_wake_word) plus
|
|
# the existing Phase 3 Assist pipeline for everything after the wake word.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
i2s_audio:
|
|
- id: i2s_in
|
|
i2s_lrclk_pin: GPIO2
|
|
i2s_bclk_pin: GPIO15
|
|
- id: i2s_out
|
|
i2s_lrclk_pin: GPIO38
|
|
i2s_bclk_pin: GPIO48
|
|
|
|
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
|
|
|
|
speaker:
|
|
- platform: i2s_audio
|
|
id: i2s_speaker
|
|
i2s_audio_id: i2s_out
|
|
i2s_dout_pin: GPIO47
|
|
dac_type: external
|
|
i2s_mode: primary
|
|
|
|
# On-device wake-word spotting (TFLite, runs on the ESP32-S3 itself) — matches
|
|
# this project's Phase 11.8 principle that wake-word detection happens locally,
|
|
# not by streaming continuously to Home Assistant. "okay_nabu" is the same
|
|
# phrase as the thin client's VOICE_WAKE_WORD, so the household has one
|
|
# consistent wake word regardless of which kind of satellite answers.
|
|
micro_wake_word:
|
|
microphone:
|
|
microphone: i2s_microphone
|
|
channels: 0
|
|
models:
|
|
- model: okay_nabu
|
|
id: okay_nabu_model
|
|
on_wake_word_detected:
|
|
- micro_wake_word.stop:
|
|
- voice_assistant.start:
|
|
wake_word: !lambda "return wake_word;"
|
|
|
|
voice_assistant:
|
|
id: va
|
|
microphone: i2s_microphone
|
|
speaker: i2s_speaker
|
|
use_wake_word: false # micro_wake_word above starts a session explicitly instead
|
|
noise_suppression_level: 2
|
|
auto_gain: 31dBFS
|
|
volume_multiplier: 2.0
|
|
on_listening:
|
|
- light.turn_on:
|
|
id: display_backlight
|
|
brightness: 100%
|
|
- lvgl.resume:
|
|
- script.execute: show_visualizer_listening
|
|
on_stt_end:
|
|
- script.execute: show_visualizer_thinking
|
|
on_tts_start:
|
|
- script.execute: show_visualizer_replying
|
|
on_tts_end:
|
|
- script.execute: hide_visualizer
|
|
on_end:
|
|
- script.execute: hide_visualizer
|
|
- micro_wake_word.start:
|
|
on_error:
|
|
- script.execute: show_visualizer_error
|
|
- delay: 2s
|
|
- script.execute: hide_visualizer
|
|
- micro_wake_word.start:
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Home Assistant data this device mirrors — both entity IDs are placeholders,
|
|
# see README.md. Neither is guessed at; an unset value just means that part of
|
|
# the idle page/media page stays blank, same "don't build against a guess"
|
|
# rule this repo already applies to Phase 12/13's own unresolved data sources.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
sensor:
|
|
- platform: homeassistant
|
|
id: weather_temperature
|
|
entity_id: !secret weather_entity_id
|
|
attribute: temperature
|
|
on_value:
|
|
then:
|
|
- lvgl.label.update:
|
|
id: lbl_weather_temp
|
|
text: !lambda "return (str_sprintf(\"%.0f\", x) + \"°\");"
|
|
- platform: wifi_signal
|
|
id: wifi_signal_sensor
|
|
update_interval: 60s
|
|
on_value:
|
|
then:
|
|
- lvgl.label.update:
|
|
id: lbl_wifi_signal
|
|
text: !lambda "return (\"WiFi: \" + std::to_string((int) x) + \" dBm\");"
|
|
- platform: uptime
|
|
id: uptime_sensor
|
|
update_interval: 60s
|
|
on_value:
|
|
then:
|
|
- lvgl.label.update:
|
|
id: lbl_uptime
|
|
text: !lambda |-
|
|
int s = (int) x;
|
|
return "Uptime: " + std::to_string(s / 3600) + "h " + std::to_string((s % 3600) / 60) + "m";
|
|
|
|
globals:
|
|
- id: media_is_playing
|
|
type: bool
|
|
restore_value: no
|
|
initial_value: "false"
|
|
- id: idle_showing_weather
|
|
type: bool
|
|
restore_value: no
|
|
initial_value: "false"
|
|
|
|
text_sensor:
|
|
- platform: homeassistant
|
|
id: weather_condition
|
|
entity_id: !secret weather_entity_id
|
|
on_value:
|
|
then:
|
|
- lvgl.label.update:
|
|
id: lbl_weather_cond
|
|
text: !lambda "return x;"
|
|
|
|
- platform: homeassistant
|
|
id: media_state
|
|
entity_id: !secret media_player_entity_id
|
|
on_value:
|
|
then:
|
|
- if:
|
|
condition:
|
|
lambda: 'return x == "playing";'
|
|
then:
|
|
- globals.set:
|
|
id: media_is_playing
|
|
value: "true"
|
|
- lvgl.page.show: media_page
|
|
else:
|
|
- globals.set:
|
|
id: media_is_playing
|
|
value: "false"
|
|
- lvgl.page.show: idle_page
|
|
|
|
- platform: homeassistant
|
|
id: media_title
|
|
entity_id: !secret media_player_entity_id
|
|
attribute: media_title
|
|
on_value:
|
|
then:
|
|
- lvgl.label.update:
|
|
id: lbl_media_title
|
|
text: !lambda "return x;"
|
|
|
|
- platform: homeassistant
|
|
id: media_artist
|
|
entity_id: !secret media_player_entity_id
|
|
attribute: media_artist
|
|
on_value:
|
|
then:
|
|
- lvgl.label.update:
|
|
id: lbl_media_artist
|
|
text: !lambda "return x;"
|
|
|
|
# entity_picture from Home Assistant is a path relative to the HA instance
|
|
# (e.g. "/api/media_player_proxy/...") — this device fetches it directly over
|
|
# HTTP, not through the HA connection, so ha_base_url has to be prepended.
|
|
- platform: homeassistant
|
|
id: media_picture
|
|
entity_id: !secret media_player_entity_id
|
|
attribute: entity_picture
|
|
on_value:
|
|
then:
|
|
- online_image.set_url:
|
|
id: media_cover_image
|
|
url: !lambda |-
|
|
if (x.rfind("http", 0) == 0) return x;
|
|
return std::string("${ha_base_url}") + x;
|
|
|
|
- platform: wifi_info
|
|
ip_address:
|
|
id: wifi_ip
|
|
on_value:
|
|
then:
|
|
- lvgl.label.update:
|
|
id: lbl_ip
|
|
text: !lambda "return (\"IP: \" + x);"
|
|
|
|
# Must be the online_image platform (not a plain local file/mdi image) —
|
|
# that's the only image type online_image.set_url (used above) can retarget
|
|
# at runtime. The initial URL points at Home Assistant's own /local/
|
|
# (config/www/) static folder rather than anywhere external — nothing else
|
|
# this device talks to leaves the LAN, and it stays that way here too. It
|
|
# 404s harmlessly (shows no image, not a broken canvas) until either the
|
|
# first real track plays or you drop a no_art.png into config/www/ yourself
|
|
# — see README.md.
|
|
image:
|
|
- platform: online_image
|
|
id: media_cover_image
|
|
url: "${ha_base_url}/local/no_art.png"
|
|
format: PNG
|
|
type: RGB565
|
|
resize: 220x220
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Screen content
|
|
# ---------------------------------------------------------------------------
|
|
|
|
font:
|
|
- file: "gfonts://Montserrat"
|
|
id: font_time
|
|
size: 54
|
|
- file: "gfonts://Montserrat"
|
|
id: font_date
|
|
size: 20
|
|
- file: "gfonts://Montserrat"
|
|
id: font_weather
|
|
size: 32
|
|
- file: "gfonts://Montserrat"
|
|
id: font_weather_cond
|
|
size: 18
|
|
- file: "gfonts://Montserrat"
|
|
id: font_media
|
|
size: 20
|
|
- file: "gfonts://Montserrat"
|
|
id: font_small
|
|
size: 14
|
|
|
|
# Idle-page auto-cycle: only ever touches idle_page's own two widget groups,
|
|
# and only while media isn't playing — the priority automation above already
|
|
# forces media_page the instant something starts, so this never fights it.
|
|
interval:
|
|
- interval: 8s
|
|
then:
|
|
- if:
|
|
condition:
|
|
lambda: "return !id(media_is_playing);"
|
|
then:
|
|
- globals.set:
|
|
id: idle_showing_weather
|
|
value: !lambda "return !id(idle_showing_weather);"
|
|
- if:
|
|
condition:
|
|
lambda: "return id(idle_showing_weather);"
|
|
then:
|
|
- lvgl.widget.hide: idle_clock_group
|
|
- lvgl.widget.show: idle_weather_group
|
|
else:
|
|
- lvgl.widget.show: idle_clock_group
|
|
- lvgl.widget.hide: idle_weather_group
|
|
|
|
script:
|
|
- id: show_visualizer_listening
|
|
then:
|
|
- lvgl.spinner.update:
|
|
id: voice_visualizer
|
|
arc_color: 0x003300
|
|
indicator:
|
|
arc_color: 0x00FF00
|
|
- lvgl.widget.show: voice_visualizer
|
|
- id: show_visualizer_thinking
|
|
then:
|
|
- lvgl.spinner.update:
|
|
id: voice_visualizer
|
|
arc_color: 0x332200
|
|
indicator:
|
|
arc_color: 0xFF9900
|
|
- lvgl.widget.show: voice_visualizer
|
|
- id: show_visualizer_replying
|
|
then:
|
|
- lvgl.spinner.update:
|
|
id: voice_visualizer
|
|
arc_color: 0x001a33
|
|
indicator:
|
|
arc_color: 0x00AAFF
|
|
- lvgl.widget.show: voice_visualizer
|
|
- id: show_visualizer_error
|
|
then:
|
|
- lvgl.spinner.update:
|
|
id: voice_visualizer
|
|
arc_color: 0x330000
|
|
indicator:
|
|
arc_color: 0xFF0000
|
|
- lvgl.widget.show: voice_visualizer
|
|
- id: hide_visualizer
|
|
then:
|
|
- lvgl.widget.hide: voice_visualizer
|
|
|
|
lvgl:
|
|
displays:
|
|
- main_display
|
|
touchscreens:
|
|
- touchscreen_id: my_touchscreen
|
|
buffer_size: 25%
|
|
color_depth: 16
|
|
bg_color: 0x000000
|
|
|
|
on_idle:
|
|
timeout: 30s
|
|
then:
|
|
- lvgl.pause:
|
|
- light.turn_off:
|
|
id: display_backlight
|
|
transition_length: 5s
|
|
|
|
# Voice-state visualizer: a spinning ring covering the whole screen,
|
|
# regardless of which page is active underneath — "a visualizer when
|
|
# speaking" as an overlay concern, not a page of its own. Hidden by
|
|
# default; the voice_assistant scripts above are the only things that
|
|
# show/hide or recolour it.
|
|
top_layer:
|
|
widgets:
|
|
- spinner:
|
|
id: voice_visualizer
|
|
align: CENTER
|
|
width: 356
|
|
height: 356
|
|
arc_color: 0x003300
|
|
arc_width: 6
|
|
spin_time: 1200ms
|
|
arc_length: 60
|
|
indicator:
|
|
arc_color: 0x00FF00
|
|
arc_width: 6
|
|
hidden: true
|
|
|
|
pages:
|
|
- id: idle_page
|
|
widgets:
|
|
- obj:
|
|
id: idle_clock_group
|
|
width: 100%
|
|
height: 100%
|
|
widgets:
|
|
- label:
|
|
id: lbl_time
|
|
align: CENTER
|
|
y: -20
|
|
text_font: font_time
|
|
text_color: 0xFFFFFF
|
|
text: "--:--"
|
|
- label:
|
|
id: lbl_date
|
|
align: CENTER
|
|
y: 35
|
|
text_font: font_date
|
|
text_color: 0xAAAAAA
|
|
text: "--- -- ---"
|
|
- obj:
|
|
id: idle_weather_group
|
|
width: 100%
|
|
height: 100%
|
|
hidden: true
|
|
widgets:
|
|
- label:
|
|
id: lbl_weather_temp
|
|
align: CENTER
|
|
y: -20
|
|
text_font: font_weather
|
|
text_color: 0x00BFFF
|
|
text: "--°"
|
|
- label:
|
|
id: lbl_weather_cond
|
|
align: CENTER
|
|
y: 25
|
|
text_font: font_weather_cond
|
|
text_color: 0x87CEEB
|
|
text: "Weather loading..."
|
|
|
|
- id: media_page
|
|
widgets:
|
|
- image:
|
|
id: img_media_cover
|
|
src: media_cover_image
|
|
align: CENTER
|
|
y: -40
|
|
- label:
|
|
id: lbl_media_title
|
|
align: CENTER
|
|
y: 90
|
|
text_font: font_media
|
|
text_color: 0xFFFFFF
|
|
text: ""
|
|
- label:
|
|
id: lbl_media_artist
|
|
align: CENTER
|
|
y: 118
|
|
text_font: font_small
|
|
text_color: 0xAAAAAA
|
|
text: ""
|
|
|
|
- id: settings_page
|
|
widgets:
|
|
- label:
|
|
align: TOP_MID
|
|
y: 50
|
|
text_font: font_weather_cond
|
|
text_color: 0xFFFFFF
|
|
text: "Settings"
|
|
- label:
|
|
align: CENTER
|
|
x: -70
|
|
y: 0
|
|
text_font: font_small
|
|
text_color: 0xFFFFFF
|
|
text: "Mute mic"
|
|
- switch:
|
|
id: mute_switch_widget
|
|
align: CENTER
|
|
x: 60
|
|
y: 0
|
|
# Delegates to the HA-exposed template switch below rather than
|
|
# touching micro_wake_word directly — that switch's turn_on_action/
|
|
# turn_off_action already does the real stop/start and reports its
|
|
# new state back to this widget via lvgl.widget.update, so this is
|
|
# the only place that pairing needs to be defined.
|
|
on_click:
|
|
then:
|
|
- switch.toggle: mute_switch
|
|
|
|
- id: info_page
|
|
widgets:
|
|
- label:
|
|
align: TOP_MID
|
|
y: 50
|
|
text_font: font_weather_cond
|
|
text_color: 0xFFFFFF
|
|
text: "Device info"
|
|
- label:
|
|
id: lbl_ip
|
|
align: CENTER
|
|
y: -20
|
|
text_font: font_small
|
|
text_color: 0x00FF00
|
|
text: "IP: connecting..."
|
|
- label:
|
|
id: lbl_wifi_signal
|
|
align: CENTER
|
|
y: 10
|
|
text_font: font_small
|
|
text_color: 0x00FF00
|
|
text: "WiFi: --"
|
|
- label:
|
|
id: lbl_uptime
|
|
align: CENTER
|
|
y: 40
|
|
text_font: font_small
|
|
text_color: 0x00FFFF
|
|
text: "Uptime: --"
|
|
|
|
button:
|
|
- platform: restart
|
|
name: "Restart"
|
|
|
|
switch:
|
|
- platform: template
|
|
name: "Mute microphone"
|
|
id: mute_switch
|
|
icon: "mdi:microphone-off"
|
|
optimistic: true
|
|
turn_on_action:
|
|
- micro_wake_word.stop:
|
|
- lvgl.widget.update:
|
|
id: mute_switch_widget
|
|
state:
|
|
checked: true
|
|
turn_off_action:
|
|
- micro_wake_word.start:
|
|
- lvgl.widget.update:
|
|
id: mute_switch_widget
|
|
state:
|
|
checked: false
|