Waveshare ESP32-S3 LCD Touch 4"

You’re great Dennis! With the first one I resolved my problem with the driver (actually, that one contains its own driver, I guess that helps, and it’s only posted yesterday?). I can now see the custom Dashboard I have in homeassistant, so the buttons and the style is exactly the same whichever screen I use.

Here is the code:

esphome:
  name: mini-screen
  friendly_name: Mini Screen

  # Boot Sequence to set backlight and make a noise to test those functions
  on_boot:
    then:
      # Turn down the backlight
      - delay: 1000ms
      - light.turn_on:
          id: display_backlight
          brightness: 50%
      - delay: 500ms
      - switch.turn_on: board_buzzer
      - delay: 1000ms
      - switch.turn_off: board_buzzer

external_components:
  - source: github://pr#10071
    components: [waveshare_io_ch32v003]
    refresh: 1h
  - source: github://pr#14193
    components: [gt911]
    refresh: 1h
  - source: github://strange-v/RemoteWebViewClient@main
    refresh: 0s
    components: [remote_webview]

esp32:
  board: esp32-s3-devkitc-1
  variant: esp32s3
  framework:
    type: esp-idf
    components:
      - name: "espressif/esp_websocket_client"
        ref: 1.5.0
      - name: "espressif/esp-dsp"
        ref: 1.7.0
      - name: "bitbank2/jpegdec"
        source: https://github.com/strange-v/jpegdec-esphome

  # Flash/RAM settings for high performance
  flash_size: 16MB

psram:
  mode: octal
  speed: 80MHz

logger:
  level: DEBUG

# ================================================================
# WiFi
# ================================================================
wifi:
  ssid: !secret wifi_ssid                   # Defined in secrets.yaml
  password: !secret wifi_password           # Defined in secrets.yaml

# ================================================================
# I2C Buses
# ================================================================
i2c:
  # IO Expander, I2C lines on external connector, RTC, Touchscreen
  - id: bus_a
    setup_priority: 1000
    sda: GPIO15
    scl: GPIO7
    scan: true
    frequency: 400kHz


# ================================================================
# SPI for display
# ================================================================
spi:
  - id: spi0
    clk_pin: GPIO2
    mosi_pin: GPIO1

# ================================================================
# IO Expander (New Waveshare mcu based ch32v003)
# Using external PR component until integrated.
# ================================================================
waveshare_io_ch32v003:
  - id: wave_io_hub
    setup_priority: 850
    address: 0x24
    i2c_id: bus_a


# ================================================================
# Outputs on the IO Expander
# ================================================================
output:
  # PWM output for backlight control.
  # Inverted and zero_means_zero to match Waveshare's circuitry and prevent
  # flickering at low levels.
  - platform: waveshare_io_ch32v003
    waveshare_io_ch32v003_id: wave_io_hub
    id: wave_io_pwm_output
    inverted: true
    zero_means_zero: true
    safe_pwm_levels:  # 0...255
      min_value: 0    # Default: 0
      max_value: 247  # Default: 247. Safe max for ESP32S3-Touch-LCD-7B circuitry

switch: #Need to define and use this or buzzer just stays on at boot.
  - platform: gpio
    name: "Onboard Buzzer"
    id: board_buzzer
    setup_priority: 700
    pin:
      waveshare_io_ch32v003: wave_io_hub
      number: 6
      mode:
        output: true
      inverted: False

# ================================================================
# Light (Backlight Control)
# ================================================================
light:
  - platform: monochromatic
    name: "Display Backlight"
    id: display_backlight
    output: wave_io_pwm_output
    restore_mode: ALWAYS_ON
    default_transition_length: 1s

# ================================================================
# Touchscreen (GT911)
# Don't specify the I2C address here, the driver looks for it if the interrupt pin isn't set.
# The interrupt pin on this board is either on the IO expander or not connected at all, 
# so we'll just let the driver scan for the address.
# ================================================================
touchscreen:
  - platform: gt911
    setup_priority: 600
    id: touch_panel
    i2c_id: bus_a
    display: lcd_display
    update_interval: 20ms

    intcfg_pin:
      waveshare_io_ch32v003: wave_io_hub
      number: 2

    reset_pin:
      waveshare_io_ch32v003: wave_io_hub
      number: 1

    # on_touch:
    #   - logger.log:
    #       format: "Touch at %d/%d"
    #       args: [touch.x, touch.y]

# ================================================================
# Display Driver (ST7701S via mipi_rgb)
# ================================================================
display:
  - platform: mipi_rgb
    setup_priority: 750
    model: WAVESHARE-4-480X480
    id: lcd_display
    reset_pin:
      waveshare_io_ch32v003: wave_io_hub
      number: 3
    pixel_mode: 18bit

# ================================================================
# RemoteWebView — renders HA dashboard remotely and streams to display
# ================================================================
remote_webview:
  id: rwv
  display_id: lcd_display
  touchscreen_id: touch_panel
  device_id: mini-screen
  server: "192.168.42.10:8181"   # Replace with your server hostname or IP
  url: "http://hass:8123/cc-screen"   # Replace with your HA dashboard URL
  tile_size: 64
  full_frame_tile_count: 2
  full_frame_area_threshold: 0.30
  full_frame_every: 30
  every_nth_frame: 2
  min_frame_interval: 600
  jpeg_quality: 80
  max_bytes_per_msg: 61440

# ================================================================
# Expose the dashboard URL as a Home Assistant text entity
# ================================================================
text:
  - platform: template
    id: rwv_url
    name: "Dashboard URL"
    optimistic: true
    mode: TEXT
    set_action:
      - lambda: |-
          if (!id(rwv).open_url(std::string(x.c_str()))) {
            id(rwv).set_url(std::string(x.c_str()));
            ESP_LOGI("remote_webview", "URL queued (not connected): %s", x.c_str());
          }

I’ve actually done the exact same thing! Printed an enclosure to fit the waveshare on the wall in a NEMA 2-gang wallbox, works like a charm.

Please note that my PR broke and I’ve resubmitted it. The PR has been merged so this YAML works in the base ESPHome driver in dev now. Will roll into release on next cycle.
I’m publishing yaml updates to GitHub - PedanticAvenger/Waveshare_ESP32-S3-TOUCH-LCD-4: ESPHome config and board docs for the Waveshare_ESP32-S3-TOUCH-LCD-4 (rev. 4.0 currently) as I get more things fully tested. But I’ve got touch fully working (on rev4) and the only thing I haven’t looked at yet is the battery related stuff.
Also note I’ve added some esp-idf parameters which should keep the screen from going crazy when doing OTA.

Also, the setup priorities might not be needed. They ended up in there while I was trying to figure out the root problem with the GT911 touchscreen controller. They aren’t wrong but might not be needed.

Thanks Raymond, works pretty well. Just removed the canbus stuf because it gave me grief. Also don’t seem to need the screen initialisation stuff really.

The default initialization for WAVESHARE-4-480x480 on the ESP32-S3-Touch-LCD-4 (rev 4 at least) was unstable as heck for me as it looks to have been built for the ESP32-S3-Touch-LCD-4B and is slightly different than this. There could be some manufacturing run differences so if it works without then great. I have more boards I’ll be working with soon so will see if they need it like my first batch.

If these boards all behave consistently with my initial boards then I’ll submit the init sequence as a new mipi_rgp model or abandon this. I might have needed the init sequence due to board initialization oddities introduced trying to figure everything else out.

Do you know if there is a way to turn off the powerled (without covering it or destroying it)?

Hello and thanks for your work @PedanticAvenger

even with the last version on your github repo, i had that error

  interrupt_pin: 
    
    [waveshare_io_ch32v003] is an invalid option for [interrupt_pin]. Please check the indentation.
    waveshare_io_ch32v003: wave_io_hub
    number: 2
  reset_pin: 
    waveshare_io_ch32v003: wave_io_hub
    number: 1

Could you please tell me which exact version of esphome docker are you using, that will help me a lot.

thanks by advance

I’m using dev, but it should work with current release. Looks like the external component for the gt911 isn’t getting loaded correctly. Might want to move to dev version (I don’t run docker, I have a esphome builder container for release and I build locally on my machine for dev branch building and testing.). This will be mainstream in the March esphome release so if you want to stay with your current builder update it then and you should be good.

thanks for your answer, i’ll wait for the march release

I’ve just tested with 2026.3.0-dev20260304 version and it works ! Thank again for your work :slight_smile:

I finally got the Waveshare ESP32-S3-Touch-LCD-4B working reliably with ESPHome, including after a complete power cycle.

I want to document the full sequence because there were actually several related symptoms.
So I asked ChatGPT, this is the result what we did in the last hours ^^

Initial situation

The display initially worked correctly:

  • ST7701S display was working
  • LVGL was working
  • touch was working
  • the ESPHome device connected to Home Assistant

A normal software restart also worked.

However, after completely removing power and powering the board again, the backlight came on but the display remained black.

First power-cycle workaround

Based on the information in this thread, I added an on_boot routine for the PCA9554 I/O expander.

The first version did much more than just prepare the expander pins. It:

  • configured the three display SPI pins
  • implemented 9-bit software SPI manually
  • sent the complete ST7701S initialization sequence again
  • contained several blocking delays

It was essentially this kind of initialization:

esphome:
  on_boot:
    - priority: 200
      then:
        - lambda: |-
            id(lcd_spi_clk)->setup();
            id(lcd_spi_mosi)->setup();
            id(lcd_spi_cs)->setup();
            id(lcd_spi_cs)->digital_write(true);

            auto spi_write = [](bool is_data, uint8_t byte) {
              // Manual 9-bit SPI implementation
            };

            auto cmd = [&](uint8_t value) {
              spi_write(false, value);
            };

            auto dat = [&](uint8_t value) {
              spi_write(true, value);
            };

            cmd(0x11);
            delay(120);

            // Complete ST7701S register initialization
            // ...

            cmd(0x29);

With that code, the display no longer stayed black after a power cycle. However, two new problems appeared at the same time:

  • the display had a strong pink/magenta color cast
  • the ESPHome device no longer connected properly to Home Assistant

The large manual initialization inside on_boot, including the complete register sequence and blocking delays, was unnecessary because model: ST7701S already performs the controller initialization.

Corrected power-cycle fix

I therefore removed the complete manual SPI and ST7701S initialization from on_boot.

It was replaced with only the part actually needed: configuring the three PCA9554 pins as outputs and assigning them defined levels before the display component starts.

esphome:
  name: schlafzimmer-display
  friendly_name: Schlafzimmer Display

  on_boot:
    priority: 600
    then:
      - logger.log: "4B Display-SPI vorbereiten"
      - lambda: |-
          // EXIO0 = CS, inactive HIGH
          id(expander)->pin_mode(0, esphome::gpio::FLAG_OUTPUT);
          id(expander)->digital_write(0, true);

          // EXIO1 = MOSI, initial LOW
          id(expander)->pin_mode(1, esphome::gpio::FLAG_OUTPUT);
          id(expander)->digital_write(1, false);

          // EXIO2 = CLK, initial LOW
          id(expander)->pin_mode(2, esphome::gpio::FLAG_OUTPUT);
          id(expander)->digital_write(2, false);

The three pins are used as follows:

spi:
  - id: display_spi
    interface: software
    clk_pin:
      pca9554: expander
      number: 2
    mosi_pin:
      pca9554: expander
      number: 1

display:
  - platform: mipi_rgb
    model: ST7701S
    spi_id: display_spi
    cs_pin:
      pca9554: expander
      number: 0

This small on_boot fix was enough to make the display start after a complete power cycle.

Because the long manual initialization and delays were gone, the ESPHome API connection to Home Assistant also worked normally again.

The pink/magenta colors, however, were still present and had to be solved separately.

Pink/magenta color problem

The display image itself was stable, but the colors were clearly wrong:

  • white appeared pink
  • the whole UI had a magenta cast
  • yellow looked almost correct

I tested several things without success:

  • color_order: RGB and BGR
  • pclk_inverted: true and false
  • swapping the red and blue pin groups
  • different initialization commands
  • manually sending 0x3A and inversion commands through init_sequence

The final solution was:

pixel_mode: 18bit
color_order: RGB
invert_colors: true

The relevant working display configuration is:

display:
  - platform: mipi_rgb
    id: lcd_display
    model: ST7701S

    spi_id: display_spi
    cs_pin:
      pca9554: expander
      number: 0

    data_rate: 1MHz

    dimensions:
      width: 480
      height: 480

    data_pins:
      red: [GPIO10, GPIO11, GPIO12, GPIO13, GPIO14]
      green: [GPIO21, GPIO8, GPIO18, GPIO45, GPIO38, GPIO39]
      blue: [GPIO40, GPIO41, GPIO42, GPIO2, GPIO1]

    de_pin: GPIO17
    hsync_pin: GPIO46
    vsync_pin: GPIO3
    pclk_pin: GPIO9

    pclk_frequency: 16MHz
    pclk_inverted: false

    hsync_pulse_width: 10
    hsync_back_porch: 10
    hsync_front_porch: 20

    vsync_pulse_width: 10
    vsync_back_porch: 10
    vsync_front_porch: 10

    pixel_mode: 18bit
    color_order: RGB
    invert_colors: true

    update_interval: never
    auto_clear_enabled: false

pixel_mode: 18bit makes ESPHome configure the ST7701S for RGB666 using register value 0x3A = 0x66.

invert_colors: true enables display inversion using command 0x21.

These are also the same two settings used by an older working manual initialization for this panel.

Final result

The display now:

  • starts correctly after a software restart
  • starts correctly after completely removing and restoring power
  • connects normally to Home Assistant
  • displays the correct colors
  • has working LVGL and touch input

So, in my case, both parts were required:

  1. A minimal early on_boot setup for the three PCA9554 display-SPI pins.
  2. pixel_mode: 18bit together with invert_colors: true.

Hi! I set all this up using a custom component. But I’m curious: did you manage to turn off the backlight completely, or was that not something you were aiming for? Like a sleep mode…

Thanks for sharing! FWIW I found:

If I used “- platform: st7701s”, the blank screen on a cold boot issue is there no matter what I did in the on_boot section.
Moving my code to use “- platform: mipi_rgb” addressed the cold boot problem when combined with the on_boot kick in the proverbials.
I did have to drop to “pclk_frequency: 10MHz” to address some random interference/flickering happening on the screen.

Then by using

external_components:
  - source: github://RAR/esphome-axp2101
    components: [ axp2101, aw9523 ]

I have been able to expose a number of ‘bonus’ sensors such as

  # AXP2101 binary sensors
  - platform: axp2101
    axp2101_id: axp2101_component
    battery_charging:
      name: "Battery Charging"
    vbus_connected:
      name: "USB Connected"
    low_battery_level1:
      name: "Low Battery Warning"
      on_press:
        - logger.log: "Low battery warning (level 1)!"
    low_battery_level2:
      name: "Critical Battery Warning"
      on_press:
        - logger.log: "Critical battery warning (level 2)!"
    pkey_short_press:
      name: "Power Button Short Press"
      on_press:
        - logger.log: "Power button short press!"
    pkey_long_press:
      name: "Power Button Long Press"
      on_press:
        - logger.log: "Power button long press!"
    pkey_positive:
      name: "Power Button Pressed"
    pkey_negative:
      name: "Power Button Released"

combined with

binary_sensor:
  - platform: gpio
    name: "Onboard Boot Button"
    pin:
      number: GPIO0
      inverted: true
      mode: INPUT_PULLUP
      ignore_strapping_warning: true
    on_press:
      - logger.log: "Boot button was pressed!"

So all button combinations I can think of are now covered. I was also able to control the charging light by adding:

  - platform: axp2101
    axp2101_id: axp2101_component
#    id: axp_backlight_level
#    backlight:
#      name: "Backlight Brightness"
    charging_led_mode:
      name: "Charging LED Mode"

Sadly, controlling the backlight via either pca9554 or the axp2102 platform does not work. The axp2102 does provide a slider control but it does nothing, so have commented that out. Doesn’t surprise me given it was not created with the Waveshare panel in mind and in any case I think it is controlled via pca9554. Have left it in above if anyone wants to try - possibly the current code (GitHub - RAR/esphome-axp2101: ESPHome component for AXP2101 · GitHub) can be forked and updated to support this extra board. I did manage to turn the backlight off completely, but sadly on moving to the mipi_rgb platform I lost that again, so best I can do is use GPIO04 to dim it a bit. Anything below about 40% starts to flicker quite badly, though. Before I move this thing ‘into production’ and stick it on a wall, I would like to be able to turn the screen off - will keep investigating!

OK. I’ve worked out a way to do it, but I’ll admit it’s a hack and I’m not super proud. Works though. Basically, I’ve setup a page that is totally black. Then to ‘turn off the screen’ what I do is use an automation to turn the screen off via pwm (which dims the screen a lot, just not completely turns it off) and then displays the black screen. Turning it on just swaps to the standard screen and turns the backlight back to its original state (so if it was at 40% before, it goes back to 40%). For all intents and purposes the screen looks off. This of course is of no help if someone wants to run this thing off a battery, but mine is going to be constantly powered so it works for me. Hopefully in time we can work out how to actually drop the power to the screen properly.

I can then toggle the screen on/off with the automations:

alias: 4in01 - turn on screen
description: ''
triggers:
  - trigger: occupancy.detected
    target:
      entity_id: binary_sensor.presence_sensor_fp2_bac6_presence_sensor_3
    options:
      for: '00:00:00'
conditions: []
actions:
  - action: switch.turn_off
    metadata: {}
    target:
      entity_id: switch.living_4in01_screen_night_mode
    data: {}
  - action: light.turn_on
    metadata: {}
    target:
      entity_id: light.living_4in01_display_backlight_brightness
    data: {}
mode: single

and

alias: 4in01 - turn off screen
description: ''
triggers:
  - trigger: occupancy.cleared
    target:
      entity_id: binary_sensor.presence_sensor_fp2_bac6_presence_sensor_3
    options:
      for:
        hours: 0
        minutes: 1
        seconds: 0
conditions: []
actions:
  - action: switch.turn_on
    metadata: {}
    target:
      entity_id: switch.living_4in01_screen_night_mode
    data: {}
  - action: light.turn_off
    metadata: {}
    target:
      entity_id: light.living_4in01_display_backlight_brightness
    data: {}
mode: single

Oh - FWIW The presence sensor turns the screen on/off automatically as people approach and leave.

It’s been a couple of days now, and so far I am very happy with this thing. It is super responsive even when compared to an ipad or similar. It’s about the size of a large light switch, so is not too big, but has enough screen real estate for four good sized ‘buttons’ or info with the advantage of being able to have extra pages of buttons or info like weather etc. Image quality is excellent as well. It cost me $80 AUD from Amazon, and there are cheaper options out there such as using an old mobile, but I think it’s worth the cost.

As an aside I considered setting this up using MQTT - there are definite advantages to doing that, and I don’t think it’s a bad choice, but for me the disadvantage is response times. Don’t get me wrong - MQTT is fast and I do use it for other things, but direct device to home assistant versus device to mqtt service to home assistant has that extra step and potential slight delay between touching a virtual light switch and the light turning on/off. Given that after receiving the command, HA also then has to send the command to zwave/zigbee/etc, well, removing a step is a good thing.

What I would have liked - well, some better esphome support would have saved some pain initially configuring it, and obviously the issues with turning the screen off was time consuming. Waveshare to their credit do have a lot of doco and samples online, just nothing for esphome directly so needed some code scraping to pull out things like interrupt pins. I’d like to be able to also use it for bluetooth tracking and proxy services, but in testing so far that drops the available memory from around 128k to 48k which is a bit borderline (I start to get concerned around the 90k mark). It works, but I’d like it to be higher just to avoid possible glitches. Have tried moving stuff into psram but it hasn’t really helped. Oh, an official wall mount to buy or 3d print would have been nice, but it’s simple enough to do and there are a couple of examples out there. At the moment my printer extruder has died so I’m waiting for parts to arrive.

Something I haven’t looked at yet is taking advantage of the microphones and speaker - will have to think about that!

Had to put this in a separate message due to length! My code is as follows:

# Board: Waveshare ESP32-S3-Touch-LCD-4B 
# https://www.waveshare.com/wiki/ESP32-S3-Touch-LCD-4B
# https://github.com/waveshareteam/ESP32-S3-Touch-LCD-4B
# https://www.espboards.dev/esp32/waveshare-esp32-s3-touch-lcd-4b/
# The power button is closest to the side of the case, the boot is closest to the USB-C port
# The central USB-C port is OTG, either port can be used for power or flashing
# The tiny holes either side are for microphones, the tiny hole next to the USB-C port is for the led
# ESP32-S3-Touch-LCD-4B is a development board launched by Waveshare, featuring an ESP32-S3 2.4GHz Wi-Fi and BLE 5 module, integrated with 16MB Flash and 8MB PSRAM. It has a built-in 4inch LCD screen with a 480 × 480 resolution RGB
# ST7701 driver chip and GT911 capacitive touch chip, communicating through RGB and I2C interfaces respectively
# QMI8658 6-axis IMU (3-axis accelerometer and 3-axis gyroscope) for detecting motion gesture, step counting, etc.
# PCF85063 RTC chip connected to the battery via the AXP2101 for uninterrupted power supply
# PWR and BOOT two side buttons with customizable functions, allowing for custom function development
# AXP2101 include efficient power management, support for multiple output voltages, charging and battery management functions, and optimization for battery life

# Press and hold the power button for 6 seconds to turn it off. Single press turns it back on again.

substitutions:
  name: 4in01
  friendly_name: 4in01
  devicename: 4in01
  location: living

esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  min_version: 2025.5.0
  name_add_mac_suffix: false
  project:
    name: ninkasi.4in
    version: '1.0'
  comment: Waveshare 4inch $location         
  on_boot:
    priority: 800
    then:
      - logger.log: "Preparing 4B Display Panel and SPI lines"
      - lambda: |-
          // EXIO0 = CS, inactive HIGH
          id(my_expander)->pin_mode(0, esphome::gpio::FLAG_OUTPUT);
          id(my_expander)->digital_write(0, true);

          // EXIO1 = MOSI, initial LOW
          id(my_expander)->pin_mode(1, esphome::gpio::FLAG_OUTPUT);
          id(my_expander)->digital_write(1, false);

          // EXIO2 = CLK, initial LOW
          id(my_expander)->pin_mode(2, esphome::gpio::FLAG_OUTPUT);
          id(my_expander)->digital_write(2, false);
          
          
esp32:
  variant: esp32s3
  framework:
    type: esp-idf
    sdkconfig_options:
      CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240M: "y"
      CONFIG_SPIRAM_MODE_OCT: "y"
      CONFIG_SPIRAM_SPEED_80M: "y"
      CONFIG_SPIRAM_BOOT_INIT: "y"
  partitions: 4in01.csv
  flash_size: 16MB    
# Note that the 4in01.csv file is just the following:
## Name,   Type, SubType, Offset,  Size, Flags
#nvs,      data, nvs,     0x9000,  0x5000,
#otadata,  data, ota,     0xe000,  0x2000,
#app0,     app,  ota_0,   0x10000,  0x640000,
#app1,     app,  ota_1,   0x650000, 0x640000,
#spiffs,   data, spiffs,  0xc90000, 0x360000,
#coredump, data, coredump,0xFF0000, 0x10000,

  
# Enable logging
logger:
  baud_rate: 0
  level: INFO
    
# Enable Home Assistant API
api:
  encryption:
    key: !secret esphome_encryption_key

ota:
  - platform: esphome
    encryption:

wifi:
  networks:
  - ssid: !secret wifIoT_ssid
    password: !secret wifIoT_password
    priority: 2
  - ssid: !secret wifi_ssid
    password: !secret wifi_password
    priority: 1
  ap:
    ssid: "$devicename Fallback Hotspot"
    password: !secret ota_password

captive_portal:

external_components:
  - source: github://RAR/esphome-axp2101
    components: [ axp2101, aw9523 ]
    
time:
  - platform: homeassistant
    id: sntp_time

  - platform: sntp
    on_time:
      - seconds: 0
        minutes: 0
        hours: 2
        then:
          - delay: 10s
          - button.press: restart_esphome

button:
  - platform: restart
    name: "$devicename Restart"
    id: restart_esphome
    icon: "mdi:restart"
    
debug:
  update_interval: 5s

pca9554:
  - id: my_expander
    address: 0x20
    i2c_id: touchscreen_bus
    
output:
  - platform: ledc
    pin: 
      number: GPIO04
      inverted: true
    id: lcd_backlight_pin
    frequency: 20000Hz
    min_power: 0.0      # Removes the 15% floor that forces the backlight to stay partially lit
    max_power: 1.0
    
light:
  - platform: monochromatic
    output: lcd_backlight_pin
    name: "Display Backlight Brightness"
    id: display_backlight_dim
    restore_mode: RESTORE_DEFAULT_ON
    gamma_correct: 2.8
    default_transition_length: 1s

    
binary_sensor:
  - platform: gpio
    name: "Onboard Boot Button"
    pin:
      number: GPIO0
      inverted: true
      mode: INPUT_PULLUP
      ignore_strapping_warning: true
    on_press:
      - logger.log: "Boot button was pressed!"

  # AXP2101 binary sensors
  - platform: axp2101
    axp2101_id: axp2101_component
    battery_charging:
      name: "Battery Charging"
    vbus_connected:
      name: "USB Connected"
    low_battery_level1:
      name: "Low Battery Warning"
      on_press:
        - logger.log: "Low battery warning (level 1)!"
    low_battery_level2:
      name: "Critical Battery Warning"
      on_press:
        - logger.log: "Critical battery warning (level 2)!"
    pkey_short_press:
      name: "Power Button Short Press"
      on_press:
        - logger.log: "Power button short press!"
    pkey_long_press:
      name: "Power Button Long Press"
      on_press:
        - logger.log: "Power button long press!"
    pkey_positive:
      name: "Power Button Pressed"
    pkey_negative:
      name: "Power Button Released"

i2c_device:
  - id: axp2101_pmic
    address: 0x34
    
switch:        
  # Added Night Mode Switch to show/hide the blank black screen page
  - platform: template
    name: "Screen Night Mode"
    id: screen_night_mode
    assumed_state: true
    turn_on_action:
      - logger.log: "Switching to black night page..."
      - lvgl.page.show: page_night_mode
    turn_off_action:
      - logger.log: "Restoring normal dashboard..."
      - lvgl.page.show: main_page

  - platform: homeassistant
    id: living_lights
    entity_id: light.living_lights
    name: "Living Room Lights"
    on_state:
      then:
        - if:
            condition:
              lambda: 'return x;'
            then:
              - lvgl.widget.hide: light_icon_widget
              - lvgl.widget.show: light_icon_on_widget
            else:
              - lvgl.widget.show: light_icon_widget
              - lvgl.widget.hide: light_icon_on_widget
  - platform: homeassistant
    id: kitchen_bench_light
    entity_id: light.kitchenbench
    name: "Kitchen Bench Light"

  - platform: homeassistant
    id: fireplace_light_entity
    entity_id: light.fireplacelight
    name: "Fireplace Light"
    
  - platform: homeassistant
    id: fireplace_mode
    entity_id: switch.fireplace_mode
    name: "Fireplace Mode"

  - platform: homeassistant
    id: projector_watch_switch
    entity_id: automation.projector_watch
    name: "Projector Watch Automation"

  - platform: homeassistant
    id: projector_finish_switch
    entity_id: automation.projector_finish
    name: "Projector Finish Automation"

spi:
  - id: spihwd
    interface: software
    clk_pin:
      pca9554: my_expander
      number: 2
    mosi_pin:
      pca9554: my_expander
      number: 1

  
i2c:
  id: touchscreen_bus
  sda: GPIO47
  scl: GPIO48
  frequency: 100kHz
  scan: true

psram:
  mode: octal
  speed: 80MHz
  enable_ecc: true

# -------------------- FONTS --------------------
font:
  - file: "gfonts://Montserrat"
    id: montserrat_120
    size: 120
    bpp: 4

  - file: "gfonts://Montserrat"
    id: montserrat_96
    size: 96
    bpp: 4

  - file: "gfonts://Montserrat"
    id: montserrat_45
    size: 45
    bpp: 4

  - file: "gfonts://Montserrat"
    id: montserrat_27
    size: 27
    bpp: 4

# -------------------- IMAGES --------------------
image:
  - platform: file
    file: mdi:home-assistant
    id: icon_home_assistant
    resize: 96x96
    type: rgb565
    transparency: alpha_channel
  - platform: file
    file: swipeL.png
    id: swipe_left
    resize: 96x96
    type: rgb565
    transparency: alpha_channel
  - platform: file
    file: bulb_off.png
    id: icon_light
    resize: 240x240
    type: rgb565
    transparency: alpha_channel
  - platform: file
    file: bulb_on.png
    id: icon_light_on
    resize: 240x240
    type: rgb565
    transparency: alpha_channel
  - platform: file
    file: fireplace.png
    id: icon_fire
    resize: 240x240
    type: rgb565
    transparency: alpha_channel
  - platform: file
    file: mdi:solar-power
    id: icon_solar
    resize: 64x64
    type: rgb565
    transparency: alpha_channel
  - platform: file
    file: mdi:battery-70
    id: icon_battery
    resize: 64x64
    type: rgb565
    transparency: alpha_channel
  - platform: file
    file: mdi:projector
    id: icon_projector
    resize: 96x96
    type: rgb565
    transparency: alpha_channel
  - platform: file
    file: mdi:projector-off
    id: icon_projector_off
    resize: 96x96
    type: rgb565
    transparency: alpha_channel
  - platform: file
    file: LED_Light.png
    id: icon_fireplace_light
    resize: 96x96
    type: rgb565
    transparency: alpha_channel
    
text_sensor:
  - platform: debug
    device:
      name: "Device Info"
    reset_reason:
      name: "Reset Reason"

# -------------------- GLOBALS (TEXT COLOR) --------------------
globals:
  - id: text_color_r_glob
    type: int
    restore_value: true
    initial_value: '255'

  - id: text_color_g_glob
    type: int
    restore_value: true
    initial_value: '255'

  - id: text_color_b_glob
    type: int
    restore_value: true
    initial_value: '255'

  - id: icon_color_r_glob
    type: int
    restore_value: true
    initial_value: '255'

  - id: icon_color_g_glob
    type: int
    restore_value: true
    initial_value: '255'

  - id: icon_color_b_glob
    type: int
    restore_value: true
    initial_value: '255'

  - id: is_page_2_active
    type: bool
    restore_value: false
    initial_value: 'false'

  - id: swipe_start_x
    type: int
    restore_value: false
    initial_value: '0'

  - id: swipe_start_y
    type: int
    restore_value: false
    initial_value: '0'

  - id: swipe_in_progress
    type: bool
    restore_value: false
    initial_value: 'false'

  - id: swipe_last_x
    type: int
    restore_value: false
    initial_value: '0'

  - id: swipe_last_y
    type: int
    restore_value: false
    initial_value: '0'

  - id: swipe_locked
    type: bool
    restore_value: false
    initial_value: 'false'    

# -------------------- DISPLAY (ST7701S RGB) --------------------
display:
  - platform: mipi_rgb # this worked, but the display flickered all the time. Fixed by dropping the pclk_frequency
    id: tft_display
    model: ST7701S

    spi_id: spihwd
    cs_pin:
      pca9554: my_expander
      number: 0

    data_rate: 2MHz # changed from 1MHz

    dimensions:
      width: 480
      height: 480

    data_pins:
      red: [GPIO10, GPIO11, GPIO12, GPIO13, GPIO14]
      green: [GPIO21, GPIO8, GPIO18, GPIO45, GPIO38, GPIO39]
      blue: [GPIO40, GPIO41, GPIO42, GPIO2, GPIO1]

    de_pin: GPIO17
    hsync_pin: 
      number: GPIO46
      ignore_strapping_warning: true
    vsync_pin:
      number: GPIO3
      ignore_strapping_warning: true
    pclk_pin: GPIO9

    pclk_frequency: 10MHz
    pclk_inverted: false

    hsync_pulse_width: 8 
    hsync_back_porch: 50
    hsync_front_porch: 10

    vsync_pulse_width: 8
    vsync_back_porch: 20
    vsync_front_porch: 10

    pixel_mode: 18bit
    color_order: RGB
    invert_colors: true

    update_interval: never
    auto_clear_enabled: false

# -------------------- TOUCHSCREEN --------------------
touchscreen:
  - platform: gt911
    id: tft_touch
    i2c_id: touchscreen_bus
    display: tft_display
    reset_pin:
      pca9554: my_expander
      number: 5
      mode: OUTPUT
    on_touch:
      then:
        - if:
            condition:
              lambda: 'return id(is_page_2_active);'
            then:
              - script.execute: page_2_idle_timer

        - lambda: |-
            if (id(swipe_locked)) {
              ESP_LOGI("swipe", "on_touch: swipe_locked, ignoring swipe start");
              id(swipe_in_progress) = false;
              return;
            }

            auto t = id(tft_touch).get_touch();
            if (!t.has_value()) {
              ESP_LOGI("swipe", "on_touch without coordinates");
              return;
            }
            auto p = t.value();
            id(swipe_start_x) = p.x;
            id(swipe_start_y) = p.y;
            id(swipe_in_progress) = true;
            ESP_LOGI("swipe", "start at (%d,%d)", p.x, p.y);

    on_update:
      then:
        - lambda: |-
            const int SWIPE_THRESHOLD = 80;

            if (id(swipe_locked)) {
              return;
            }

            if (!id(swipe_in_progress)) {
              return;
            }

            auto t = id(tft_touch).get_touch();
            if (!t.has_value()) {
              ESP_LOGI("swipe", "update: no touch -> cancel");
              id(swipe_in_progress) = false;
              return;
            }

            auto p = t.value();
            int cx = p.x;
            int cy = p.y;

            int dx = cx - id(swipe_start_x);
            int dy = cy - id(swipe_start_y);
            int adx = dx >= 0 ? dx : -dx;
            int ady = dy >= 0 ? dy : -dy;

            ESP_LOGI("swipe", "update at (%d,%d), dx=%d dy=%d", cx, cy, dx, dy);

            if (adx > ady && adx > SWIPE_THRESHOLD) {
              if (dx > 0) {
                ESP_LOGI("swipe", "DETECTED RIGHT SWIPE");
                if (id(is_page_2_active)) {
                  id(show_main_page).execute();
                }
              } else {
                ESP_LOGI("swipe", "DETECTED LEFT SWIPE");
                if (!id(is_page_2_active)) {
                  id(show_page_2).execute();
                }
              }
              id(swipe_in_progress) = false;
            }

    on_release:
      then:
        - lambda: |-
            ESP_LOGI("swipe", "release -> cancel");
            id(swipe_in_progress) = false;
            id(swipe_locked) = false;

axp2101:
  id: axp2101_component
  model: M5CORES3

number:
  - platform: axp2101
    axp2101_id: axp2101_component
#    id: axp_backlight_level
#    backlight:
#      name: "Backlight Brightness"
    charging_led_mode:
      name: "Charging LED Mode"
      
sensor:
  - platform: axp2101
    axp2101_id: axp2101_component
    battery_voltage:
      name: "Battery Voltage"
    battery_level:
      name: "Battery Level"
    temperature:
      name: "PMU Temperature"

  - platform: wifi_signal
    name: "WiFi Signal Sensor"
    id: wifisignal
    update_interval: 60s
    unit_of_measurement: dBm
    accuracy_decimals: 0
    device_class: signal_strength
    state_class: measurement
    entity_category: diagnostic
  - platform: copy
    source_id: wifisignal
    id: wifipercent
    name: "WiFi Signal Percent"
    filters:
      - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
    unit_of_measurement: "Signal %"
    entity_category: "diagnostic"
  - platform: uptime
    id: uptime_s
    name: "$devicename Uptime"
    update_interval: 60s
  - platform: template
    name: $devicename free memory
    lambda: return heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
    icon: "mdi:memory"
    entity_category: diagnostic
    state_class: measurement
    unit_of_measurement: "b"
    update_interval: 60s
  - platform: homeassistant
    id: solar_power_sensor
    entity_id: sensor.symo_15_0_3_m_1_ac_power
    internal: true
    on_value:
      then:
        - lvgl.label.update:
            id: solar_power_value
            text: !lambda |-
              if (isnan(x)) {
                return std::string("---");
              } else {
                char buf[16];
                sprintf(buf, "%.1f", x / 1000.0);
                return std::string(buf);
              }
  - platform: homeassistant
    id: battery_soc_sensor
    entity_id: sensor.sigen_plant_battery_state_of_charge
    internal: true
    on_value:
      then:
        - lvgl.label.update:
            id: battery_power_value
            text: !lambda |-
              if (isnan(x)) {
                return std::string("??%");
              } else {
                char buf[16];
                sprintf(buf, "%.0f%%", x);
                return std::string(buf);
              }
        - lvgl.bar.update:
            id: battery_bar
            value: !lambda |-
              if (isnan(x)) {
                return 0;
              } else {
                return (int32_t)x;
              }

# -------------------- SCRIPTS (COLOR & POWER HANDLING) --------------------
script:
  - id: page_2_idle_timer
    mode: restart
    then:
      - delay: 30s
      - lvgl.page.show:
          id: main_page
          animation: MOVE_RIGHT
          time: 200ms
      - lambda: 'id(is_page_2_active) = false;'

  - id: show_page_2
    mode: single
    then:
      - lambda: 'id(is_page_2_active) = true;'
      - script.execute: page_2_idle_timer
      - lvgl.page.show:
          id: page_2
          animation: MOVE_LEFT
          time: 200ms
      - lambda: |-
          lv_obj_t *page = id(page_2)->obj;
          if (page != nullptr) {
            ESP_LOGI("page_2", "Reset scroll to top for page_2 (page obj=%p)", page);
            lv_obj_scroll_to_y(page, 0, LV_ANIM_OFF);
            lv_obj_scroll_to_x(page, 0, LV_ANIM_OFF);
          }

  - id: show_main_page
    mode: single
    then:
      - script.stop: page_2_idle_timer
      - lambda: 'id(is_page_2_active) = false;'
      - lvgl.page.show:
          id: main_page
          animation: MOVE_RIGHT
          time: 200ms

  - id: apply_text_color
    then:
      - lvgl.label.update:
          id:
            - solar_power_value
            - solar_power_unit
            - battery_power_value
          text_color: !lambda |-
            int r = id(text_color_r_glob);
            int g = id(text_color_g_glob);
            int b = id(text_color_b_glob);

            if (r < 0) r = 0; if (r > 255) r = 255;
            if (g < 0) g = 0; if (g > 255) g = 255;
            if (b < 0) b = 0; if (b > 255) b = 255;

            uint32_t rgb = ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
            return lv_color_hex(rgb);

      - lvgl.bar.update:
          id: battery_bar
          border_color: !lambda |-
            int r = id(text_color_r_glob);
            int g = id(text_color_g_glob);
            int b = id(text_color_b_glob);
            if (r < 0) r = 0; if (r > 255) r = 255;
            if (g < 0) g = 0; if (g > 255) g = 255;
            if (b < 0) b = 0; if (b > 255) b = 255;
            uint32_t rgb = ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
            return lv_color_hex(rgb);
          indicator:
            bg_color: !lambda |-
              int r = id(text_color_r_glob);
              int g = id(text_color_g_glob);
              int b = id(text_color_b_glob);
              if (r < 0) r = 0; if (r > 255) r = 255;
              if (g < 0) g = 0; if (g > 255) g = 255;
              if (b < 0) b = 0; if (b > 255) b = 255;
              uint32_t rgb = ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
              return lv_color_hex(rgb);


  - id: apply_icon_color
    then:
      - lvgl.image.update:
          id:
            - fire_icon_widget
            - solar_icon_widget
            - battery_icon_widget
          image_recolor: !lambda |-
            int r = id(icon_color_r_glob);
            int g = id(icon_color_g_glob);
            int b = id(icon_color_b_glob);
            if (r < 0) r = 0; if (r > 255) r = 255;
            if (g < 0) g = 0; if (g > 255) g = 255;
            if (b < 0) b = 0; if (b > 255) b = 255;
            uint32_t rgb = ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
            return lv_color_hex(rgb);

# -------------------- LVGL CONFIGURATION --------------------
lvgl:
  log_level: INFO
  buffer_size: 25%
  bottom_layer: 0x000000
  default_font: montserrat_27

  displays:
    - tft_display

  touchscreens:
    - touchscreen_id: tft_touch

  on_ready:
    then:
      - script.execute: apply_text_color
      - script.execute: apply_icon_color

  pages:
    # ================= MAIN PAGE (DEFAULT) =================
    - id: main_page
      bg_color: 0x000000

      widgets:
        # ---------- TOP LEFT LIGHT ICON (TOGGLE STATE CONTAINER) ----------
        - obj:
            id: light_group
            align: TOP_LEFT
            x: 0
            y: 0
            width: SIZE_CONTENT
            height: SIZE_CONTENT
            bg_color: 0x151515
            bg_opa: COVER
            border_color: 0x333333
            border_width: 1
            radius: 12
            clickable: true
            on_click:
              then:
                - switch.toggle: living_lights
            widgets:
              - image:
                  id: light_icon_widget
                  src: icon_light
                  align: TOP_LEFT
                  x: 0
                  y: 0

              - image:
                  id: light_icon_on_widget
                  src: icon_light_on
                  align: TOP_LEFT
                  x: 0
                  y: 0
                  hidden: true

        # ---------- TOP RIGHT FIRE ICON (TOGGLE STATE CONTAINER) ----------
        - obj:
            id: fire_group
            align: TOP_RIGHT
            x: 0
            y: 0
            width: SIZE_CONTENT
            height: SIZE_CONTENT
            bg_color: 0x151515
            bg_opa: COVER
            border_color: 0x333333
            border_width: 1
            radius: 12
            clickable: true
            on_click:
              then:
                - switch.toggle: fireplace_mode
            widgets:
              - image:
                  id: fire_icon_widget
                  src: icon_fire
                  align: TOP_LEFT
                  x: 0
                  y: 0

        # ---------- SOLAR GENERATION (BOTTOM LEFT) ----------
        - obj:
            id: solar_group
            align: TOP_LEFT
            y: 320
            width: SIZE_CONTENT
            height: SIZE_CONTENT
            bg_color: 0x000000
            border_color: 0x000000
            widgets:
              - image:
                  id: solar_icon_widget
                  src: icon_solar
                  y: -10
                  image_recolor: 0xFFFFFF
                  image_recolor_opa: COVER

              - label:
                  id: solar_power_value
                  text: "0.0"
                  align_to:
                    id: solar_power_unit
                    align: BOTTOM_MID
                    x: 0
                    y: -30
                  text_font: montserrat_45
                  text_color: 0xFFFFFF

              - label:
                  id: solar_power_unit
                  text: "kW"
                  x: 70
                  y: 30
                  text_font: montserrat_27
                  text_color: 0xFFFFFF

        # ---------- HOUSE BATTERY SOC (BOTTOM RIGHT) ----------
        - obj:
            id: battery_group
            align: TOP_RIGHT
            y: 320
            width: SIZE_CONTENT
            height: SIZE_CONTENT
            bg_color: 0x000000
            border_color: 0x000000
            widgets:
              - image:
                  id: battery_icon_widget
                  src: icon_battery
                  y: -10
                  image_recolor: 0xFFFFFF
                  image_recolor_opa: COVER

              - label:
                  id: battery_power_value
                  text: "??%"
                  align_to:
                    id: battery_bar
                    align: BOTTOM_MID
                    x: 0
                    y: -26
                  text_font: montserrat_45
                  text_color: 0xFFFFFF

              - bar:
                  id: battery_bar
                  x: 70
                  y: 40
                  width: 100
                  height: 20
                  min_value: 0
                  max_value: 100
                  value: 0
                  bg_color: 0x000000
                  bg_opa: 0%
                  border_color: 0xFFFFFF
                  border_width: 2
                  radius: 0
                  indicator:
                    bg_color: 0xFFFFFF
                    bg_opa: 100%
                    radius: 0
                    border_width: 0

        # ---------- SWIPE ICON ----------
        - image:
            id: swipe_l_icon
            src: swipe_left
            align: BOTTOM_MID
            y: -20
            on_click:
              then:
                - script.execute: show_page_2

    # ================= PAGE 2 =================
    - id: page_2
      bg_color: 0x000000

      widgets:
        # ---------- 2x2 GRID CONTAINER ----------
        - obj:
            id: control_grid
            align: TOP_MID
            y: 30
            width: 460
            height: 360
            bg_color: 0x000000
            border_color: 0x000000
            layout:
              type: GRID
              grid_rows: [155px, 155px]
              grid_columns: [215px, 215px]
              pad_row: 15
              pad_column: 15
              grid_cell_x_align: CENTER
              grid_cell_y_align: CENTER
            widgets:
              # --- 1. PROJECTOR WATCH (Row 0, Col 0) ---
              - obj:
                  id: tile_proj_watch
                  width: 215
                  height: 155
                  bg_color: 0x151515
                  bg_opa: COVER
                  border_color: 0x333333
                  border_width: 1
                  radius: 12
                  clickable: true
                  layout:
                    type: FLEX
                    flex_flow: COLUMN
                    flex_align_main: CENTER
                    flex_align_cross: CENTER
                    pad_row: 10
                  grid_cell_row_pos: 0
                  grid_cell_column_pos: 0
                  on_click:
                    then:
                      - switch.turn_on: projector_watch_switch
                  widgets:
                    - image:
                        src: icon_projector
                        image_recolor: 0xFFFFFF
                        image_recolor_opa: COVER
                    - label:
                        text: "Projector On"
                        text_font: montserrat_27
                        text_color: 0xFFFFFF

              # --- 2. PROJECTOR FINISH (Row 0, Col 1) ---
              - obj:
                  id: tile_proj_finish
                  width: 215
                  height: 155
                  bg_color: 0x151515
                  bg_opa: COVER
                  border_color: 0x333333
                  border_width: 1
                  radius: 12
                  clickable: true
                  layout:
                    type: FLEX
                    flex_flow: COLUMN
                    flex_align_main: CENTER
                    flex_align_cross: CENTER
                    pad_row: 10
                  grid_cell_row_pos: 0
                  grid_cell_column_pos: 1
                  on_click:
                    then:
                      - switch.turn_on: projector_finish_switch
                  widgets:
                    - image:
                        src: icon_projector_off
                        image_recolor: 0xFFFFFF
                        image_recolor_opa: COVER
                    - label:
                        text: "Projector Off"
                        text_font: montserrat_27
                        text_color: 0xFFFFFF
                      
              # --- 3. KITCHEN BENCH LIGHT (Row 1, Col 0) ---
              - obj:
                  id: tile_kitchen_light
                  width: 215
                  height: 155
                  bg_color: 0x151515
                  bg_opa: COVER
                  border_color: 0x333333
                  border_width: 1
                  radius: 12
                  clickable: true
                  layout:
                    type: FLEX
                    flex_flow: COLUMN
                    flex_align_main: CENTER
                    flex_align_cross: CENTER
                    pad_row: 10
                  grid_cell_row_pos: 1
                  grid_cell_column_pos: 0
                  on_click:
                    then:
                      - switch.toggle: kitchen_bench_light
                  widgets:
                    - image:
                        src: icon_fireplace_light
                    - label:
                        text: "Kitchen"
                        text_font: montserrat_27
                        text_color: 0xFFFFFF

              # --- 4. FIREPLACE LIGHT (Row 1, Col 1) ---
              - obj:
                  id: tile_fireplace_light
                  width: 215
                  height: 155
                  bg_color: 0x151515
                  bg_opa: COVER
                  border_color: 0x333333
                  border_width: 1
                  radius: 12
                  clickable: true
                  layout:
                    type: FLEX
                    flex_flow: COLUMN
                    flex_align_main: CENTER
                    flex_align_cross: CENTER
                    pad_row: 10
                  grid_cell_row_pos: 1
                  grid_cell_column_pos: 1
                  on_click:
                    then:
                      - switch.toggle: fireplace_light_entity
                  widgets:
                    - image:
                        src: icon_fireplace_light
                    - label:
                        text: "Fireplace"
                        text_font: montserrat_27
                        text_color: 0xFFFFFF
                      
        # ---------- BACK BUTTON (BOTTOM) ----------
        - obj:
            id: back_button
            align: BOTTOM_MID
            y: -20
            width: 200
            height: 55
            bg_color: 0x151515
            border_color: 0x333333
            border_width: 1
            radius: 12
            clickable: true
            layout:
              type: FLEX
              flex_flow: ROW
              flex_align_main: CENTER
              flex_align_cross: CENTER
            on_click:
              then:
                - script.execute: show_main_page
            widgets:
              - label:
                  id: back_label
                  text: "Back"
                  text_font: montserrat_27
                  text_color: 0xFFFFFF

    # ================= NIGHT MODE PAGE =================
    - id: page_night_mode
      bg_color: 0x000000
      widgets:
        - obj:
            width: 100%
            height: 100%
            bg_color: 0x000000
            bg_opa: COVER
            border_width: 0       # Removes any border stroke
            outline_width: 0      # Removes any focus/outline rings
            radius: 0             # Removes rounded corner clipping artifacts
            pad_all: 0            # Removes internal padding
            clickable: true
            on_click:
              then:
                - switch.turn_off: screen_night_mode