ESP32-S3 1.8inch AMOLED Touch

I’ve just got one of these devices:

SH8601 Display Driver and FT3168 Touch

It is not officially supported by ESPHome, but I wondered if anyone has managed to get it working and could share an example? I’ve had a search through the forum and I can see some references to working with the SH8601 and FT3168 separately.
Thanks

I can’t find a specific example for this device, but I managed to get a basic configuration together by cribbing bits and pieces from data sheets and various ESPHome and non ESPHome configs that other people have built for similar boards.

The display works successfully, but I can’t get the touch screen working. When I tap the screen, I just get the following errors logged:

[15:20:10.094][D][esp-idf:000]: E (29193) i2c.master: I2C hardware NACK detected
[15:20:10.094][D][esp-idf:000]: E (29194) i2c.master: I2C transaction unexpected nack detected
[15:20:10.096][D][esp-idf:000]: E (29195) i2c.master: s_i2c_synchronous_transaction(945): I2C transaction failed
[15:20:10.098][D][esp-idf:000]: E (29197) i2c.master: i2c_master_execute_defined_operations(1401): I2C transaction failed

I’ve experimented with various frequencies, without success.
Config with working display but non-working touch screen:

esphome:
  name: home-monitor-2

esp32:
  variant: esp32s3
  framework:
    type: esp-idf
  flash_size: 16MB

psram:
  mode: octal

# Enable logging
logger:

# Enable Home Assistant API
api:
  on_client_connected:
    - logger.log: "api_connected"

# Allow Over-The-Air updates
ota:
- platform: esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: 192.168.1.51
    gateway: 192.168.1.254
    subnet: 255.255.255.0

i2c:
  id: i2c_1
  scl: GPIO14
  sda: GPIO15
  frequency: 40kHz
  scan: false

pca9554:
  - id: 'pca9554_device'
  
touchscreen:
  platform: cst816
  display: display_1
  i2c_id: i2c_1
  id: touchscreen_1
  interrupt_pin: GPIO21
  reset_pin: 
    pca9554: pca9554_device
    number: 0
  skip_probe: true
  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
        );
spi:
  type: quad
  clk_pin: GPIO11
  data_pins: [GPIO4, GPIO5, GPIO6, GPIO7]

# Display configuration
display:
  - platform: mipi_spi
    id: display_1
    bus_mode: quad
    dimensions: 
      width: 368
      height: 448
    model: CUSTOM
    cs_pin: GPIO12
    init_sequence:
      - [0x00]
      - delay 120ms
      - [0x01, 0xD1]
      - [0x00]
      - [0x20]
      - delay 10ms
      - [0x00, 0x00, 0x01, 0x6F]
      - [0x00, 0x00, 0x01, 0xBF]
      - [0x00]
      - delay 10ms
      - [0x00]
      - delay 10ms
      - [0xFF]
      
lvgl:
  #buffer_size: 25%
  widgets:
    - label:
        align: CENTER
        text: 'Hello World!'
    - button:
        x: 84
        y: 100
        width: 200
        height: 80
        id: btn_id
        checkable: true
        widgets:
          - label:
              align: center
              text: "Tap me"
        on_value:
          then:
            - logger.log:
                format: "Button checked state: %d"
                args: [ x ]
    - slider:
        id: dimmer_slider
        x: 20
        y: 50
        width: 30
        height: 220
        pad_all: 8
        min_value: 0
        max_value: 255

schematic gives ext02 for touch reset pin…

You’re correct. The “0” in the config I posted was incorrectly included because I wasn’t sure if I was reading the spec correctly and was experimenting with different values.
I get the same error using pin 2, so I think there is something else wrong with my configuration.

Success!
I dug through the Waveshare example repository (GitHub - waveshareteam/ESP32-display-support: Waveshare ESP32 with screen product code summary) and found the two missing pieces of information to get the touchscreen working:

  1. Backtracking on the preceding discussion, pin 0 is actually the correct value for the pca9554 I/O extender reset_pin, not pin 2 (in retrospect this is obvious from the schematic pin names: LCD_RESET EXIO0 rather than TP_RESET EXIO2)
  2. address: 0x38 is required for the touchscreen: definition

I also removed the frequency: value from my i2c: definition and it seems to be running OK on the default.
There are a lot of other features on this device (like audio I/O) that I have not attempted to configure, but it does appear to be functional for display and touch with ESPHome.

esphome:
  name: home-monitor-2

esp32:
  variant: esp32s3
  framework:
    type: esp-idf
  flash_size: 16MB

psram:
  mode: octal

# Enable logging
logger:

# Enable Home Assistant API
api:
  on_client_connected:
    - logger.log: "api_connected"

# Allow Over-The-Air updates
ota:
- platform: esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: 192.168.1.51
    gateway: 192.168.1.254
    subnet: 255.255.255.0

i2c:
  id: i2c_1
  scl: GPIO14
  sda: GPIO15
  scan: false

pca9554:
  - id: 'pca9554_device'
  
touchscreen:
  platform: cst816
  display: display_1
  address: 0x38
  i2c_id: i2c_1
  id: touchscreen_1
  interrupt_pin: GPIO21
  reset_pin: 
    pca9554: pca9554_device
    number: 0
  skip_probe: true
  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
        );
spi:
  type: quad
  clk_pin: GPIO11
  data_pins: [GPIO4, GPIO5, GPIO6, GPIO7]

# Display configuration
display:
  - platform: mipi_spi
    id: display_1
    bus_mode: quad
    dimensions: 
      width: 368
      height: 448
    model: CUSTOM
    cs_pin: GPIO12
    init_sequence:
      - [0x00]
      - delay 120ms
      - [0x01, 0xD1]
      - [0x00]
      - [0x20]
      - delay 10ms
      - [0x00, 0x00, 0x01, 0x6F]
      - [0x00, 0x00, 0x01, 0xBF]
      - [0x00]
      - delay 10ms
      - [0x00]
      - delay 10ms
      - [0xFF]
      
lvgl:
  #buffer_size: 25%
  widgets:
    - label:
        align: CENTER
        text: 'Hello World!'
    - button:
        x: 84
        y: 100
        width: 200
        height: 80
        id: btn_id
        checkable: true
        widgets:
          - label:
              align: center
              text: "Tap me"
        on_value:
          then:
            - logger.log:
                format: "Button checked state: %d"
                args: [ x ]
    - slider:
        id: dimmer_slider
        x: 20
        y: 50
        width: 30
        height: 220
        pad_all: 8
        min_value: 0
        max_value: 255

A small change that fixes some issues and simplifies the config:
I couldn’t get brightness working with the CUSTOM display model: configuration I had cobbled together from some code examples, but discovered that I could drop the custom init_sequence and just use the model: WAVESHARE-ESP32-S3-TOUCH-AMOLED-1.75 option, which seems to work fine (the pin errors in my initial config efforts obscured the fact that this option was valid).

[Edit: I have an unresolved issue with the touchscreen only working immediately after flashing; if I subsequently power cycle the device the touch screen no longer responds ]

Updated config:

esphome:
  name: home-monitor-2

esp32:
  variant: esp32s3
  framework:
    type: esp-idf
  flash_size: 16MB

psram:
  mode: octal

# Enable logging
logger:

# Enable Home Assistant API
api:
  on_client_connected:
    - logger.log: "api_connected"

# Allow Over-The-Air updates
ota:
- platform: esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: 192.168.1.51
    gateway: 192.168.1.254
    subnet: 255.255.255.0

i2c:
  id: i2c_1
  scl: GPIO14
  sda: GPIO15
  scan: false
  frequency: 100kHz

pca9554:
  - id: 'pca9554_device'
  
touchscreen:
  platform: cst816
  display: display_1
  address: 0x38
  i2c_id: i2c_1
  id: touchscreen_1
  interrupt_pin: GPIO21
  reset_pin: 
    pca9554: pca9554_device
    number: 0
  skip_probe: true
  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
        );
spi:
  type: quad
  clk_pin: GPIO11
  data_pins: [GPIO4, GPIO5, GPIO6, GPIO7]

# Display configuration
display:
  - platform: mipi_spi
    id: display_1
    brightness: 150
    bus_mode: quad
    dimensions: 
      width: 368
      height: 448
    model: WAVESHARE-ESP32-S3-TOUCH-AMOLED-1.75
    cs_pin: GPIO12
    # init_sequence:
    #   - [0x00]
    #   - delay 120ms
    #   - [0x01, 0xD1]
    #   - [0x00]
    #   - [0x20]
    #   - delay 10ms
    #   - [0x00, 0x00, 0x01, 0x6F]
    #   - [0x00, 0x00, 0x01, 0xBF]
    #   - [0x00]
    #   - delay 10ms
    #   - [0x00]1
    #   - delay 10ms
    #   - [0xFF]
      
lvgl:
  #buffer_size: 25%
  pages:
    - id: main_page
      bg_color: black
      widgets:
        - label:
            align: CENTER
            text: 'Hello World!'
            text_color: white
        - button:
            bg_color: LightGreen
            x: 84
            y: 100
            width: 200
            height: 80
            id: btn_id
            checkable: true
            checked:
              bg_color: Grey
            widgets:
              - label:
                  align: center
                  text: "Tap me"
            on_value:
              then:
                - logger.log:
                    format: "Button checked state: %d"
                    args: [ x ]
        - slider:
            id: dimmer_slider
            x: 20
            y: 50
            width: 30
            height: 220
            pad_all: 8
            min_value: 0
            max_value: 255

You can get the touch screen working reliably like this:

touchscreen:
  - platform: ft5x06
    id: my_touchscreen
    display: my_display
    interrupt_pin: GPIO21
    address: 0x38

Thanks very much. That was a well timed reply because I was just about to post another message after failing to find a solution to the reboot problem.

I switched to platform: ft5x06 and can confirm that the touchscreen is working correctly when flashed, and also continuing to work when the device is rebooted.

My revised configuration is below. With the fx5x06 plaform, the reset_pin and skip_probe options are no longer valid, and the pca9554 definition is therefore no longer required.

Thanks again!

Revised config:

esphome:
  name: home-monitor-2

esp32:
  variant: esp32s3
  framework:
    type: esp-idf
  flash_size: 16MB

psram:
  mode: octal

# Enable logging
logger:

# Enable Home Assistant API
api:
  on_client_connected:
    - logger.log: "api_connected"

# Allow Over-The-Air updates
ota:
- platform: esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: 192.168.1.51
    gateway: 192.168.1.254
    subnet: 255.255.255.0

i2c:
  id: i2c_1
  scl: GPIO14
  sda: GPIO15
  scan: true
  frequency: 100kHz

touchscreen:
  platform: ft5x06
  display: display_1
  address: 0x38
  i2c_id: i2c_1
  id: touchscreen_1
  interrupt_pin: GPIO21
  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
        );
spi:
  type: quad
  clk_pin: GPIO11
  data_pins: [GPIO4, GPIO5, GPIO6, GPIO7]

# Display configuration
display:
  - platform: mipi_spi
    id: display_1
    brightness: 150
    bus_mode: quad
    dimensions: 
      width: 368
      height: 448
    model: WAVESHARE-ESP32-S3-TOUCH-AMOLED-1.75
    cs_pin: GPIO12
      
lvgl:
  #buffer_size: 25%
  pages:
    - id: main_page
      on_swipe_right:
        - lvgl.page.previous:
      on_swipe_left:
        - lvgl.page.next:
      bg_color: black
      widgets:
        - label:
            align: CENTER
            text: 'Hello World!'
            text_color: white
        - button:
            bg_color: LightGreen
            x: 84
            y: 100
            width: 200
            height: 80
            id: btn_id
            checkable: true
            checked:
              bg_color: Grey
            widgets:
              - label:
                  align: center
                  text: "Tap me"
            on_value:
              then:
                - logger.log:
                    format: "Button checked state: %d"
                    args: [ x ]
        - slider:
            id: dimmer_slider
            x: 20
            y: 50
            width: 30
            height: 220
            pad_all: 8
            min_value: 0
            max_value: 255
    - id: page_2
      on_swipe_right:
        - lvgl.page.previous:
      on_swipe_left:
        - lvgl.page.next:
      bg_color: black
      widgets:
        - label:
            align: CENTER
            text: 'Hello World 2!'
            text_color: white

Audio works fine too, here’s a tap to talk voice assistant with a volume slider:

esphome:
  name: esp-assistant
  friendly_name: esp-assistant

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: esp-idf

psram:
  mode: octal

logger:

api:

ota:

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

captive_portal:

# I2C Bus - Shared by touchscreen (FT5x06) and audio codec (ES8311)
i2c:
  id: i2c_bus
  sda: GPIO15
  scl: GPIO14
  frequency: 200kHz
  scan: true

# Quad SPI Bus - AMOLED display
spi:
  - id: quad_spi
    type: quad
    clk_pin: GPIO11
    data_pins:
      - GPIO4   # D0
      - GPIO5   # D1
      - GPIO6   # D2
      - GPIO7   # D3

# AMOLED Display (368x448)
display:
  - platform: mipi_spi
    id: my_display
    brightness: 150
    bus_mode: quad
    dimensions:
      width: 368
      height: 448
    model: WAVESHARE-ESP32-S3-TOUCH-AMOLED-1.75
    cs_pin: GPIO12

# FT5x06 Capacitive Touchscreen
touchscreen:
  - platform: ft5x06
    id: my_touchscreen
    display: my_display
    interrupt_pin: GPIO21
    address: 0x38
    transform:
      swap_xy: false
      mirror_x: false
      mirror_y: false

# ============================================================================
# AUDIO CONFIGURATION
# ============================================================================

# I2S Audio Bus - Connects ESP32 to ES8311 codec
i2s_audio:
  - id: i2s_audio_bus
    i2s_lrclk_pin: GPIO45  # Word select / left-right clock
    i2s_bclk_pin: GPIO9     # Bit clock
    i2s_mclk_pin: GPIO16    # Master clock

# ES8311 Audio Codec
audio_dac:
  - platform: es8311
    id: es8311_codec
    address: 0x18
    i2c_id: i2c_bus
    use_mclk: true
    use_microphone: false  # Analog microphone (not PDM digital)
    bits_per_sample: 16bit
    sample_rate: 16000
    mic_gain: 18DB

# Speaker - Audio output through ES8311 DAC
speaker:
  - platform: i2s_audio
    id: external_speaker
    dac_type: external
    audio_dac: es8311_codec
    i2s_audio_id: i2s_audio_bus
    i2s_dout_pin: GPIO8
    sample_rate: 16000
    bits_per_sample: 16bit
    timeout: 2s

# Microphone - Audio input through ES8311 ADC
microphone:
  - platform: i2s_audio
    id: external_microphone
    adc_type: external
    i2s_audio_id: i2s_audio_bus
    i2s_din_pin: GPIO10
    bits_per_sample: 16bit
    sample_rate: 16000

# Power Amplifier - GPIO46 controls speaker amplifier enable
switch:
  - platform: gpio
    pin: GPIO46
    id: pa_enable
    name: "Speaker Amplifier"
    restore_mode: ALWAYS_ON
    internal: false

# Volume Control - Synced with ES8311 hardware volume and UI slider
number:
  - platform: template
    id: volume_control
    name: "Volume"
    min_value: 0
    max_value: 100
    initial_value: 70
    step: 5
    optimistic: true
    set_action:
      - lambda: |-
          id(es8311_codec).set_volume(x / 100.0);
          ESP_LOGI("volume", "Volume set to: %.0f%%", x);

# RTTTL Ringtone Player - Plays simple tones and melodies
rtttl:
  id: buzzer
  speaker: external_speaker

# ============================================================================
# VOICE ASSISTANT
# ============================================================================

voice_assistant:
  id: va
  microphone: external_microphone
  speaker: external_speaker

  # Listening state - Button turns blue
  on_listening:
    - logger.log: "Voice assistant started listening"
    - lvgl.label.update:
        id: status_label
        text: "Listening..."
    - lvgl.widget.update:
        id: btn_id
        bg_color: DodgerBlue

  # Speech-to-text complete - Display recognized text
  on_stt_end:
    - logger.log:
        format: "Speech recognized: %s"
        args: [ 'x.c_str()' ]
    - lvgl.label.update:
        id: status_label
        text: !lambda 'return "You said: " + x;'

  # Text-to-speech starting - Display speaking status
  on_tts_start:
    - logger.log: "Speaking response..."
    - lvgl.label.update:
        id: status_label
        text: "Speaking..."

  # Session complete - Button returns to green
  on_end:
    - logger.log: "Voice assistant session ended"
    - lvgl.label.update:
        id: status_label
        text: "Ready"
    - lvgl.widget.update:
        id: btn_id
        bg_color: LightGreen

  # Error handling - Flash red, then return to green
  on_error:
    - logger.log:
        format: "Voice assistant error: %s"
        args: [ 'code.c_str()' ]
    - lvgl.label.update:
        id: status_label
        text: "Error!"
    - lvgl.widget.update:
        id: btn_id
        bg_color: Red
    - delay: 1s
    - lvgl.widget.update:
        id: btn_id
        bg_color: LightGreen
    - lvgl.label.update:
        id: status_label
        text: "Ready"

# ============================================================================
# USER INTERFACE (LVGL)
# ============================================================================

lvgl:
  pages:
    - id: main_page
      bg_color: black
      widgets:
        # Status label - Shows current voice assistant state
        - label:
            id: status_label
            align: CENTER
            y: 50
            text: 'Ready'
            text_color: white

        # Main voice assistant button - Tap to activate
        - button:
            bg_color: LightGreen
            x: 84
            y: 100
            width: 200
            height: 160
            id: btn_id
            widgets:
              - label:
                  align: center
                  text: "Tap to Talk"

            on_click:
              then:
                - if:
                    condition:
                      not: voice_assistant.is_running
                    then:
                      - rtttl.play: 'beep:d=16,o=5,b=100:c6'
                      - delay: 200ms
                      - voice_assistant.start:
                    else:
                      - logger.log: "Ignored tap: Voice Assistant already running"

        # Volume slider - Vertical slider on left side
        - slider:
            id: volume_slider
            x: 20
            y: 50
            width: 30
            height: 308
            pad_all: 8
            min_value: 0
            max_value: 100
            value: 70
            on_value:
              then:
                - logger.log:
                    format: "Volume slider: %d"
                    args: [ x ]
                - number.set:
                    id: volume_control
                    value: !lambda 'return x;'

        # Volume label
        - label:
            x: 60
            y: 180
            text_color: white
            text: "Vol"

Thanks to everyone for you work! I just received this device

Thanks for the previous posts - I started with the example @argsnd gave at the beginning of the month last night. Then today, while thinking about wake words rather than continuous listening, I stumbled across the examples in realdeco xiaozhi-esphome gh repo for the waveshare esp32-s3 1.85C lcd. I replaced the values for gpio pins and display settings and such like for the values in the examples here, and fairly quickly had my first protype up and running.

I’ll do a bit more checking on how it works / behaves, and will also compare with the 206 watch config, as I have seen a comment on using that.

I got mine working with ESP-NOW, LVGL, Touch on ESPHome.

Repo for yaml (along with a few other devices).

The project is for an offline GPS receiver that runs on an ESP32C6 and sends data over to two displays running ESPHome using ESP-NOW

All three ESP32 things (on the left is the ESP32-C6-LCD-1.47

Waveshare ESP32-S3-Touch-AMOLED-1.8:

Sender GPS ESP32C6 also running the dashboard:

Having problems with this one, any ideas?

[15:43:29][D][rtttl:062]: Playing song beep
[15:43:29][D][i2s_audio.speaker:102]: Starting
[15:43:29][D][i2s_audio.speaker:106]: Started
[15:43:29][D][ring_buffer:034][speaker_task]: Created ring buffer with size 16000
[15:43:29][D][voice_assistant:478]: State changed from IDLE to START_MICROPHONE
[15:43:29][D][voice_assistant:485]: Desired state set to START_PIPELINE
[15:43:29][D][voice_assistant:207]: Starting Microphone
[15:43:29][D][ring_buffer:034]: Created ring buffer with size 16384
[15:43:29][D][voice_assistant:478]: State changed from START_MICROPHONE to STARTING_MICROPHONE
[15:43:29][E][i2s_audio.microphone:516]: Driver failed to start; retrying in 1 second
[15:43:29][E][component:362]: i2s_audio.microphone set Error flag: unspecified
[15:43:29][D][i2s_audio.speaker:111]: Stopping
[15:43:29][D][i2s_audio.speaker:116]: Stopped
[15:43:29][D][rtttl:405]: Playback finished
[15:43:30][E][component:379]: i2s_audio.microphone cleared Error flag
[15:43:30][D][voice_assistant:478]: State changed from STARTING_MICROPHONE to START_PIPELINE
[15:43:30][D][voice_assistant:228]: Requesting start
[15:43:30][D][voice_assistant:478]: State changed from START_PIPELINE to STARTING_PIPELINE
[15:43:30][D][voice_assistant:500]: Client started, streaming microphone
[15:43:30][D][voice_assistant:478]: State changed from STARTING_PIPELINE to STREAMING_MICROPHONE
[15:43:30][D][voice_assistant:485]: Desired state set to STREAMING_MICROPHONE

Hi together

I have received my ESP32-S3-Touch-AMOLED-1.8 today.
Thanks or ll the detailed information in this forum about it.

Should i be possible to use this device with the “ESP32-S3-BOX voice assistant” configuration? Has already anybody thy this?

Mine was delivered 2 days ago, thanks to the work others have done setting up the config file. I would like to use this little 1.8 as a remote to control entities in HA for my motorhome. I’m just starting to learn how to set this up. I would like to create several pages with buttons for control, with the status being displayed. Any help would be appreciated.

Has anyone set it up to show the battery percentage on the display? I’ve already run the battery down several times testing out code.

Here's a (more-or-less complete) Home-Assistant 'WAKE_WORD_VOICE_ASSISTANT' script for this device.

It is based on the esphome HA example, and includes micro-wake-word and timer support.

###################################################################
# Home-Assistant Voice Assistant                                  #
#  for Waveshare ESP32-S3-Touch-AMOLED-1.8 or clone               #
#                                             Version: 2026.05.16 #
###################################################################
substitutions:
  name: esp32-s3-touch
  friendly_name: esp32-s3-touch

  # 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"
  font_family: Figtree
  font_size: 30

  # Voice Assistant settings.
  # These are direct links to te v1 models.  The v2 models dont fit in the tensor space.
  # The alex model was much easier to trigger than the okay_nabu.    
  micro_wake_word_model: github://esphome/micro-wake-word-models/models/alexa.json@main
  #micro_wke_word_model: github://esphome/micro-wake-word-models/models/okay_nabu.json@main
    
esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  min_version: 2025.2.0
  name_add_mac_suffix: true
  on_boot:
    priority: 600
    then:
      - delay: 0.4s
      - if:
          condition:
            lambda: 'return id(auto_off).state > 0;'
          then:
            - light.turn_on: led  
    #  - if:
    #      condition:
    #        lambda: 'return id(auto_sleep).state > 0;'
    #      then:
    #        - lambda: |-
    #           id(sleep_0).set_run_duration(id(auto_sleep).state * 60000);
    #        - deep_sleep.allow: sleep_0
      #- 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
    
esp32:
  variant: esp32s3
  flash_size: 16MB
  framework:
    type: esp-idf
    sdkconfig_options:
      CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y"
      CONFIG_COMPILER_OPTIMIZATION: "Performance"
      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 


psram:
  mode: octal
  speed: 80MHz

logger:
  hardware_uart: USB_SERIAL_JTAG
  level: DEBUG

api:
  on_client_connected:
    - lvgl.image.update:
        id: img_id
        src: casita_initializing 
  on_client_disconnected:
    - lvgl.image.update:
        id: img_id
        src: error_no_ha 

ota:
  - platform: esphome
    password: !secret basic_pwd
    id: ota_esphome
#    on_begin:
#      then:
#        - deep_sleep.prevent: sleep_0
#        - 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
  ap:
    ssid: $friendly_name Hotspot
    password: !secret basic_pwd
  on_connect:
    - lvgl.image.update:
        id: img_id
        src: error_no_ha 
  on_disconnect:
    - lvgl.image.update:
        id: img_id
        src: error_no_wifi

captive_portal:

i2c:
  id: i2c_bus
  sda: GPIO15
  scl: GPIO14
  frequency: 200kHz
  scan: true

spi:
  - id: quad_spi
    type: quad
    clk_pin: GPIO11
    data_pins:
      - GPIO4   # D0
      - GPIO5   # D1
      - GPIO6   # D2
      - GPIO7   # D3

pca9554:
  - id: ext_io
    i2c_id: i2c_bus

time:
  - platform: pcf85063
    id: pcf85063_time
    i2c_id: i2c_bus
    # 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:

# AMOLED Display (368x448)
display:
  - platform: mipi_spi
    id: my_display
    brightness: 150
    bus_mode: quad
    dimensions:
      width: 368
      height: 448
    model: WAVESHARE-ESP32-S3-TOUCH-AMOLED-1.75
    cs_pin: GPIO12

# FT5x06 Capacitive Touchscreen
#  no reset_pin support so sometimes needs reboot!
touchscreen:
  - platform: ft5x06
    id: my_touchscreen
    display: my_display
    interrupt_pin: GPIO21
    address: 0x38
    transform:
      swap_xy: false
      mirror_x: false
      mirror_y: false

# ============================================================================
# AUDIO CONFIGURATION
# ============================================================================

# I2S Audio Bus - Connects ESP32 to ES8311 codec
i2s_audio:
  - id: i2s_audio_bus
    i2s_lrclk_pin: GPIO45  # Word select / left-right clock
    i2s_bclk_pin: GPIO9     # Bit clock
    i2s_mclk_pin: GPIO16    # Master clock

# ES8311 Audio Codec
audio_dac:
  - platform: es8311
    id: es8311_codec
    address: 0x18
    i2c_id: i2c_bus
    use_mclk: true
    use_microphone: false  # Analog microphone (not PDM digital)
    bits_per_sample: 16bit
    sample_rate: 16000
    mic_gain: 18DB

# Speaker - Audio output through ES8311 DAC
speaker:
  - platform: i2s_audio
    id: i2s_speaker
    dac_type: external
    audio_dac: es8311_codec
    i2s_audio_id: i2s_audio_bus
    i2s_dout_pin: GPIO8
    sample_rate: 16000
    bits_per_sample: 16bit
    timeout: 2s

# Microphone - Audio input through ES8311 ADC
microphone:
  - platform: i2s_audio
    id: i2s_microphone
    adc_type: external
    i2s_audio_id: i2s_audio_bus
    i2s_din_pin: GPIO10
    bits_per_sample: 16bit
    sample_rate: 16000

button:
  - platform: factory_reset
    id: factory_reset_btn
    internal: true    

switch:
  # Power Amplifier - mute_pin not supported by dac driver
  #  currently uses the media player on_mute/unmute
  - platform: gpio
    pin: GPIO46
    id: pa_enable
    name: "Amplifier"
    restore_mode: ALWAYS_ON
    internal: true

  # Touchscreen - reset_pin not supported by touch driver
  #  touch screen sometimes does not initialize.  This does not help!
  - platform: gpio
    pin: 
      pca9554: ext_io
      number: 2
    id: tp_reset
    name: "Touch Reset"
    restore_mode: ALWAYS_ON
    internal: false

  - platform: template
    name: Mute
    id: mute
    icon: "mdi:microphone-off"
    optimistic: true
    restore_mode: RESTORE_DEFAULT_OFF
    entity_category: config
    on_turn_off:
      - script.execute: start_voice_assistant
    on_turn_on:
      - script.execute: stop_voice_assistant

  - platform: template
    id: timer_ringing
    name: Alarm
    icon: "mdi:alarm-note"
    optimistic: true
    internal: false
    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
      - delay: 500ms
      - script.execute: start_voice_assistant
    on_turn_on:
      - script.execute: stop_voice_assistant
      - delay: 200ms
      # 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
      - lvgl.image.update:
          id: img_id
          src: casita_timer_finished      
      - text_sensor.template.publish:
          id: text_response
          state: "Timer complete!"
      - delay: 15min
      - switch.turn_off: timer_ringing

text_sensor:
  - id: text_request
    platform: template
    on_value:
      - lvgl.label.update: 
          id: request_id
          text:  !lambda "return x.c_str();"

  - id: text_response
    platform: template
    on_value:
      - lvgl.label.update: 
          id: response_id
          text: !lambda "return x.c_str();"

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:
      #- wait_until:
      #    lambda: return id(voice_assistant_phase) == ${voice_assist_muted_phase_id} || id(voice_assistant_phase) == ${voice_assist_idle_phase_id};
      - script.execute: stop_voice_assistant
      - delay: 200ms
      - script.execute: start_voice_assistant

number:
  - platform: template
    id: volume_control
    name: "Volume"
    min_value: 0
    max_value: 100
    initial_value: 70
    step: 5
    optimistic: true
    disabled_by_default: true
    set_action:
      - lambda: |-
          id(es8311_codec).set_volume(x / 100.0);
          ESP_LOGI("volume", "Volume set to: %.0f%%", x);

  - platform: template
    name: "Screen Off"
    id: auto_off
    optimistic: true
    restore_value: true
    min_value: -1
    max_value: 300
    step: 1
    unit_of_measurement: s
    icon: 'mdi:television-off'
    disabled_by_default: true
    initial_value: 0

binary_sensor:
  - platform: gpio
    pin:
      number: GPIO0
      mode: INPUT_PULLUP
      inverted: true
    name: "Boot Button"
    on_multi_click:
      # Click
      - timing:
          - ON for at most 500ms
          - OFF for at least 200ms
        then:
          - script.execute: press_to_talk
      # Double-click
      - timing:
          - ON for at most 500ms
          - OFF for at most 400ms
          - ON for at most 500ms
        then:
          - switch.toggle: mute
      # Press-and-hold
    #  - timing:
    #      - ON for at least 2s
    #      - OFF for at least 400ms
    #    then:
    #      - deep_sleep.enter: sleep_0
      # Press-and-hold-long
      - timing:
          - ON for at least 10s
        then:         
          - button.press: factory_reset_btn

output:
  - platform: template
    id: backlight_output
    type: float
    write_action:
      - lambda: |-
          id(my_display).set_brightness(state*255);

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
    on_turn_on: 
      then:
      - if:
          condition:
            lambda: 'return id(auto_off).state > 0;'
          then:
            - delay: !lambda 'return id(auto_off).state*1000;'
            - light.turn_off: 
                id: led
                transition_length: 10s

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_mute:
      - switch.turn_off: pa_enable
    on_unmute:
      - switch.turn_on: pa_enable
    #on_announcement:
    #  - if:
    #      condition:
    #        not:
    #          voice_assistant.is_running:
    #      then:
    #      - script.execute: set_idle_status
    on_idle:
      - script.execute: start_voice_assistant
      #- deep_sleep.allow: sleep_0
    #on_play:
    #  - deep_sleep.prevent: sleep_0


# ============================================================================
# VOICE ASSISTANT
# ============================================================================

micro_wake_word:
  models:
    - ${micro_wake_word_model}
  on_wake_word_detected:
    - logger.log: "MWW detected"
    - voice_assistant.start:
        wake_word: !lambda return wake_word;

voice_assistant:
  id: va
  microphone: i2s_microphone
  media_player: speaker_media_player
  use_wake_word: true
  noise_suppression_level: 2 #4
  auto_gain: 31dBFS
  volume_multiplier: 8.0

  on_listening:
    #- deep_sleep.prevent: sleep_0
    - logger.log: "ASSIST listening"
    - lvgl.image.update:
        id: img_id
        src: casita_listening        
    - text_sensor.template.publish:
        id: text_request
        state: "..."
    - light.turn_on: led 
  on_stt_vad_end:
    - logger.log: "ASSIST stt_vad_end"
    - lvgl.image.update:
        id: img_id
        src: casita_thinking        
  on_stt_end:
    - logger.log: "ASSIST stt_end"
    - text_sensor.template.publish:
        id: text_request
        state: !lambda return x;
  on_tts_start:
    - logger.log: "ASSIST tts_start"
    - text_sensor.template.publish:
        id: text_response
        state: !lambda return x;
    - lvgl.image.update:
        id: img_id
        src: casita_replying        
  on_end:
    - logger.log: "ASSIST end"
    - wait_until:
        and:
          - not:
              media_player.is_announcing:
          - not:
              voice_assistant.is_running:
    - script.execute: set_idle_status
    - 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:
          - lvgl.image.update:
              id: img_id
              src: casita_error
          - delay: 1s
          - script.execute: set_idle_status
  on_client_connected:
    - lambda: id(init_in_progress) = false;
    - script.execute: start_voice_assistant
  on_client_disconnected:
    - script.execute: stop_voice_assistant
    - lvgl.image.update:
        id: img_id
        src: error_no_ha
  on_timer_started:
    - wait_until:
        media_player.is_announcing:
    - voice_assistant.stop:
    - script.execute: set_timer_status
  on_timer_cancelled:
    - script.execute: set_timer_status
  on_timer_updated:
    - script.execute: set_timer_status
  on_timer_tick:
    - if:
        condition:
          not:
            - voice_assistant.is_running
        then:
          - script.execute: set_timer_status
  on_timer_finished:
    - switch.turn_on: timer_ringing
    - light.turn_on: led

globals:
  - id: ota_active
    type: bool
    initial_value: 'false'
  - id: mic_muted
    type: bool
    restore_value: no
    initial_value: 'false'
  - id: init_in_progress
    type: bool
    restore_value: false
    initial_value: "true"
  - 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

script:
  - id: update_timers
    then:
      - lambda: |
          const auto timers = id(va).get_timers();
          auto output_timer = timers.begin()->second;
          bool output = false;
          auto active_timer = timers.begin()->second;
          bool active = false;
          for (auto &timer : timers) {
            if (timer.second.seconds_left <= output_timer.seconds_left) {
              output_timer = timer.second;
              output = true;
            }
            if (timer.second.is_active && timer.second.seconds_left <= active_timer.seconds_left) {
              active_timer = timer.second;
              active = true;
            }
          }
          id(global_first_active_timer) = active_timer;
          id(global_is_timer_active) = active;
          id(global_first_timer) = output_timer;
          id(global_is_timer) = output;


  - id: set_timer_status
    then:
      - script.execute: update_timers
      - logger.log: "TIMER update"
      - lambda: |
          auto seconds = 0;
          auto total_seconds = 100;
          if (id(global_is_timer_active)) {
            seconds = id(global_first_active_timer).seconds_left;
            total_seconds =  id(global_first_active_timer).total_seconds;
          } else if (id(global_is_timer)) {
            seconds = id(global_first_timer).seconds_left;
            total_seconds = id(global_first_timer).total_seconds;
          }

          int percent = 0;
          if (total_seconds > 0) {
            percent = round( 100 * seconds / total_seconds );
          }
          auto *bar = id(progress_id);
          lv_bar_set_value(bar, percent, LV_ANIM_ON);

          std::string display_string = "00:00";
          if (seconds > 0) {
            int hours_left = floor(seconds / 3600);
            int minutes_left = floor((seconds - hours_left * 3600) / 60);
            int seconds_left = seconds - 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) ;

            if (hours_left > 0) {
              display_string = display_hours + ":" + display_minute;
            } else {
              display_string = display_minute + ":" + display_seconds;
            }
          }          
          auto *label = id(timer_id);
          lv_label_set_text(label, display_string.c_str());

  - id: start_voice_assistant
    then:
      - if:
          condition:
            lambda: return !id(init_in_progress);
          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
          - script.execute: set_idle_status

  - id: stop_voice_assistant
    then:
     - if:
        condition:
          lambda: return !id(init_in_progress);
        then:
          - lambda: id(va).set_use_wake_word(false);
          - micro_wake_word.stop:
          - voice_assistant.stop:
          - script.execute: set_idle_status
          
  - id: set_idle_status
    then:
      - if:
          condition:
            switch.is_off: mute
          then:
            - lvgl.image.update:
                id: img_id
                src: casita_idle
          else:
            - lvgl.image.update:
                id: img_id
                src: casita_initializing
  
  - id: press_to_talk
    then:
      - if:
          condition: 
            - switch.is_on: timer_ringing
          then:
            - switch.turn_off: timer_ringing
          else:
            - script.execute: stop_voice_assistant
            #- rtttl.play: 'beep:d=16,o=5,b=100:c6'
            - delay: 200ms
            - voice_assistant.start:
      - light.turn_on: led


# ============================================================================
# USER INTERFACE (LVGL)
# ============================================================================
image:
  - file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/error_320_240.png
    id: casita_error
    resize: 320x240
    type: RGB565
    transparency: alpha_channel
  - file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/idle_320_240.png
    id: casita_idle
    resize: 320x240
    type: RGB565
    transparency: alpha_channel
  - file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/listening_320_240.png
    id: casita_listening
    resize: 320x240
    type: RGB565
    transparency: alpha_channel
  - file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/thinking_320_240.png
    id: casita_thinking
    resize: 320x240
    type: RGB565
    transparency: alpha_channel
  - file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/replying_320_240.png
    id: casita_replying
    resize: 320x240
    type: RGB565
    transparency: alpha_channel
  - file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/timer_finished_320_240.png
    id: casita_timer_finished
    resize: 320x240
    type: RGB565
    transparency: alpha_channel
  - file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/loading_320_240.png
    id: casita_initializing
    resize: 320x240
    type: RGB565
    transparency: alpha_channel
  - file: https://github.com/esphome/wake-word-voice-assistants/raw/main/error_box_illustrations/error-no-wifi.png
    id: error_no_wifi
    resize: 320x240
    type: RGB565
    transparency: alpha_channel
  - file: https://github.com/esphome/wake-word-voice-assistants/raw/main/error_box_illustrations/error-no-ha.png
    id: error_no_ha
    resize: 320x240
    type: RGB565
    transparency: alpha_channel

font:
  - file:
      type: gfonts
      family: ${font_family}
      weight: 300
      italic: true
    id: font_request
    size: ${font_size}
    glyphsets:
      - ${font_glyphsets}
  - file:
      type: gfonts
      family: ${font_family}
      weight: 300
    id: font_response
    size: ${font_size}
    glyphsets:
      - ${font_glyphsets}
  - file:
      type: gfonts
      family: ${font_family}
      weight: 300
    id: font_timer
    size: ${font_size}
    glyphsets:
      - ${font_glyphsets}

lvgl:
  pages:
    - id: main_page
      bg_color: black
      widgets:
        # Timer
        - container:
            id: alarm_id 
            align: TOP_MID
            y: 2
            widgets:
              - bar:
                  id: progress_id
                  min_value: 0
                  max_value: 100
                  value: 0
                  align: TOP_MID
                  height: !lambda 'return ${font_size} + 8;'
              - label:
                  id: timer_id
                  text: '00:00'
                  text_font: font_timer
                  text_color: white
                  align: TOP_MID

        # Voice Assistant 
        - button:
            id: btn_id
            bg_color: black
            align: CENTER
            widgets:
              - image:
                  id: img_id
                  align: center
                  src: error_no_wifi
                  radius: 11
                  clip_corner: true
              - label:
                  id: request_id
                  text: '...'
                  align_to: 
                    id: img_id
                    align: OUT_TOP_LEFT
                    y: ${font_size}
                  width: 320
                  height: !lambda 'return ${font_size} + 10;'
                  text_font: font_request
                  text_color: white
                  #text_align: LEFT
                  long_mode: SCROLL_CIRCULAR
              - label:
                  id: response_id
                  align_to:
                    id: btn_id
                    align: OUT_BOTTOM_RIGHT
                    y: ${font_size}
                  width: 320
                  height: !lambda 'return ${font_size} + 10;'
                  text: '...'
                  text_font: font_response
                  text_color: white
                  text_align: RIGHT
                  long_mode: SCROLL_CIRCULAR

            on_click:
              then:
                - script.execute: press_to_talk
        
        # Volume slider - Vertical slider on left side
        #- slider:
        #    id: volume_slider
        #    align: TOP_MID
        #    y: 2
        #    min_value: 0
        #    max_value: 100
        #    value: 70
        #    on_value:
        #      then:
        #        - logger.log:
        #            format: "Volume slider: %d"
        #            args: [ x ]
        #        - number.set:
        #            id: volume_control
        #            value: !lambda 'return x;'

Definitely, still room for improvement, but a good step forwards! Struggling with LVGL and there may still be stability issues when switching between modes,