Unable to Play Sound with I2S on ESP32 (MAX98357) in ESPHome

I am trying to set up a universal speaker system using an ESP32 with I2S audio output, connected to the MAX98357 audio amplifier and an 8 Ohm, 1W speaker. I want to play any audio (like radio or music) via Home Assistant.

However, when I try to play audio, the configuration compiles successfully, but no sound is produced, even though the screen updates. The shared GPIO pin 46, which is used by both the screen and the audio, needs to be in the HIGH state for the audio to work, but I’m not sure what I might be missing. Any advice would be appreciated!

esphome:
  name: lilka
  friendly_name: lilka
  on_boot:
    priority: 600
    then:
      - output.turn_on: display_power
      - delay: 100ms

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: arduino

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: "Lilka Fallback Hotspot"
    password: "hnYImYM4O6I0"

captive_portal:

web_server:
  port: 80

api:
  encryption:
    key: "Yw3yq8lxsaze+jdggjAwxKpV4GD6gNZptvw4d6OPfOc="

logger:

ota:
  - platform: esphome
    password: "bb198feb99159fdaefc8092f10039acd"

output:
  - platform: gpio
    pin: 46
    id: display_power
    inverted: false

substitutions:
  i2s_lrclk: GPIO1  # LRC Pin of the Max98357
  i2s_bclk: GPIO42  # BCLK Pin of the Max98357
  spk: GPIO2       # DIN Pin of the Max98357

i2s_audio:
  - id: i2s_in
    i2s_lrclk_pin: ${i2s_lrclk} # LRC Pin of the Max98357
    i2s_bclk_pin: ${i2s_bclk}   # BCLK Pin of the Max98357

media_player:
  - platform: i2s_audio
    name: "Lilka Speaker"
    id: media_player_speaker
    i2s_audio_id: i2s_in
    dac_type: external
    i2s_dout_pin: ${spk}    # DIN Pin of the MAX98357A Audio Amplifier
    mode: mono
    on_play:
      - logger.log: "Media playing!"
      - media_player.volume_set:
          id: media_player_speaker
          volume: 100%

my all config - lilka + speaker all - Pastebin.com
scheme

When I try to play audio in Radio Browser, this is what I see in the ESPHome logs.

[17:41:52.281][D][media_player:090]:   Media URL: https://zvukipro.com/uploads/files/2021-07/1627104739_21071.mp3
[17:41:52.920][D][main:447]: Media playing!
[17:41:52.920][D][media_player:083]: 'Lilka Speaker' - Setting
[17:41:52.920][D][media_player:093]:   Volume: 1.00
[17:41:52.920][D][main:447]: Media playing!
[17:41:52.920][D][media_player:083]: 'Lilka Speaker' - Setting
[17:41:52.920][D][media_player:093]:   Volume: 1.00
[17:41:52.920][D][main:447]: Media playing!
[17:41:52.920][D][media_player:083]: 'Lilka Speaker' - Setting
[17:41:52.920][D][media_player:093]:   Volume: 1.00
[17:41:52.920][D][main:447]: Media playing!
[17:41:52.920][D][media_player:083]: 'Lilka Speaker' - Setting
[17:41:52.920][D][media_player:093]:   Volume: 1.00
[17:41:52.920][D][main:447]: Media playing!
INFO Processing unexpected disconnect from ESPHome API for lilka @ 192.168.31.229
WARNING Disconnected from API
INFO Successfully resolved lilka @ 192.168.31.229 in 0.002s
INFO Successfully connected to lilka @ 192.168.31.229 in 0.009s
INFO Successful handshake with lilka @ 192.168.31.229 in 0.062s
[17:42:00.719][I][lvgl:000]: lv_obj_update_layout: Layout update begin 	(in lv_obj_pos.c line #314)
[17:42:00.723][I][lvgl:000]: lv_obj_update_layout: Layout update begin 	(in lv_obj_pos.c line #314)
[17:42:05.719][I][lvgl:000]: lv_obj_update_layout: Layout update begin 	(in lv_obj_pos.c line #314)

I’m not 100% sure if this is the last working version (I’m currently rebuilding the station), but I think with these settings I was able to output radio and sound files from the media folder via the ESP.

esp32:
  board: esp32dev
  framework:
    type: esp-idf
    version: recommended

.
.
.
i2s_audio:
  - id: i2s_out
    i2s_lrclk_pin: GPIO22 #LRC an MAX98357A
    i2s_bclk_pin: GPIO23  #BCL an MAX98357A
.
.
.

speaker:
  - platform: i2s_audio
    id: marvin_speaker
    dac_type: external
    i2s_audio_id: i2s_out
    i2s_dout_pin: GPIO18
    channel: mono
    sample_rate: 16000  #Defaults to 16000
    bits_per_sample: 16bit #One of 8bit, 16bit, 24bit, or 32bit. Defaults to 16bit. 
 
media_player:
  - platform: speaker
    name: "Marvin Media Player"
    id: marvin_media_player
    buffer_size: 10000 #Must be between 4000 and 4000000. Defaults to 100000
    codec_support_enabled: true # set to flase to save and specify format to save recources 
    announcement_pipeline:
        speaker: marvin_speaker
  #      format: MP3 # One of FLAC, MP3, WAV, or NONE.
        num_channels: 1

1 Like

It compiles, but I get this error.

[19:19:07.982][D][speaker_media_player:406]: State changed to ANNOUNCING
[19:19:07.988][D][speaker_media_player.pipeline:114]: Reading FLAC file type
[19:19:07.995][D][ring_buffer:034][ann_read]: Created ring buffer with size 10000
[19:19:08.688][I][lvgl:000]: lv_obj_update_layout: Layout update begin 	(in lv_obj_pos.c line #314)
[19:19:09.378][D][sensor:131]: 'Battery Level': Sending state 83.60835 % with 0 decimals of accuracy
[19:19:12.293][D][sensor:131]: 'Battery Voltage': Sending state 4.00463 V with 2 decimals of accuracy
[19:19:13.272][D][speaker_media_player.pipeline:124]: Decoded audio has 1 channels, 16000 Hz sample rate, and 16 bits per sample
[19:19:13.279][D][i2s_audio.speaker:102]: Starting
[19:19:13.284][D][i2s_audio.speaker:111]: Stopping
[19:19:13.298][D][i2s_audio.speaker:116]: Stopped
[19:19:13.298][E][i2s_audio.speaker:130]: Not enough memory
[19:19:13.326][D][i2s_audio.speaker:102]: Starting
[19:19:13.333][D][i2s_audio.speaker:111]: Stopping
[19:19:13.347][D][i2s_audio.speaker:116]: Stopped
[19:19:13.348][E][i2s_audio.speaker:130]: Not enough memory
[19:19:13.626][E][i2s_audio.speaker:158]: Task failed to start, retrying in 1 second
[19:19:13.626][I][lvgl:000]: lv_obj_update_layout: Layout update begin 	(in lv_obj_pos.c line #314)
[19:19:14.391][E][component:342]: i2s_audio.speaker cleared Error flag
[19:19:14.412][D][i2s_audio.speaker:102]: Starting
[19:19:14.417][D][i2s_audio.speaker:111]: Stopping
[19:19:14.435][D][i2s_audio.speaker:116]: Stopped
[19:19:14.439][E][i2s_audio.speaker:130]: Not enough memory
[19:19:14.466][D][esp-idf:000]: E (167297) i2s_common: i2s_alloc_dma_desc(509): allocate DMA buffer failed
[19:19:23.939][D][esp-idf:000]: E (167305) i2s_std: i2s_std_set_slot(111): allocate memory for dma descriptor failed
[19:19:24.719][E][component:342]: i2s_audio.speaker cleared Error flag
[19:19:24.736][E][i2s_audio.speaker:158]: Task failed to start, retrying in 1 second
[19:19:24.809][E][component:314]: i2s_audio.speaker set Error flag: unspecified
[19:19:25.752][E][component:342]: i2s_audio.speaker cleared Error flag
[19:19:25.768][E][i2s_audio.speaker:158]: Task failed to start, retrying in 1 second
[19:19:25.778][E][component:314]: i2s_audio.speaker set Error flag: unspecified
[19:19:26.785][E][component:342]: i2s_audio.speaker cleared Error flag
[19:19:26.804][E][i2s_audio.speaker:158]: Task failed to start, retrying in 1 second
[19:19:26.810][E][component:314]: i2s_audio.speaker set Error flag: unspecified

Have you set the buffer_size:? The default setting is so large that it usually doesn’t work. Radio streaming works with a smaller buffer. For sound files, they mustn’t be too large (i.e., smaller than the buffer size).

What ESP32-S3 board do you have? How much flash memory and PSRAM does it have?

Could you perhaps add more PSRAM?

1 Like

ESP32-S3-WROOM-1-N16R8

16 MB NOR Flash memory
8 MB PSRAM
RAM: 512 KB

4 SPI buses (2 of which are user-accessible)
2 I2S buses
2 I2C buses
3 UART buses

Could it be due to the use of LVGL and several codecs simultaneously?

i2s_audio:
  - id: i2s_out
    i2s_lrclk_pin: GPIO1 #LRC an MAX98357A
    i2s_bclk_pin: GPIO42  #BCL an MAX98357A

speaker:
  - platform: i2s_audio
    id: lilka_speaker
    dac_type: external
    i2s_audio_id: i2s_out
    i2s_dout_pin: GPIO2
    channel: mono
    sample_rate: 16000  #Defaults to 16000
    bits_per_sample: 16bit #One of 8bit, 16bit, 24bit, or 32bit. Defaults to 16bit. 
 
media_player:
  - platform: speaker
    name: "Lilka Media Player"
    id: lilka_media_player
    buffer_size: 10000 #Must be between 4000 and 4000000. Defaults to 100000
    codec_support_enabled: true # set to flase to save and specify format to save recources 
    announcement_pipeline:
        speaker: lilka_speaker
  #      format: MP3 # One of FLAC, MP3, WAV, or NONE.
        num_channels: 1

lilka audio work! - Pastebin.com it work! Thaks

without PSRAM

esphome:
  name: lilka
  friendly_name: lilka
  on_boot:
    priority: 600
    then:
      - output.turn_on: display_power
      - delay: 100ms

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: esp-idf
    version: recommended
    # Оптимізація пам'яті
    sdkconfig_options:
      CONFIG_ESP32S3_DATA_CACHE_64KB: y
      CONFIG_ESP32S3_DATA_CACHE_LINE_64B: y
      CONFIG_SPIRAM_MODE_OCT: y
      CONFIG_SPIRAM: y

# Додати якщо є PSRAM
psram:
  mode: octal
  speed: 80MHz

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  power_save_mode: none
  ap:
    ssid: "Lilka Fallback Hotspot"
    password: "hnYImYM4O6I0"

captive_portal:

web_server:
  port: 80

api:
  encryption:
    key: "Yw3yq8lxsaze+jdggjAwxKpV4GD6gNZptvw4d6OPfOc="

logger:
  level: INFO
  logs:
    lvgl: WARN
    speaker_media_player: INFO
    i2s_audio.speaker: INFO

ota:
  - platform: esphome
    password: "bb198feb99159fdaefc8092f10039acd"

output:
  - platform: gpio
    pin: 46
    id: display_power
    inverted: false

i2s_audio:
  - id: i2s_out
    i2s_lrclk_pin: GPIO1
    i2s_bclk_pin: GPIO42

speaker:
  - platform: i2s_audio
    id: lilka_speaker
    dac_type: external
    i2s_audio_id: i2s_out
    i2s_dout_pin: GPIO2
    channel: mono
    sample_rate: 16000
    bits_per_sample: 16bit
    buffer_duration: 100ms
 
media_player:
  - platform: speaker
    name: "Lilka Media Player"
    id: lilka_media_player
    buffer_size: 4000
    codec_support_enabled: false
    announcement_pipeline:
        speaker: lilka_speaker
        format: WAV
        num_channels: 1

# ---------------------------------------------------
# СЕНСОРИ
# ---------------------------------------------------

sensor:
  - platform: adc
    pin: GPIO3
    id: battery_voltage
    name: "Battery Voltage"
    icon: "mdi:lightning-bolt"
    update_interval: 10s
    attenuation: 12db
    filters:
      - multiply: 1.33
    
  - platform: template
    id: battery_level
    name: "Battery Level"
    icon: "mdi:battery"
    unit_of_measurement: "%"
    accuracy_decimals: 0
    update_interval: 10s
    lambda: |-
      float voltage = id(battery_voltage).state;
      float percent = (voltage - 3.0) / (4.2 - 3.0) * 100.0;
      if (percent > 100) percent = 100;
      if (percent < 0) percent = 0;
      return percent;

# ---------------------------------------------------
# ДИСПЛЕЙ ST7789V + LVGL
# ---------------------------------------------------

spi:
  clk_pin: 18
  mosi_pin: 17

display:
  - platform: mipi_spi
    model: ST7789V
    id: my_display
    dc_pin: 15
    cs_pin: 7
    update_interval: 2s
    rotation: 270
    invert_colors: true
    color_order: BGR
    pixel_mode: 16bit
    dimensions:
      width: 280
      height: 240
      offset_width: 20
      offset_height: 0
    auto_clear_enabled: false

lvgl:
  log_level: WARN
  buffer_size: 25%
  
  style_definitions:
    - id: default_style
      bg_color: 0x000000
      text_color: 0xFFFFFF
    - id: focused_style
      border_color: 0xFFFFFF
      border_width: 3

  pages:
    - id: main_page
      bg_color: 0x000000
      widgets:
        - button:
            id: red_button
            align: CENTER
            y: -40
            width: 180
            height: 50
            bg_color: 0xFF0000
            widgets:
              - label:
                  id: battery_label
                  text: "Battery: --"
                  align: CENTER

        - button:
            id: green_button
            align: CENTER
            y: 20
            width: 180
            height: 50
            bg_color: 0x00FF00
            widgets:
              - label:
                  id: voltage_label
                  text: "Voltage: --"
                  align: CENTER
                  text_color: 0x000000

        - button:
            id: blue_button
            align: CENTER
            y: 80
            width: 180
            height: 50
            bg_color: 0x0000FF
            widgets:
              - label:
                  text: "Lilka LVGL"
                  align: CENTER
                  text_color: 0xFFFFFF

# ---------------------------------------------------
# ГЛОБАЛЬНІ ЗМІННІ ТА СКРИПТ ФОКУСУ
# ---------------------------------------------------

globals:
  - id: current_button_index
    type: int
    restore_value: no
    initial_value: '0'

script:
  - id: update_focus
    then:
      - lvgl.widget.update:
          id: red_button
          border_width: !lambda 'return id(current_button_index) == 0 ? 3 : 0;'
      - lvgl.widget.update:
          id: green_button
          border_width: !lambda 'return id(current_button_index) == 1 ? 3 : 0;'
      - lvgl.widget.update:
          id: blue_button
          border_width: !lambda 'return id(current_button_index) == 2 ? 3 : 0;'

# ---------------------------------------------------
# КНОПКИ
# ---------------------------------------------------

binary_sensor:
  - platform: gpio
    pin:
      number: 38
      mode: INPUT_PULLUP
      inverted: true
    id: button_up
    name: "Button Up"
    icon: "mdi:arrow-up-circle"
    filters:
      - delayed_on_off: 50ms
    on_press:
      - lambda: |-
          id(current_button_index)--;
          if (id(current_button_index) < 0) id(current_button_index) = 2;
      - script.execute: update_focus

  - platform: gpio
    pin:
      number: 41
      mode: INPUT_PULLUP
      inverted: true
    id: button_down
    name: "Button Down"
    icon: "mdi:arrow-down-circle"
    filters:
      - delayed_on_off: 50ms
    on_press:
      - lambda: |-
          id(current_button_index)++;
          if (id(current_button_index) > 2) id(current_button_index) = 0;
      - script.execute: update_focus

  - platform: gpio
    pin:
      number: 39
      mode: INPUT_PULLUP
      inverted: true
    id: button_left
    name: "Button Left"
    icon: "mdi:arrow-left-circle"
    filters:
      - delayed_on_off: 50ms

  - platform: gpio
    pin:
      number: 40
      mode: INPUT_PULLUP
      inverted: true
    id: button_right
    name: "Button Right"
    icon: "mdi:arrow-right-circle"
    filters:
      - delayed_on_off: 50ms

  - platform: gpio
    pin:
      number: 5
      mode: INPUT_PULLUP
      inverted: true
    id: button_a
    name: "Button A"
    icon: "mdi:alpha-a-circle"
    filters:
      - delayed_on_off: 50ms

  - platform: gpio
    pin:
      number: 6
      mode: INPUT_PULLUP
      inverted: true
    id: button_b
    name: "Button B"
    icon: "mdi:alpha-b-circle"
    filters:
      - delayed_on_off: 50ms

  - platform: gpio
    pin:
      number: 10
      mode: INPUT_PULLUP
      inverted: true
    id: button_c
    name: "Button C"
    icon: "mdi:alpha-c-circle"
    filters:
      - delayed_on_off: 50ms

  - platform: gpio
    pin:
      number: 9
      mode: INPUT_PULLUP
      inverted: true
    id: button_d
    name: "Button D"
    icon: "mdi:alpha-d-circle"
    filters:
      - delayed_on_off: 50ms

  - platform: gpio
    pin:
      number: 4
      mode: INPUT_PULLUP
      inverted: true
    id: button_start
    name: "Button Start"
    icon: "mdi:play-circle"
    filters:
      - delayed_on_off: 50ms

  - platform: gpio
    pin:
      number: 0
      mode: INPUT_PULLUP
      inverted: true
    id: button_select
    name: "Button Select"
    icon: "mdi:checkbox-marked-circle"
    filters:
      - delayed_on_off: 50ms

# ---------------------------------------------------
# ОНОВЛЕННЯ СТАТУСУ БАТАРЕЇ НА ЕКРАНІ
# ---------------------------------------------------

interval:
  - interval: 5s
    then:
      - lvgl.label.update:
          id: battery_label
          text: !lambda |-
            return std::string("Battery: ") + to_string((int)id(battery_level).state) + "%";

      - lvgl.label.update:
          id: voltage_label
          text: !lambda |-
            char buf[20];
            snprintf(buf, sizeof(buf), "Voltage: %.2fV", id(battery_voltage).state);
            return std::string(buf);

With PSRAM 8 MB

esphome:
  name: lilka
  friendly_name: lilka
  on_boot:
    priority: 600
    then:
      - output.turn_on: display_power
      - delay: 100ms

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: esp-idf
    version: recommended
    # Оптимізація для 8MB PSRAM
    sdkconfig_options:
      # Увімкнути PSRAM
      CONFIG_SPIRAM: y
      CONFIG_SPIRAM_MODE_OCT: y
      CONFIG_SPIRAM_SPEED_80M: y
      # Використовувати PSRAM для malloc
      CONFIG_SPIRAM_USE_MALLOC: y
      CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL: "16384"
      # Кеш оптимізація
      CONFIG_ESP32S3_DATA_CACHE_64KB: y
      CONFIG_ESP32S3_DATA_CACHE_LINE_64B: y

# Налаштування для 8MB PSRAM
psram:
  mode: octal
  speed: 80MHz

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  power_save_mode: none
  ap:
    ssid: "Lilka Fallback Hotspot"
    password: "hnYImYM4O6I0"

captive_portal:

web_server:
  port: 80

api:
  encryption:
    key: "Yw3yq8lxsaze+jdggjAwxKpV4GD6gNZptvw4d6OPfOc="

logger:
  level: INFO
  logs:
    lvgl: WARN
    speaker_media_player: INFO
    i2s_audio.speaker: INFO

ota:
  - platform: esphome
    password: "bb198feb99159fdaefc8092f10039acd"

output:
  - platform: gpio
    pin: 46
    id: display_power
    inverted: false

i2s_audio:
  - id: i2s_out
    i2s_lrclk_pin: GPIO1
    i2s_bclk_pin: GPIO42

speaker:
  - platform: i2s_audio
    id: lilka_speaker
    dac_type: external
    i2s_audio_id: i2s_out
    i2s_dout_pin: GPIO2
    channel: mono
    sample_rate: 16000
    bits_per_sample: 16bit
    # З 8MB PSRAM можна збільшити буфер для кращої якості
    buffer_duration: 500ms
 
media_player:
  - platform: speaker
    name: "Lilka Media Player"
    id: lilka_media_player
    # З PSRAM можна збільшити буфер
    buffer_size: 50000
    # Можна увімкнути codec support з PSRAM
    codec_support_enabled: true
    announcement_pipeline:
        speaker: lilka_speaker
        format: MP3  # Тепер можна використовувати MP3 або FLAC
        num_channels: 1

# ---------------------------------------------------
# СЕНСОРИ
# ---------------------------------------------------

sensor:
  - platform: adc
    pin: GPIO3
    id: battery_voltage
    name: "Battery Voltage"
    icon: "mdi:lightning-bolt"
    update_interval: 10s
    attenuation: 12db
    filters:
      - multiply: 1.33
    
  - platform: template
    id: battery_level
    name: "Battery Level"
    icon: "mdi:battery"
    unit_of_measurement: "%"
    accuracy_decimals: 0
    update_interval: 10s
    lambda: |-
      float voltage = id(battery_voltage).state;
      float percent = (voltage - 3.0) / (4.2 - 3.0) * 100.0;
      if (percent > 100) percent = 100;
      if (percent < 0) percent = 0;
      return percent;

# ---------------------------------------------------
# ДИСПЛЕЙ ST7789V + LVGL
# ---------------------------------------------------

spi:
  clk_pin: 18
  mosi_pin: 17

display:
  - platform: mipi_spi
    model: ST7789V
    id: my_display
    dc_pin: 15
    cs_pin: 7
    # З PSRAM можна частіше оновлювати
    update_interval: 1s
    rotation: 270
    invert_colors: true
    color_order: BGR
    pixel_mode: 16bit
    dimensions:
      width: 280
      height: 240
      offset_width: 20
      offset_height: 0
    auto_clear_enabled: false

lvgl:
  log_level: WARN
  # З PSRAM можна використовувати більший буфер для плавнішого відображення
  buffer_size: 50%
  
  style_definitions:
    - id: default_style
      bg_color: 0x000000
      text_color: 0xFFFFFF
    - id: focused_style
      border_color: 0xFFFFFF
      border_width: 3

  pages:
    - id: main_page
      bg_color: 0x000000
      widgets:
        - button:
            id: red_button
            align: CENTER
            y: -40
            width: 180
            height: 50
            bg_color: 0xFF0000
            widgets:
              - label:
                  id: battery_label
                  text: "Battery: --"
                  align: CENTER

        - button:
            id: green_button
            align: CENTER
            y: 20
            width: 180
            height: 50
            bg_color: 0x00FF00
            widgets:
              - label:
                  id: voltage_label
                  text: "Voltage: --"
                  align: CENTER
                  text_color: 0x000000

        - button:
            id: blue_button
            align: CENTER
            y: 80
            width: 180
            height: 50
            bg_color: 0x0000FF
            widgets:
              - label:
                  text: "Lilka LVGL"
                  align: CENTER
                  text_color: 0xFFFFFF

# ---------------------------------------------------
# ГЛОБАЛЬНІ ЗМІННІ ТА СКРИПТ ФОКУСУ
# ---------------------------------------------------

globals:
  - id: current_button_index
    type: int
    restore_value: no
    initial_value: '0'

script:
  - id: update_focus
    then:
      - lvgl.widget.update:
          id: red_button
          border_width: !lambda 'return id(current_button_index) == 0 ? 3 : 0;'
      - lvgl.widget.update:
          id: green_button
          border_width: !lambda 'return id(current_button_index) == 1 ? 3 : 0;'
      - lvgl.widget.update:
          id: blue_button
          border_width: !lambda 'return id(current_button_index) == 2 ? 3 : 0;'

# ---------------------------------------------------
# КНОПКИ
# ---------------------------------------------------

binary_sensor:
  - platform: gpio
    pin:
      number: 38
      mode: INPUT_PULLUP
      inverted: true
    id: button_up
    name: "Button Up"
    icon: "mdi:arrow-up-circle"
    filters:
      - delayed_on_off: 50ms
    on_press:
      - lambda: |-
          id(current_button_index)--;
          if (id(current_button_index) < 0) id(current_button_index) = 2;
      - script.execute: update_focus

  - platform: gpio
    pin:
      number: 41
      mode: INPUT_PULLUP
      inverted: true
    id: button_down
    name: "Button Down"
    icon: "mdi:arrow-down-circle"
    filters:
      - delayed_on_off: 50ms
    on_press:
      - lambda: |-
          id(current_button_index)++;
          if (id(current_button_index) > 2) id(current_button_index) = 0;
      - script.execute: update_focus

  - platform: gpio
    pin:
      number: 39
      mode: INPUT_PULLUP
      inverted: true
    id: button_left
    name: "Button Left"
    icon: "mdi:arrow-left-circle"
    filters:
      - delayed_on_off: 50ms

  - platform: gpio
    pin:
      number: 40
      mode: INPUT_PULLUP
      inverted: true
    id: button_right
    name: "Button Right"
    icon: "mdi:arrow-right-circle"
    filters:
      - delayed_on_off: 50ms

  - platform: gpio
    pin:
      number: 5
      mode: INPUT_PULLUP
      inverted: true
    id: button_a
    name: "Button A"
    icon: "mdi:alpha-a-circle"
    filters:
      - delayed_on_off: 50ms

  - platform: gpio
    pin:
      number: 6
      mode: INPUT_PULLUP
      inverted: true
    id: button_b
    name: "Button B"
    icon: "mdi:alpha-b-circle"
    filters:
      - delayed_on_off: 50ms

  - platform: gpio
    pin:
      number: 10
      mode: INPUT_PULLUP
      inverted: true
    id: button_c
    name: "Button C"
    icon: "mdi:alpha-c-circle"
    filters:
      - delayed_on_off: 50ms

  - platform: gpio
    pin:
      number: 9
      mode: INPUT_PULLUP
      inverted: true
    id: button_d
    name: "Button D"
    icon: "mdi:alpha-d-circle"
    filters:
      - delayed_on_off: 50ms

  - platform: gpio
    pin:
      number: 4
      mode: INPUT_PULLUP
      inverted: true
    id: button_start
    name: "Button Start"
    icon: "mdi:play-circle"
    filters:
      - delayed_on_off: 50ms

  - platform: gpio
    pin:
      number: 0
      mode: INPUT_PULLUP
      inverted: true
    id: button_select
    name: "Button Select"
    icon: "mdi:checkbox-marked-circle"
    filters:
      - delayed_on_off: 50ms

# ---------------------------------------------------
# ОНОВЛЕННЯ СТАТУСУ БАТАРЕЇ НА ЕКРАНІ
# ---------------------------------------------------

interval:
  - interval: 5s
    then:
      - lvgl.label.update:
          id: battery_label
          text: !lambda |-
            return std::string("Battery: ") + to_string((int)id(battery_level).state) + "%";

      - lvgl.label.update:
          id: voltage_label
          text: !lambda |-
            char buf[20];
            snprintf(buf, sizeof(buf), "Voltage: %.2fV", id(battery_voltage).state);
            return std::string(buf);