Connect your device via USB and click the button below to install.
substitutions:
name: guition-knob-185-v2
friendly_name: Guition Knob 1.85 v2
# User settings
startup_sound_file: Home_Connected # options are: Home_Connected or Home_Connected_Male
imagemodel: "HA-character" # options are: AlbertQuackmore, Alita, Casita, Gwen, HA-character, ha-girl, Hacker, Salem, Sarah, Shaun
# Home Assistant weather entity.
ha_weather_entity: weather.forecast_home
# presence and light sensors
sensor1_entity: "binary_sensor.room_1_presence"
light1_entity: "binary_sensor.room_1_light"
sensor1_label: "Room 1"
sensor2_entity: "binary_sensor.room_2_presence"
light2_entity: "binary_sensor.room_2_light"
sensor2_label: "Room 2"
sensor3_entity: "binary_sensor.room_3_presence"
light3_entity: "binary_sensor.room_3_light"
sensor3_label: "Room 3"
sensor4_entity: "binary_sensor.room_4_presence"
light4_entity: "binary_sensor.room_4_light"
sensor4_label: "Room 4"
sensor5_entity: "binary_sensor.room_5_presence"
light5_entity: "binary_sensor.room_5_light"
sensor5_label: "Room 5"
sensor6_entity: "binary_sensor.room_6_presence"
light6_entity: "binary_sensor.room_6_light"
sensor6_label: "Room 6"
# pulldown menu switches (labels are set from Home Assistant, see the text entities)
pulldown1_entity: "light.ceiling"
pulldown2_entity: "light.ceiling_2"
pulldown3_entity: "light.hue_p45_7_light"
pulldown4_entity: "light.color_light"
display_model: "JC3636W518V2"
pdm_mic_type: "true"
imagewidth: "360"
imageheight: "360"
screenwidth: "360"
screenheight: "360"
album_fade_time: "1500ms"
media_sample_rate: 48000
ui_phase_standby: "0"
ui_phase_player_controls: "1"
ui_phase_now_playing: "2"
ui_phase_playlist_menu: "3"
ui_phase_pulldown: "4"
clock_style_digital: "0"
clock_style_analog: "1"
clock_style_rooms: "2"
clock_style_retro: "3"
clock_style_mario: "4"
spi:
id: display_qspi
type: quad
clk_pin: GPIO11
data_pins: [GPIO13, GPIO14, GPIO15, GPIO16]
i2c:
id: touchscreen_bus
sda: GPIO9
scl: GPIO10
external_components:
- source: github://RAR/esphome-drv2605
components: [drv2605]
refresh: 1h
packages:
extcomp:
url: https://github.com/RealDeco/xiaozhi-esphome.git
ref: main
refresh: 0s
files:
- devices/Under_Development/Modular/HW/sendspin-external_components_2026.8.0.yaml
esphome:
name: ${name}
friendly_name: ${friendly_name}
min_version: 2026.9.1
name_add_mac_suffix: true
project:
name: realdeco.sendspin_guition
version: "2026.9.1"
on_boot:
priority: -100
then:
- script.execute:
id: set_ui_phase
phase: ${ui_phase_standby}
- script.execute: refresh_playlist_labels
- script.execute: refresh_pulldown_labels
- script.execute: apply_accent_color_debounced
- script.execute: led_ring_apply_state
- script.execute:
id: apply_clock_style
style_mode: !lambda |-
std::string opt = id(clock_style_choice).current_option();
if (opt == "Analog Clock") return ${clock_style_analog};
if (opt == "Rooms") return ${clock_style_rooms};
if (opt == "Retro") return ${clock_style_retro};
if (opt == "Mario") return ${clock_style_mario};
return ${clock_style_digital};
esp32:
variant: esp32s3
flash_size: 16MB
framework:
type: esp-idf
advanced:
execute_from_psram: True
# sdkconfig_options:
# CONFIG_SPIRAM_RODATA: "y"
# CONFIG_SPIRAM_USE_MALLOC: "y"
# CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL: "2048"
logger:
level: WARN
api:
on_client_connected:
- lambda: |-
if (!id(boot_sound_played)) {
id(boot_sound_played) = true;
if (id(startup_sound_switch).state) {
id(play_sound)->execute(true, std::string("ready_sound"));
}
}
- delay: 1s
- script.execute:
id: apply_clock_style
style_mode: !lambda 'return id(clock_style_mode);'
- script.execute: rooms_grid_apply
- script.execute: refresh_pulldown_states
ota:
- platform: esphome
wifi:
on_connect:
- delay: 1s
- if:
condition:
switch.is_on: standby_background_enabled
then:
- script.execute: load_standby_background
ap:
improv_serial:
psram:
mode: octal
speed: 80MHz
ignore_not_found: false
captive_portal:
button:
- platform: restart
id: restart_button
name: "Restart"
entity_category: config
icon: "mdi:restart"
time:
- platform: homeassistant
id: homeassistant_time
on_time_sync:
- component.update: clock_hms
- component.update: clock_date
interval:
- interval: 1s
then:
- script.execute: analog_clock_redraw
- script.execute: retro_clock_redraw
- script.execute: mario_clock_tick
globals:
- id: sendspin_album_art_valid
type: bool
restore_value: no
initial_value: "false"
- id: last_album_art_valid
type: bool
restore_value: false
initial_value: 'false'
- id: album_art_slide_active
type: bool
restore_value: no
initial_value: "false"
- id: artist_art_valid
type: bool
restore_value: no
initial_value: "false"
- id: now_playing_art_mode
type: bool
restore_value: no
initial_value: "false"
- id: current_album_name
type: std::string
restore_value: no
- id: previous_album_name
type: std::string
restore_value: no
- id: confirmed_art_album_name
type: std::string
restore_value: no
- id: album_art_awaiting_swipe_art
type: bool
restore_value: no
initial_value: "false"
- id: pre_previous_swipe_title
type: std::string
restore_value: no
initial_value: '""'
- id: boot_sound_played
type: bool
restore_value: no
initial_value: "false"
- id: boot_announce_active
type: bool
restore_value: no
initial_value: "false"
- id: weather_available
type: bool
restore_value: no
initial_value: "false"
- id: ui_phase
type: int
restore_value: no
initial_value: "${ui_phase_standby}"
- id: touch_active
type: bool
restore_value: no
initial_value: "false"
- id: touch_swipe_detected
type: bool
restore_value: no
initial_value: "false"
- id: touch_start_x
type: int
restore_value: no
initial_value: "0"
- id: touch_start_y
type: int
restore_value: no
initial_value: "0"
- id: touch_last_x
type: int
restore_value: no
initial_value: "0"
- id: touch_last_y
type: int
restore_value: no
initial_value: "0"
- id: touch_start_ms
type: uint32_t
restore_value: no
initial_value: "0"
- id: preferred_screen_brightness
type: float
restore_value: yes
initial_value: "1.0"
- id: screen_timeout_dimmed
type: bool
restore_value: no
initial_value: "false"
- id: haptic_last_ms
type: uint32_t
restore_value: no
initial_value: "0"
- id: volume_ring_overlay
type: lv_obj_t*
restore_value: no
initial_value: "nullptr"
- id: volume_ring_label
type: lv_obj_t*
restore_value: no
initial_value: "nullptr"
- id: accent_red
type: int
restore_value: yes
initial_value: "255"
- id: accent_green
type: int
restore_value: yes
initial_value: "255"
- id: accent_blue
type: int
restore_value: yes
initial_value: "0"
- id: accent2_red
type: int
restore_value: yes
initial_value: "255"
- id: accent2_green
type: int
restore_value: yes
initial_value: "255"
- id: accent2_blue
type: int
restore_value: yes
initial_value: "255"
- id: accent3_red
type: int
restore_value: yes
initial_value: "102"
- id: accent3_green
type: int
restore_value: yes
initial_value: "204"
- id: accent3_blue
type: int
restore_value: yes
initial_value: "255"
- id: accent4_red
type: int
restore_value: yes
initial_value: "255"
- id: accent4_green
type: int
restore_value: yes
initial_value: "255"
- id: accent4_blue
type: int
restore_value: yes
initial_value: "255"
- id: analog_clock_canvas_widget
type: lv_obj_t*
restore_value: no
initial_value: "nullptr"
- id: retro_static_buf
type: uint8_t*
restore_value: no
initial_value: "nullptr"
- id: clock_style_mode
type: int
restore_value: no
initial_value: "${clock_style_digital}"
- id: mario_anim_active
type: bool
restore_value: no
initial_value: "false"
- id: mario_y_offset
type: int
restore_value: no
initial_value: "0"
- id: mario_direction_up
type: bool
restore_value: no
initial_value: "true"
- id: mario_last_minute
type: int
restore_value: no
initial_value: "-1"
- id: hourblock_offset
type: int
restore_value: no
initial_value: "0"
- id: hourblock_bouncing
type: bool
restore_value: no
initial_value: "false"
- id: minuteblock_offset
type: int
restore_value: no
initial_value: "0"
- id: minuteblock_bouncing
type: bool
restore_value: no
initial_value: "false"
- id: hourblock_going_up
type: bool
restore_value: no
initial_value: "true"
- id: minuteblock_going_up
type: bool
restore_value: no
initial_value: "true"
- id: mario_displayed_hour
type: int
restore_value: no
initial_value: "0"
- id: mario_displayed_minute
type: int
restore_value: no
initial_value: "0"
image:
- platform: file
file: https://raw.githubusercontent.com/RealDeco/xiaozhi-esphome/main/images/${imagemodel}/360x360/playing.png
id: fallback_playing_image
resize: ${imageheight}x${imageheight}
type: RGB565
dither: NONE
### wallpaper
- platform: online_image
id: standby_background_image
url: https://raw.githubusercontent.com/RealDeco/xiaozhi-esphome/main/images/Other/360x360/clock.png
format: PNG
type: RGB565
resize: ${imagewidth}x${imageheight}
update_interval: never
on_download_finished:
- if:
condition:
switch.is_on: standby_background_enabled
then:
- lvgl.image.update:
id: standby_background_widget
src: standby_background_image
- lvgl.image.update:
id: pulldown_background_widget
src: standby_background_image
- lambda: |-
lv_obj_clear_flag(id(standby_background_widget), LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(id(pulldown_background_widget), LV_OBJ_FLAG_HIDDEN);
### sendspin album art
- platform: sendspin
id: album_slot
format: JPEG
type: RGB565
resize: ${imagewidth}x${imageheight}
display_offset: 1s
source: ALBUM
current_image:
id: sendspin_album_art
transition_image:
id: sendspin_album_art_transition
on_image_display:
- lambda: |-
id(last_album_art_valid) = id(sendspin_album_art_valid);
id(sendspin_album_art_valid) = true;
id(album_art_awaiting_swipe_art) = false;
id(confirmed_art_album_name) = id(sendspin_album).get_state();
- if:
condition:
lambda: 'return !id(album_art_slide_active);'
then:
- script.execute: album_art_crossfade_apply
else:
- sendspin.image.transition_finished: album_slot
- lvgl.image.update:
id: controls_album_art_widget
src: sendspin_album_art
- lvgl.image.update:
id: playlist_album_art_widget
src: sendspin_album_art
on_image_clear:
- logger.log:
format: "Album art cleared"
level: WARN
- lambda: |-
id(last_album_art_valid) = id(sendspin_album_art_valid);
id(sendspin_album_art_valid) = false;
- script.execute: album_art_show_fallback
on_image_error:
- logger.log:
format: "Album art error"
level: WARN
- lambda: |-
id(last_album_art_valid) = id(sendspin_album_art_valid);
id(sendspin_album_art_valid) = false;
- script.execute: album_art_show_fallback
- platform: sendspin
id: artist_slot
format: PNG
type: RGB565
resize: ${imagewidth}x${imageheight}
source: artist
current_image:
id: artist_art
on_image_display:
- logger.log:
format: "Artist art available (late by %u ms)"
args: ["(unsigned) lateness_ms"]
level: INFO
- lambda: |-
id(artist_art_valid) = true;
id(refresh_artist_art_dot)->execute();
on_image_clear:
- logger.log:
format: "No artist art for this song"
level: INFO
- lambda: |-
id(artist_art_valid) = false;
id(refresh_artist_art_dot)->execute();
on_image_error:
- lambda: |-
ESP_LOGW("artist_art", "failed to decode artist art");
id(artist_art_valid) = false;
id(refresh_artist_art_dot)->execute();
sendspin:
id: sendspin_hub
task_stack_in_psram: true
drv2605:
id: haptic_driver
i2c_id: touchscreen_bus
address: 0x5A
motor_type: LRA
library: 6
audio_file:
- id: ready_sound
file: https://github.com/RealDeco/xiaozhi-esphome/raw/main/sounds/${startup_sound_file}.flac
http_request:
switch:
- platform: template
id: sendspin_delay_adjustable
name: "Sendspin adjustable delay"
entity_category: config
optimistic: true
restore_mode: RESTORE_DEFAULT_OFF
turn_on_action:
- sendspin.media_source.enable_static_delay_adjustment:
turn_off_action:
- sendspin.media_source.disable_static_delay_adjustment:
- platform: gpio
name: Speaker Enable
icon: "mdi:speaker"
entity_category: config
pin:
number: GPIO46
ignore_strapping_warning: true
restore_mode: RESTORE_DEFAULT_ON
- platform: template
id: startup_sound_switch
name: Startup sound
icon: "mdi:card-text-outline"
entity_category: config
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
- platform: template
id: rotary_haptic_feedback_enabled
name: Rotary haptic feedback
icon: "mdi:vibrate"
entity_category: config
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
- platform: template
id: led_ring_enabled
name: LED ring
icon: "mdi:led-strip-variant"
entity_category: config
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- script.execute: led_ring_apply_state
on_turn_off:
- light.turn_off: rgb_ring
- platform: template
id: standby_screen_timeout_enabled
name: Standby screen timeout
icon: "mdi:monitor-off"
entity_category: config
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- script.execute: standby_screen_activity
on_turn_off:
- script.execute: standby_screen_activity
- platform: template
id: standby_background_enabled
name: Standby clock background
icon: "mdi:image-outline"
entity_category: config
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- script.execute: load_standby_background
on_turn_off:
- lambda: |-
lv_obj_add_flag(id(standby_background_widget), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(pulldown_background_widget), LV_OBJ_FLAG_HIDDEN);
- online_image.release: standby_background_image
- platform: template
id: clock_12h_format
name: 12-Hour Clock
icon: "mdi:clock-time-four-outline"
entity_category: config
optimistic: true
restore_mode: RESTORE_DEFAULT_OFF
on_turn_on:
- component.update: clock_hms
- script.execute: retro_clock_redraw
- script.execute:
id: mario_draw_scene
full_redraw: true
on_turn_off:
- component.update: clock_hms
- script.execute: retro_clock_redraw
- script.execute:
id: mario_draw_scene
full_redraw: true
- platform: template
id: clock_show_date
name: Show Date
icon: "mdi:calendar-blank-outline"
entity_category: config
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- if:
condition:
and:
- lvgl.page.is_showing: idle_page
- lambda: 'return id(clock_style_mode) == ${clock_style_digital};'
then:
- lvgl.widget.show: date_label
- script.execute: analog_clock_redraw
on_turn_off:
- lvgl.widget.hide: date_label
- script.execute: analog_clock_redraw
- platform: template
id: clock_show_seconds
name: Show Seconds
icon: "mdi:timer-outline"
entity_category: config
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- component.update: clock_hms
- script.execute: analog_clock_redraw
- script.execute: retro_clock_redraw
on_turn_off:
- component.update: clock_hms
- script.execute: analog_clock_redraw
- script.execute: retro_clock_redraw
- platform: template
id: clock_show_gfx
name: Show GFX
icon: "mdi:radar"
entity_category: config
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- script.execute: analog_clock_redraw
on_turn_off:
- script.execute: analog_clock_redraw
- platform: template
id: clock_show_weather
name: Show Weather
icon: "mdi:weather-partly-cloudy"
entity_category: config
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- component.update: weather_line
on_turn_off:
- component.update: weather_line
- platform: template
id: clock_show_feels_like
name: Show Feels Like
icon: "mdi:thermometer-lines"
entity_category: config
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- component.update: weather_line
on_turn_off:
- component.update: weather_line
number:
- platform: template
id: player_controls_timeout_seconds
name: Player controls timeout delay
unit_of_measurement: "s"
icon: "mdi:timer-outline"
entity_category: config
optimistic: true
restore_value: true
mode: box
min_value: 1
max_value: 60
step: 1
initial_value: 5
set_action:
- if:
condition:
lambda: 'return id(ui_phase) == ${ui_phase_player_controls};'
then:
- script.execute: player_controls_timeout
- platform: template
id: standby_screen_timeout_seconds
name: Standby screen timeout delay
unit_of_measurement: "s"
icon: "mdi:timer-outline"
entity_category: config
optimistic: true
restore_value: true
mode: box
min_value: 15
max_value: 3600
step: 5
initial_value: 60
set_action:
- script.execute: standby_screen_activity
select:
- platform: template
id: clock_style_choice
name: Clock Style
icon: "mdi:clock-outline"
entity_category: config
optimistic: true
restore_value: true
initial_option: Digital Clock
options:
- Digital Clock
- Analog Clock
- Rooms
- Retro
- Mario
set_action:
- script.execute:
id: apply_clock_style
style_mode: !lambda |-
if (x == "Analog Clock") return ${clock_style_analog};
if (x == "Rooms") return ${clock_style_rooms};
if (x == "Retro") return ${clock_style_retro};
if (x == "Mario") return ${clock_style_mario};
return ${clock_style_digital};
- platform: template
id: standby_background_choice
name: Standby clock background image
icon: "mdi:image-multiple-outline"
entity_category: config
optimistic: true
restore_value: true
initial_option: clock
options:
- chip
- clock
- hacker
- mission
- neonwaves
- redmarble
- starrysky
- tron
- waterdrops
- waves
set_action:
- script.execute: standby_screen_activity
- if:
condition:
switch.is_on: standby_background_enabled
then:
- online_image.set_url:
id: standby_background_image
url: !lambda |-
return "https://raw.githubusercontent.com/RealDeco/xiaozhi-esphome/main/images/Other/360x360/" + x + ".png";
- platform: template
id: accent_color_preset
name: Accent color preset
icon: "mdi:palette"
entity_category: config
optimistic: true
restore_value: true
initial_option: Pitch Black
options:
- Pitch Black
- Burning Red
- Cyber Blue
set_action:
- if:
condition:
lambda: return x == "Pitch Black";
then:
- light.turn_on:
id: accent_color_picker
brightness: 100%
red: 100%
green: 100%
blue: 0%
- light.turn_on:
id: accent_color_picker_2
brightness: 100%
red: 100%
green: 100%
blue: 100%
- light.turn_on:
id: accent_color_picker_3
brightness: 100%
red: 40%
green: 80%
blue: 100%
- light.turn_on:
id: accent_color_picker_4
brightness: 100%
red: 100%
green: 100%
blue: 100%
- if:
condition:
lambda: return x == "Burning Red";
then:
- light.turn_on:
id: accent_color_picker
brightness: 100%
red: 100%
green: 0%
blue: 0%
- light.turn_on:
id: accent_color_picker_2
brightness: 100%
red: 100%
green: 100%
blue: 0%
- light.turn_on:
id: accent_color_picker_3
brightness: 100%
red: 100%
green: 100%
blue: 100%
- light.turn_on:
id: accent_color_picker_4
brightness: 100%
red: 100%
green: 100%
blue: 100%
- if:
condition:
lambda: return x == "Cyber Blue";
then:
- light.turn_on:
id: accent_color_picker
brightness: 100%
red: 40%
green: 80%
blue: 100%
- light.turn_on:
id: accent_color_picker_2
brightness: 100%
red: 0%
green: 100%
blue: 40%
- light.turn_on:
id: accent_color_picker_3
brightness: 100%
red: 100%
green: 100%
blue: 0%
- light.turn_on:
id: accent_color_picker_4
brightness: 100%
red: 100%
green: 100%
blue: 100%
text:
- platform: template
id: playlist_media_player_entity
name: Playlist media player entity
entity_category: config
optimistic: true
restore_value: true
initial_value: ""
mode: text
- platform: template
id: playlist_1_name
name: Playlist 1 name
entity_category: config
optimistic: true
restore_value: true
initial_value: My playlist 1
mode: text
on_value:
- script.execute: refresh_playlist_labels
- platform: template
id: playlist_1_uri
name: Playlist 1 URI
entity_category: config
optimistic: true
restore_value: true
initial_value: ""
mode: text
- platform: template
id: playlist_2_name
name: Playlist 2 name
entity_category: config
optimistic: true
restore_value: true
initial_value: My playlist 2
mode: text
on_value:
- script.execute: refresh_playlist_labels
- platform: template
id: playlist_2_uri
name: Playlist 2 URI
entity_category: config
optimistic: true
restore_value: true
initial_value: ""
mode: text
- platform: template
id: playlist_3_name
name: Playlist 3 name
entity_category: config
optimistic: true
restore_value: true
initial_value: My playlist 3
mode: text
on_value:
- script.execute: refresh_playlist_labels
- platform: template
id: playlist_3_uri
name: Playlist 3 URI
entity_category: config
optimistic: true
restore_value: true
initial_value: ""
mode: text
- platform: template
id: playlist_4_name
name: Playlist 4 name
entity_category: config
optimistic: true
restore_value: true
initial_value: My playlist 4
mode: text
on_value:
- script.execute: refresh_playlist_labels
- platform: template
id: playlist_4_uri
name: Playlist 4 URI
entity_category: config
optimistic: true
restore_value: true
initial_value: ""
mode: text
- platform: template
id: playlist_5_name
name: Playlist 5 name
entity_category: config
optimistic: true
restore_value: true
initial_value: My playlist 5
mode: text
on_value:
- script.execute: refresh_playlist_labels
- platform: template
id: playlist_5_uri
name: Playlist 5 URI
entity_category: config
optimistic: true
restore_value: true
initial_value: ""
mode: text
- platform: template
id: playlist_6_name
name: Playlist 6 name
entity_category: config
optimistic: true
restore_value: true
initial_value: My playlist 6
mode: text
on_value:
- script.execute: refresh_playlist_labels
- platform: template
id: playlist_6_uri
name: Playlist 6 URI
entity_category: config
optimistic: true
restore_value: true
initial_value: ""
mode: text
- platform: template
id: pulldown1_label
name: Pulldown button 1 label
entity_category: config
optimistic: true
restore_value: true
initial_value: Light 1
mode: text
on_value:
- script.execute: refresh_pulldown_labels
- platform: template
id: pulldown2_label
name: Pulldown button 2 label
entity_category: config
optimistic: true
restore_value: true
initial_value: Light 2
mode: text
on_value:
- script.execute: refresh_pulldown_labels
- platform: template
id: pulldown3_label
name: Pulldown button 3 label
entity_category: config
optimistic: true
restore_value: true
initial_value: Light 3
mode: text
on_value:
- script.execute: refresh_pulldown_labels
- platform: template
id: pulldown4_label
name: Pulldown button 4 label
entity_category: config
optimistic: true
restore_value: true
initial_value: Light 4
mode: text
on_value:
- script.execute: refresh_pulldown_labels
i2s_audio:
# New 1.85" knob board:
# Speaker/PCM5100A: WS GPIO45, BCK GPIO3, DO GPIO42, no MCLK
# PDM mic: SCK GPIO5, DATA GPIO4
#
# PDM mic uses GPIO5 as clock and GPIO4 as data; no LRCLK/WS pin is routed for mic input.
- id: i2s_mic
i2s_bclk_pin: GPIO5
- id: i2s_audio_bus
i2s_lrclk_pin:
number: GPIO45
ignore_strapping_warning: true
i2s_bclk_pin: GPIO3
microphone:
- platform: i2s_audio
id: box_mic
i2s_audio_id: i2s_mic
i2s_din_pin: GPIO4
adc_type: external
channel: left
pdm: ${pdm_mic_type}
sample_rate: 16000
bits_per_sample: 16bit
speaker:
- platform: i2s_audio
id: box_speaker
i2s_audio_id: i2s_audio_bus
i2s_dout_pin: GPIO42
dac_type: external
channel: stereo
buffer_duration: 300ms
- platform: mixer
id: mixing_speaker
output_speaker: box_speaker
num_channels: 2
task_stack_in_psram: true
source_speakers:
- id: announcement_mixing_input
- id: media_mixing_input
- platform: resampler
id: announcement_resampling_speaker
output_speaker: announcement_mixing_input
sample_rate: ${media_sample_rate}
bits_per_sample: 16
- platform: resampler
id: media_resampling_speaker
output_speaker: media_mixing_input
sample_rate: ${media_sample_rate}
bits_per_sample: 16
media_source:
- platform: audio_file
id: audio_file_announcement_source
- platform: audio_http
id: http_announcement_source
- platform: audio_http
id: http_media_source
- platform: sendspin
id: sendspin_media_source
media_player:
- platform: sendspin
id: sendspin_group_media_player
name: Sendspin Group Media Player
on_idle:
- if:
condition:
lambda: 'return id(ui_phase) != ${ui_phase_player_controls};'
then:
- script.execute:
id: set_ui_phase
phase: ${ui_phase_standby}
- platform: speaker_source
id: external_media_player
name: Media Player
announcement_pipeline:
format: FLAC
num_channels: 1
sample_rate: ${media_sample_rate}
speaker: announcement_resampling_speaker
sources:
- audio_file_announcement_source
- http_announcement_source
media_pipeline:
format: FLAC
num_channels: 2
sample_rate: ${media_sample_rate}
speaker: media_resampling_speaker
sources:
- http_media_source
- sendspin_media_source
volume_increment: 0.05
volume_min: 0.4
volume_max: 0.85
on_play:
then:
- script.execute: led_ring_apply_state
- if:
condition:
lambda: 'return id(ui_phase) == ${ui_phase_player_controls};'
then:
- script.execute:
id: set_ui_phase
phase: ${ui_phase_player_controls}
else:
- script.execute:
id: set_ui_phase
phase: ${ui_phase_now_playing}
on_announcement:
then:
- if:
condition:
lambda: 'return id(boot_announce_active);'
then:
- lvgl.page.show: boot_sound_page
- delay: 2s
- script.execute:
id: set_ui_phase
phase: !lambda 'return id(ui_phase);'
- lambda: 'id(boot_announce_active) = false;'
on_idle:
then:
- script.execute: led_ring_apply_state
on_pause:
then:
- script.execute: led_ring_apply_state
- script.execute:
id: set_ui_phase
phase: ${ui_phase_player_controls}
binary_sensor:
- platform: gpio
id: rotary_cw_pulse
internal: true
pin:
number: GPIO1
mode:
input: true
pullup: true
use_interrupt: false
filters:
- delayed_on_off: 2ms
on_press:
then:
- logger.log:
level: WARN
tag: rotary
format: "GPIO1 CW pulse"
- script.execute: standby_screen_activity
- media_player.volume_up: external_media_player
- script.execute: haptic_click
- script.execute: show_rotary_volume_feedback
- platform: gpio
id: rotary_ccw_pulse
internal: true
pin:
number: GPIO2
mode:
input: true
pullup: true
use_interrupt: false
filters:
- delayed_on_off: 2ms
on_press:
then:
- logger.log:
level: WARN
tag: rotary
format: "GPIO2 CCW pulse"
- script.execute: standby_screen_activity
- media_player.volume_down: external_media_player
- script.execute: haptic_click
- script.execute: show_rotary_volume_feedback
- platform: homeassistant
id: room1_presence
entity_id: ${sensor1_entity}
on_state:
- script.execute: rooms_grid_apply
- platform: homeassistant
id: room1_light
entity_id: ${light1_entity}
on_state:
- script.execute: rooms_grid_apply
- platform: homeassistant
id: room2_presence
entity_id: ${sensor2_entity}
on_state:
- script.execute: rooms_grid_apply
- platform: homeassistant
id: room2_light
entity_id: ${light2_entity}
on_state:
- script.execute: rooms_grid_apply
- platform: homeassistant
id: room3_presence
entity_id: ${sensor3_entity}
on_state:
- script.execute: rooms_grid_apply
- platform: homeassistant
id: room3_light
entity_id: ${light3_entity}
on_state:
- script.execute: rooms_grid_apply
- platform: homeassistant
id: room4_presence
entity_id: ${sensor4_entity}
on_state:
- script.execute: rooms_grid_apply
- platform: homeassistant
id: room4_light
entity_id: ${light4_entity}
on_state:
- script.execute: rooms_grid_apply
- platform: homeassistant
id: room5_presence
entity_id: ${sensor5_entity}
on_state:
- script.execute: rooms_grid_apply
- platform: homeassistant
id: room5_light
entity_id: ${light5_entity}
on_state:
- script.execute: rooms_grid_apply
- platform: homeassistant
id: room6_presence
entity_id: ${sensor6_entity}
on_state:
- script.execute: rooms_grid_apply
- platform: homeassistant
id: room6_light
entity_id: ${light6_entity}
on_state:
- script.execute: rooms_grid_apply
- platform: homeassistant
id: pulldown1_state
entity_id: ${pulldown1_entity}
on_state:
- script.execute: refresh_pulldown_states
- platform: homeassistant
id: pulldown2_state
entity_id: ${pulldown2_entity}
on_state:
- script.execute: refresh_pulldown_states
- platform: homeassistant
id: pulldown3_state
entity_id: ${pulldown3_entity}
on_state:
- script.execute: refresh_pulldown_states
- platform: homeassistant
id: pulldown4_state
entity_id: ${pulldown4_entity}
on_state:
- script.execute: refresh_pulldown_states
debug:
sensor:
- platform: debug
free:
name: "SRAM Free Memory"
block:
name: "SRAM Largest Contiguous Block"
psram:
name: "PSRAM Free Memory"
- platform: homeassistant
id: ha_weather_temperature
entity_id: ${ha_weather_entity}
attribute: temperature
on_value:
- component.update: weather_temp_big
- component.update: weather_line
- platform: homeassistant
id: ha_weather_wind_speed
entity_id: ${ha_weather_entity}
attribute: wind_speed
on_value:
- component.update: weather_line
text_sensor:
- platform: homeassistant
id: ha_weather_condition
entity_id: ${ha_weather_entity}
on_value:
- lambda: |-
const char* icon = "\U000F0590";
auto c = id(ha_weather_condition).state;
if (c == "sunny" || c == "clear") {
icon = "\U000F0599";
} else if (c == "partlycloudy") {
icon = "\U000F0595";
} else if (c == "cloudy") {
icon = "\U000F0590";
} else if (c == "rainy") {
icon = "\U000F0597";
} else if (c == "pouring") {
icon = "\U000F0596";
} else if (c == "snowy") {
icon = "\U000F0598";
} else if (c == "lightning") {
icon = "\U000F0593";
} else if (c == "hail") {
icon = "\U000F0592";
}
lv_label_set_text(id(weather_icon_glow), icon);
lv_label_set_text(id(weather_icon), icon);
lv_label_set_text(id(analog_weather_icon), icon);
- platform: homeassistant
id: ha_weather_temperature_unit
entity_id: ${ha_weather_entity}
attribute: temperature_unit
on_value:
- component.update: weather_temp_big
- component.update: weather_line
- platform: homeassistant
id: ha_weather_wind_speed_unit
entity_id: ${ha_weather_entity}
attribute: wind_speed_unit
on_value:
- component.update: weather_line
- platform: sendspin
id: sendspin_title
name: "Song Title"
type: title
on_value:
- component.update: now_playing_text
- lambda: |-
id(artist_art_valid) = false;
id(now_playing_art_mode) = false;
id(refresh_artist_art_dot)->execute();
- lambda: |-
id(previous_album_name) = id(sendspin_album).get_state();
- delay: 100ms
- lambda: |-
id(current_album_name) = id(sendspin_album).get_state();
- lambda: |-
ESP_LOGW("albumcheck", "title-fired | current_album='%s' | previous_album='%s' | art_valid_before=%d",
id(current_album_name).c_str(),
id(previous_album_name).c_str(),
id(sendspin_album_art_valid));
- if:
condition:
and:
- lambda: 'return id(album_art_awaiting_swipe_art);'
- lambda: 'return id(current_album_name) == id(previous_album_name);'
- lambda: 'return id(current_album_name) == id(confirmed_art_album_name);'
then:
- lambda: |-
id(sendspin_album_art_valid) = true;
id(album_art_awaiting_swipe_art) = false;
- script.execute: album_art_crossfade_apply
else:
- lambda: |-
id(album_art_awaiting_swipe_art) = false;
- lvgl.label.update:
id: controls_title_label
text: !lambda return x;
- platform: sendspin
id: sendspin_artist
name: "Song Artist"
type: artist
on_value:
- component.update: now_playing_text
- lvgl.label.update:
id: controls_artist_label
text: !lambda return x;
- platform: sendspin
id: sendspin_album
name: "Album Name"
type: album
on_value:
- component.update: now_playing_text
- lvgl.label.update:
id: controls_album_label
text: !lambda return x;
- platform: template
id: now_playing_text
update_interval: never
lambda: |-
std::string s = id(sendspin_title).get_state();
auto a = id(sendspin_artist).get_state();
if (!a.empty()) {
if (!s.empty()) s += " - ";
s += a;
}
return s;
on_value:
- lvgl.label.update:
id: now_playing_label
text: !lambda return x;
- platform: template
id: clock_hms
update_interval: 1s
lambda: |-
auto t = id(homeassistant_time).now();
if (!t.is_valid()) return std::string("--:--:--");
bool show_seconds = id(clock_show_seconds).state;
int hour_disp = t.hour;
if (id(clock_12h_format).state) {
hour_disp = t.hour % 12;
if (hour_disp == 0) hour_disp = 12;
}
bool hide_colons = show_seconds && ((t.second % 2) == 0);
char buf[16];
if (show_seconds) {
if (hide_colons) snprintf(buf, sizeof(buf), "%02d %02d %02d", hour_disp, t.minute, t.second);
else snprintf(buf, sizeof(buf), "%02d:%02d:%02d", hour_disp, t.minute, t.second);
} else {
if (hide_colons) snprintf(buf, sizeof(buf), "%02d %02d", hour_disp, t.minute);
else snprintf(buf, sizeof(buf), "%02d:%02d", hour_disp, t.minute);
}
return std::string(buf);
on_value:
- if:
condition:
and:
- lvgl.page.is_showing: idle_page
- lambda: 'return id(clock_style_mode) == ${clock_style_digital};'
then:
- lvgl.label.update:
id: clock_label
text: !lambda return x;
- lambda: |-
if (!id(clock_12h_format).state) {
lv_obj_add_flag(id(clock_ampm_label), LV_OBJ_FLAG_HIDDEN);
return;
}
auto t = id(homeassistant_time).now();
lv_label_set_text(id(clock_ampm_label), (t.is_valid() && t.hour >= 12) ? "P" : "A");
lv_point_t txt_size;
lv_text_get_size(&txt_size, x.c_str(), id(font_7seg)->get_lv_font(), 0, 0, LV_COORD_MAX, LV_TEXT_FLAG_NONE);
lv_area_t clock_area;
lv_obj_get_coords(id(clock_label), &clock_area);
int32_t label_cx = (clock_area.x1 + clock_area.x2) / 2;
int32_t label_bottom = clock_area.y2;
lv_obj_set_pos(id(clock_ampm_label), label_cx + txt_size.x / 2 + 4, label_bottom - 26);
lv_obj_clear_flag(id(clock_ampm_label), LV_OBJ_FLAG_HIDDEN);
- if:
condition:
and:
- lvgl.page.is_showing: idle_page
- lambda: 'return id(clock_style_mode) == ${clock_style_rooms};'
then:
- lvgl.label.update:
id: rooms_clock_label
text: !lambda return x;
- platform: template
id: clock_date
update_interval: 30s
lambda: |-
auto t = id(homeassistant_time).now();
if (!t.is_valid()) return std::string("------");
static const char* months[] = {
"January","February","March","April","May","June",
"July","August","September","October","November","December"
};
const char* mon = (t.month >= 1 && t.month <= 12) ? months[t.month - 1] : "???";
char buf[32];
snprintf(buf, sizeof(buf), "%d %s", t.day_of_month, mon);
return std::string(buf);
on_value:
- if:
condition:
and:
- lvgl.page.is_showing: idle_page
- lambda: 'return id(clock_style_mode) == ${clock_style_digital};'
then:
- lvgl.label.update:
id: date_label
text: !lambda return x;
- if:
condition:
and:
- lvgl.page.is_showing: idle_page
- lambda: 'return id(clock_style_mode) == ${clock_style_rooms};'
then:
- lvgl.label.update:
id: rooms_date_label
text: !lambda return x;
- platform: template
id: weather_temp_big
update_interval: 15s
lambda: |-
if (!id(ha_weather_temperature).has_state()) return std::string("--");
std::string tunit = id(ha_weather_temperature_unit).state.c_str();
if (tunit.empty()) tunit = "\u00B0C";
lv_label_set_text(id(weather_big_temp_unit), tunit.c_str());
int ti = (int) lroundf(id(ha_weather_temperature).state);
char buf[8];
snprintf(buf, sizeof(buf), "%d", ti);
char abuf[16];
snprintf(abuf, sizeof(abuf), "%d%s", ti, tunit.c_str());
lv_label_set_text(id(analog_weather_temp), abuf);
return std::string(buf);
on_value:
- if:
condition:
and:
- lvgl.page.is_showing: idle_page
- lambda: 'return id(clock_style_mode) == ${clock_style_digital};'
then:
- lvgl.label.update:
id: weather_big_temp
text: !lambda return x;
- platform: template
id: weather_line
update_interval: 15s
lambda: |-
if (!id(ha_weather_temperature).has_state() || !id(ha_weather_wind_speed).has_state()) {
return std::string("Weather unavailable");
}
std::string tunit = id(ha_weather_temperature_unit).state.c_str();
if (tunit.empty()) tunit = "\u00B0C";
std::string wunit = id(ha_weather_wind_speed_unit).state.c_str();
if (wunit.empty()) wunit = "m/s";
float temp_c = id(ha_weather_temperature).state;
float wind_ms = id(ha_weather_wind_speed).state;
float feels_c = temp_c;
if (temp_c <= 10.0f && wind_ms > 1.3f) {
float wind_kmh = wind_ms * 3.6f;
float v16 = powf(wind_kmh, 0.16f);
feels_c = 13.12f + 0.6215f * temp_c - 11.37f * v16 + 0.3965f * temp_c * v16;
}
char buf[160];
snprintf(buf, sizeof(buf),
"Wind speed: %.1f %s\nFeels like: %.1f%s",
wind_ms, wunit.c_str(),
feels_c, tunit.c_str());
return std::string(buf);
on_value:
- lambda: |-
id(weather_available) = (x != "Weather unavailable");
- if:
condition:
and:
- lvgl.page.is_showing: idle_page
- lambda: 'return id(clock_style_mode) == ${clock_style_digital};'
then:
- if:
condition:
and:
- lambda: return id(weather_available);
- lambda: return id(clock_show_weather).state;
then:
- lvgl.widget.show: weather_header
else:
- lvgl.widget.hide: weather_header
- if:
condition:
and:
- lambda: return id(weather_available);
- lambda: return id(clock_show_feels_like).state;
then:
- lvgl.widget.show: weather_label
- lvgl.label.update:
id: weather_label
text: !lambda return x;
else:
- lvgl.widget.hide: weather_label
else:
- lvgl.widget.hide: weather_header
- lvgl.widget.hide: weather_label
- if:
condition:
and:
- lvgl.page.is_showing: idle_page
- lambda: 'return id(clock_style_mode) == ${clock_style_analog};'
- lambda: return id(weather_available);
- lambda: return id(clock_show_weather).state;
then:
- lvgl.widget.show: analog_weather_icon
- lvgl.widget.show: analog_weather_temp
else:
- lvgl.widget.hide: analog_weather_icon
- lvgl.widget.hide: analog_weather_temp
font:
- file:
type: gfonts
family: Noto Sans
weight: 300
id: font_display
size: 22
glyphsets:
- GF_Latin_Core
- GF_Cyrillic_Core
- file:
type: gfonts
family: Noto Sans
weight: 300
id: font_display_medium
size: 26
glyphsets:
- GF_Latin_Core
- GF_Cyrillic_Core
- file:
type: gfonts
family: Noto Sans
weight: 300
id: font_playlist
size: 18
glyphsets:
- GF_Latin_Core
- GF_Cyrillic_Core
- file:
type: gfonts
family: Figtree
weight: 300
id: font_date
size: 28
glyphsets:
- GF_Latin_Core
- file:
type: gfonts
family: Figtree
weight: 600
id: font_boot
size: 42
glyphsets:
- GF_Latin_Core
- file:
type: gfonts
family: Figtree
weight: 700
id: font_temp_big
size: 78
glyphs: "0123456789+-"
- file:
type: gfonts
family: Figtree
weight: 700
id: font_temp_unit
size: 30
glyphs: "\u00B0CF"
- file: https://raw.githubusercontent.com/rbtdev/spiroplot/master/fonts/DSEG7/Classic-MINI/DSEG7ClassicMini-Bold.ttf
id: font_7seg
size: 50
glyphs: "0123456789:- "
- file: https://raw.githubusercontent.com/rbtdev/spiroplot/master/fonts/DSEG7/Classic-MINI/DSEG7ClassicMini-Bold.ttf
id: font_7seg_small
size: 34
glyphs: "0123456789:- "
- file: https://raw.githubusercontent.com/rbtdev/spiroplot/master/fonts/DSEG7/Classic-MINI/DSEG7ClassicMini-Bold.ttf
id: font_ampm
size: 22
glyphs: "AP"
- file: https://github.com/Templarian/MaterialDesign-Webfont/raw/refs/heads/master/fonts/materialdesignicons-webfont.ttf
id: font_mdi_weather
size: 76
glyphs:
- "\U000F0590"
- "\U000F0592"
- "\U000F0593"
- "\U000F0595"
- "\U000F0596"
- "\U000F0597"
- "\U000F0598"
- "\U000F0599"
- file: https://github.com/Templarian/MaterialDesign-Webfont/raw/refs/heads/master/fonts/materialdesignicons-webfont.ttf
id: font_mdi_weather_small
size: 38
glyphs:
- "\U000F0590"
- "\U000F0592"
- "\U000F0593"
- "\U000F0595"
- "\U000F0596"
- "\U000F0597"
- "\U000F0598"
- "\U000F0599"
- file: https://github.com/Templarian/MaterialDesign-Webfont/raw/refs/heads/master/fonts/materialdesignicons-webfont.ttf
id: font_mdi_controls
size: 44
glyphs:
- "\U000F03E3"
- "\U000F040E"
- "\U000F04AD"
- "\U000F04AE"
- "\U000F075D"
- "\U000F075E"
- "\U000F075F"
- file: https://github.com/Templarian/MaterialDesign-Webfont/raw/refs/heads/master/fonts/materialdesignicons-webfont.ttf
id: font_mdi_rooms
size: 32
glyphs:
- "\U000F0004"
- file:
type: gfonts
family: Silkscreen
weight: 400
id: font_retro_numeral
size: 24
glyphs: "12369"
- file:
type: gfonts
family: Silkscreen
weight: 400
id: font_retro_small
size: 16
glyphs: "0123456789:RETO"
- file:
type: gfonts
family: Silkscreen
weight: 400
id: font_mario_digits
size: 36
glyphs: "0123456789"
script:
- id: led_ring_apply_state
mode: restart
then:
- if:
condition:
switch.is_off: led_ring_enabled
then:
- light.turn_off: rgb_ring
else:
- if:
condition:
media_player.is_playing: external_media_player
then:
- light.turn_on:
id: rgb_ring
brightness: 45%
red: 0%
green: 100%
blue: 0%
else:
- light.turn_on:
id: rgb_ring
brightness: 28%
red: 100%
green: 0%
blue: 0%
- id: apply_accent_color_debounced
mode: restart
then:
- delay: 40ms
- script.execute: apply_accent_color
- id: apply_accent_color
mode: restart
then:
- lambda: |-
const int r = std::max(0, std::min(255, id(accent_red)));
const int g = std::max(0, std::min(255, id(accent_green)));
const int b = std::max(0, std::min(255, id(accent_blue)));
const uint32_t accent_u32 = ((uint32_t) r << 16) | ((uint32_t) g << 8) | (uint32_t) b;
const uint32_t dark_u32 = ((uint32_t) (r / 10) << 16) | ((uint32_t) (g / 10) << 8) | (uint32_t) (b / 10);
const lv_color_t accent = lv_color_hex(accent_u32);
const lv_color_t accent_dark = lv_color_hex(dark_u32);
const int r2 = std::max(0, std::min(255, id(accent2_red)));
const int g2 = std::max(0, std::min(255, id(accent2_green)));
const int b2 = std::max(0, std::min(255, id(accent2_blue)));
const uint32_t accent2_u32 = ((uint32_t) r2 << 16) | ((uint32_t) g2 << 8) | (uint32_t) b2;
const lv_color_t accent2 = lv_color_hex(accent2_u32);
const int r3 = std::max(0, std::min(255, id(accent3_red)));
const int g3 = std::max(0, std::min(255, id(accent3_green)));
const int b3 = std::max(0, std::min(255, id(accent3_blue)));
const uint32_t accent3_u32 = ((uint32_t) r3 << 16) | ((uint32_t) g3 << 8) | (uint32_t) b3;
const lv_color_t accent3 = lv_color_hex(accent3_u32);
const int r4 = std::max(0, std::min(255, id(accent4_red)));
const int g4 = std::max(0, std::min(255, id(accent4_green)));
const int b4 = std::max(0, std::min(255, id(accent4_blue)));
const uint32_t accent4_u32 = ((uint32_t) r4 << 16) | ((uint32_t) g4 << 8) | (uint32_t) b4;
const lv_color_t accent4 = lv_color_hex(accent4_u32);
lv_obj_set_style_text_color(id(clock_label), accent, 0);
lv_obj_set_style_text_color(id(clock_ampm_label), accent, 0);
lv_obj_set_style_text_color(id(weather_big_temp), accent, 0);
lv_obj_set_style_text_color(id(weather_big_temp_unit), accent, 0);
lv_obj_set_style_text_color(id(analog_weather_temp), accent, 0);
lv_obj_set_style_bg_color(id(controls_toggle_button), accent, 0);
lv_obj_set_style_border_color(id(controls_toggle_button), accent, LV_STATE_PRESSED);
lv_obj_set_style_text_color(id(date_label), accent2, 0);
lv_obj_set_style_text_color(id(weather_icon), accent2, 0);
lv_obj_set_style_text_color(id(analog_weather_icon), accent2, 0);
lv_obj_set_style_text_color(id(weather_label), accent3, 0);
lv_obj_set_style_text_color(id(controls_title_label), accent2, 0);
lv_obj_set_style_text_color(id(controls_artist_label), accent3, 0);
lv_obj_set_style_text_color(id(controls_album_label), accent3, 0);
lv_obj_set_style_text_color(id(controls_mute_icon), accent4, 0);
lv_obj_set_style_text_color(id(controls_volume_down_icon), accent4, 0);
lv_obj_set_style_text_color(id(controls_volume_up_icon), accent4, 0);
lv_obj_t *playlist_buttons[] = {
id(playlist_1_button), id(playlist_2_button), id(playlist_3_button),
id(playlist_4_button), id(playlist_5_button), id(playlist_6_button)
};
for (auto *button : playlist_buttons) {
lv_obj_set_style_bg_color(button, accent_dark, 0);
lv_obj_set_style_border_color(button, accent, 0);
lv_obj_set_style_shadow_color(button, accent, 0);
lv_obj_set_style_bg_color(button, accent, LV_STATE_PRESSED);
}
lv_obj_t *playlist_labels[] = {
id(playlist_1_label), id(playlist_2_label), id(playlist_3_label),
id(playlist_4_label), id(playlist_5_label), id(playlist_6_label)
};
for (auto *label : playlist_labels) {
lv_obj_set_style_text_color(label, accent2, 0);
}
lv_obj_set_style_text_color(id(rooms_clock_label), accent3, 0);
lv_obj_set_style_text_color(id(rooms_date_label), accent2, 0);
lv_obj_set_style_text_color(id(pulldown_title_label), accent3, 0);
lv_obj_t *room_boxes[] = {
id(room1_box), id(room2_box), id(room3_box),
id(room4_box), id(room5_box), id(room6_box)
};
for (auto *box : room_boxes) {
lv_obj_set_style_border_color(box, accent, 0);
}
- script.execute: analog_clock_redraw
- script.execute: rooms_grid_apply
- script.execute: refresh_pulldown_states
- id: analog_clock_ensure_created
then:
- lambda: |-
if (id(analog_clock_canvas_widget) != nullptr) return;
const int32_t SIZE = std::min(${screenwidth}, ${screenheight});
lv_obj_t *canvas = lv_canvas_create(id(idle_page)->obj);
lv_draw_buf_t *buf = lv_draw_buf_create(SIZE, SIZE, LV_COLOR_FORMAT_ARGB8888, LV_STRIDE_AUTO);
lv_canvas_set_draw_buf(canvas, buf);
lv_obj_set_style_bg_opa(canvas, LV_OPA_TRANSP, 0);
lv_obj_center(canvas);
lv_obj_clear_flag(canvas, LV_OBJ_FLAG_CLICKABLE);
lv_obj_clear_flag(canvas, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(canvas, LV_OBJ_FLAG_HIDDEN);
id(analog_clock_canvas_widget) = canvas;
- id: apply_clock_style
mode: restart
parameters:
style_mode: int
then:
- lambda: |-
id(clock_style_mode) = style_mode;
- if:
condition:
lambda: 'return style_mode == ${clock_style_analog};'
then:
- script.execute: analog_clock_ensure_created
- lambda: |-
lv_obj_add_flag(id(clock_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(clock_ampm_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(date_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(weather_header), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(weather_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(id(analog_clock_canvas_widget), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(rooms_clock_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(rooms_date_label), LV_OBJ_FLAG_HIDDEN);
for (auto box : {id(room1_box), id(room2_box), id(room3_box), id(room4_box), id(room5_box), id(room6_box)}) {
lv_obj_add_flag(box, LV_OBJ_FLAG_HIDDEN);
}
- script.execute: analog_clock_redraw
- component.update: clock_hms
- component.update: clock_date
- component.update: weather_temp_big
- component.update: weather_line
else:
- if:
condition:
lambda: 'return style_mode == ${clock_style_rooms};'
then:
- lambda: |-
lv_obj_add_flag(id(clock_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(clock_ampm_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(date_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(weather_header), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(weather_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(analog_weather_icon), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(analog_weather_temp), LV_OBJ_FLAG_HIDDEN);
if (id(analog_clock_canvas_widget) != nullptr) {
lv_obj_add_flag(id(analog_clock_canvas_widget), LV_OBJ_FLAG_HIDDEN);
}
lv_obj_clear_flag(id(rooms_clock_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(id(rooms_date_label), LV_OBJ_FLAG_HIDDEN);
for (auto box : {id(room1_box), id(room2_box), id(room3_box), id(room4_box), id(room5_box), id(room6_box)}) {
lv_obj_clear_flag(box, LV_OBJ_FLAG_HIDDEN);
}
- script.execute: rooms_grid_apply
- component.update: clock_hms
- component.update: clock_date
- component.update: weather_temp_big
- component.update: weather_line
else:
- if:
condition:
lambda: 'return style_mode == ${clock_style_retro};'
then:
- script.execute: analog_clock_ensure_created
- lambda: |-
lv_obj_add_flag(id(clock_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(clock_ampm_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(date_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(weather_header), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(weather_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(analog_weather_icon), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(analog_weather_temp), LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(id(analog_clock_canvas_widget), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(rooms_clock_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(rooms_date_label), LV_OBJ_FLAG_HIDDEN);
for (auto box : {id(room1_box), id(room2_box), id(room3_box), id(room4_box), id(room5_box), id(room6_box)}) {
lv_obj_add_flag(box, LV_OBJ_FLAG_HIDDEN);
}
- script.execute: retro_clock_redraw
- component.update: clock_hms
- component.update: clock_date
- component.update: weather_temp_big
- component.update: weather_line
else:
- if:
condition:
lambda: 'return style_mode == ${clock_style_mario};'
then:
- script.execute: analog_clock_ensure_created
- lambda: |-
lv_obj_add_flag(id(clock_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(clock_ampm_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(date_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(weather_header), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(weather_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(analog_weather_icon), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(analog_weather_temp), LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(id(analog_clock_canvas_widget), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(rooms_clock_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(rooms_date_label), LV_OBJ_FLAG_HIDDEN);
for (auto box : {id(room1_box), id(room2_box), id(room3_box), id(room4_box), id(room5_box), id(room6_box)}) {
lv_obj_add_flag(box, LV_OBJ_FLAG_HIDDEN);
}
id(mario_anim_active) = false;
id(mario_y_offset) = 0;
id(hourblock_offset) = 0;
id(hourblock_bouncing) = false;
id(hourblock_going_up) = true;
id(minuteblock_offset) = 0;
id(minuteblock_bouncing) = false;
id(minuteblock_going_up) = true;
- script.execute:
id: mario_draw_scene
full_redraw: true
- component.update: clock_hms
- component.update: clock_date
- component.update: weather_temp_big
- component.update: weather_line
else:
- lambda: |-
lv_obj_clear_flag(id(clock_label), LV_OBJ_FLAG_HIDDEN);
if (id(clock_show_date).state) {
lv_obj_clear_flag(id(date_label), LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(id(date_label), LV_OBJ_FLAG_HIDDEN);
}
if (id(analog_clock_canvas_widget) != nullptr) {
lv_obj_add_flag(id(analog_clock_canvas_widget), LV_OBJ_FLAG_HIDDEN);
}
lv_obj_add_flag(id(analog_weather_icon), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(analog_weather_temp), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(rooms_clock_label), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(rooms_date_label), LV_OBJ_FLAG_HIDDEN);
for (auto box : {id(room1_box), id(room2_box), id(room3_box), id(room4_box), id(room5_box), id(room6_box)}) {
lv_obj_add_flag(box, LV_OBJ_FLAG_HIDDEN);
}
- component.update: clock_hms
- component.update: clock_date
- component.update: weather_temp_big
- component.update: weather_line
- id: rooms_grid_apply
then:
- lambda: |-
auto apply_box = [&](lv_obj_t *box, lv_obj_t *icon, lv_color_t on_color, bool light_on, bool presence_on) {
if (light_on) {
lv_obj_set_style_bg_color(box, on_color, 0);
lv_obj_set_style_bg_opa(box, LV_OPA_70, 0);
} else {
lv_obj_set_style_bg_opa(box, LV_OPA_TRANSP, 0);
}
lv_obj_set_style_text_color(icon, lv_color_hex(0xFFFFFF), 0);
if (presence_on) {
lv_obj_clear_flag(icon, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(icon, LV_OBJ_FLAG_HIDDEN);
}
};
apply_box(id(room1_box), id(room1_icon), lv_color_hex(0x4FC3F7), id(room1_light).state, id(room1_presence).state);
apply_box(id(room2_box), id(room2_icon), lv_color_hex(0xE53935), id(room2_light).state, id(room2_presence).state);
apply_box(id(room3_box), id(room3_icon), lv_color_hex(0xFDD835), id(room3_light).state, id(room3_presence).state);
apply_box(id(room4_box), id(room4_icon), lv_color_hex(0xBA68C8), id(room4_light).state, id(room4_presence).state);
apply_box(id(room5_box), id(room5_icon), lv_color_hex(0x1A237E), id(room5_light).state, id(room5_presence).state);
apply_box(id(room6_box), id(room6_icon), lv_color_hex(0x81C784), id(room6_light).state, id(room6_presence).state);
- id: analog_clock_redraw
mode: restart
then:
- if:
condition:
and:
- lvgl.page.is_showing: idle_page
- lambda: 'return id(clock_style_mode) == ${clock_style_analog};'
- lambda: 'return id(analog_clock_canvas_widget) != nullptr;'
then:
- lambda: |-
lv_obj_t *canvas = id(analog_clock_canvas_widget);
const int32_t SIZE = std::min(${screenwidth}, ${screenheight});
const int32_t CX = SIZE / 2;
const int32_t CY = SIZE / 2;
const int32_t R = SIZE / 2 - 10;
lv_canvas_fill_bg(canvas, lv_color_hex(0x000000), LV_OPA_TRANSP);
lv_layer_t layer;
lv_canvas_init_layer(canvas, &layer);
const uint32_t accent = ((uint32_t) id(accent_red) << 16) | ((uint32_t) id(accent_green) << 8) | (uint32_t) id(accent_blue);
const uint32_t accent2 = ((uint32_t) id(accent2_red) << 16) | ((uint32_t) id(accent2_green) << 8) | (uint32_t) id(accent2_blue);
const uint32_t accent3 = ((uint32_t) id(accent3_red) << 16) | ((uint32_t) id(accent3_green) << 8) | (uint32_t) id(accent3_blue);
lv_color_t fg = lv_color_hex(accent);
lv_color_t fg2 = lv_color_hex(accent2);
lv_color_t fg3 = lv_color_hex(accent3);
const bool show_seconds = id(clock_show_seconds).state;
lv_draw_line_dsc_t line_dsc;
lv_draw_line_dsc_init(&line_dsc);
line_dsc.round_start = true;
line_dsc.round_end = true;
if (id(clock_show_gfx).state) {
for (int i = 0; i < 60; i++) {
float a = (i / 60.0f) * 6.2831853f - 1.5707963f;
bool major = (i % 5 == 0);
int32_t r_out = R;
int32_t r_in = major ? R - 16 : R - 8;
line_dsc.p1.x = CX + (int32_t) (cosf(a) * r_out);
line_dsc.p1.y = CY + (int32_t) (sinf(a) * r_out);
line_dsc.p2.x = CX + (int32_t) (cosf(a) * r_in);
line_dsc.p2.y = CY + (int32_t) (sinf(a) * r_in);
line_dsc.color = major ? fg : fg2;
line_dsc.width = major ? 3 : 1;
lv_draw_line(&layer, &line_dsc);
}
}
if (id(clock_show_date).state) {
std::string date_str = id(clock_date).state;
lv_draw_label_dsc_t label_dsc;
lv_draw_label_dsc_init(&label_dsc);
label_dsc.color = fg2;
label_dsc.font = id(font_date)->get_lv_font();
label_dsc.align = LV_TEXT_ALIGN_CENTER;
label_dsc.text = date_str.c_str();
label_dsc.text_local = true;
const int32_t date_y = CY + (int32_t) (R * 0.55f);
lv_area_t label_area = {CX - R, date_y, CX + R, date_y + 40};
lv_draw_label(&layer, &label_dsc, &label_area);
}
auto t = id(homeassistant_time).now();
int hh = t.is_valid() ? (t.hour % 12) : 0;
int mm = t.is_valid() ? t.minute : 0;
int ss = t.is_valid() ? t.second : 0;
float s_pos = ss / 60.0f;
float m_pos = (mm + s_pos) / 60.0f;
float h_pos = (hh + m_pos) / 12.0f;
auto draw_hand = [&](float pos, int32_t len, int32_t width, lv_color_t color) {
float a = pos * 6.2831853f - 1.5707963f;
line_dsc.p1.x = CX;
line_dsc.p1.y = CY;
line_dsc.p2.x = CX + (int32_t) (cosf(a) * len);
line_dsc.p2.y = CY + (int32_t) (sinf(a) * len);
line_dsc.color = color;
line_dsc.width = width;
lv_draw_line(&layer, &line_dsc);
};
draw_hand(h_pos, (int32_t) (R * 0.5f), 6, fg3);
draw_hand(m_pos, (int32_t) (R * 0.72f), 4, fg3);
if (show_seconds) {
draw_hand(s_pos, (int32_t) (R * 0.85f), 1, fg);
}
lv_draw_rect_dsc_t dot_dsc;
lv_draw_rect_dsc_init(&dot_dsc);
dot_dsc.bg_color = fg;
dot_dsc.bg_opa = LV_OPA_COVER;
dot_dsc.radius = LV_RADIUS_CIRCLE;
lv_area_t dot_area = {CX - 6, CY - 6, CX + 6, CY + 6};
lv_draw_rect(&layer, &dot_dsc, &dot_area);
lv_canvas_finish_layer(canvas, &layer);
lv_obj_invalidate(canvas);
- id: retro_clock_redraw
mode: restart
then:
- if:
condition:
and:
- lvgl.page.is_showing: idle_page
- lambda: 'return id(clock_style_mode) == ${clock_style_retro};'
- lambda: 'return id(analog_clock_canvas_widget) != nullptr;'
then:
- lambda: |-
lv_obj_t *canvas = id(analog_clock_canvas_widget);
const int32_t SIZE = std::min(${screenwidth}, ${screenheight});
const int32_t CX = SIZE / 2;
const int32_t CY = SIZE / 2;
const float scale = SIZE / 240.0f;
auto S = [&](float v) -> int32_t { return (int32_t) roundf(v * scale); };
const lv_color_t c_matte = lv_color_hex(0x080808);
const lv_color_t c_matte_dark = lv_color_hex(0x333333);
const lv_color_t c_charcoal = lv_color_hex(0x181818);
const lv_color_t c_white = lv_color_hex(0xFFFFFF);
const lv_color_t c_soft_white = lv_color_hex(0xD6D3D6);
const lv_color_t c_dim_white = lv_color_hex(0x7B7D7B);
const lv_color_t c_shadow = lv_color_hex(0x000000);
const lv_color_t c_orange = lv_color_hex(0xFFA600);
const lv_color_t c_orange_dark = lv_color_hex(0xB56100);
const lv_color_t c_ring_inner = lv_color_hex(0x101010);
const lv_color_t c_tick_minor = lv_color_hex(0x525152);
lv_draw_buf_t *canvas_buf = lv_canvas_get_draw_buf(canvas);
// The bezel/rings/tick-marks/numerals/caption never change - redrawing
// all ~75 of those shapes every second (on top of the moving hands) was
// cheap enough on the 360x360 reference device but expensive enough on
// this larger canvas to intermittently starve the touchscreen driver's
// own timing and make touch stop registering. Draw the static face once,
// cache a raw snapshot of the canvas buffer, and on every later call just
// memcpy it back (near-instant) before drawing only the moving parts.
if (id(retro_static_buf) == nullptr) {
lv_canvas_fill_bg(canvas, lv_color_hex(0x000000), LV_OPA_COVER);
lv_layer_t static_layer;
lv_canvas_init_layer(canvas, &static_layer);
auto fill_circle_at = [&](int32_t cx, int32_t cy, int32_t r, lv_color_t color) {
lv_draw_rect_dsc_t dsc;
lv_draw_rect_dsc_init(&dsc);
dsc.bg_color = color;
dsc.bg_opa = LV_OPA_COVER;
dsc.border_opa = LV_OPA_TRANSP;
dsc.radius = LV_RADIUS_CIRCLE;
lv_area_t area = {cx - r, cy - r, cx + r, cy + r};
lv_draw_rect(&static_layer, &dsc, &area);
};
auto fill_circle = [&](int32_t r, lv_color_t color) {
fill_circle_at(CX, CY, r, color);
};
auto stroke_circle = [&](int32_t r, lv_color_t color, int32_t width) {
lv_draw_rect_dsc_t dsc;
lv_draw_rect_dsc_init(&dsc);
dsc.bg_opa = LV_OPA_TRANSP;
dsc.border_color = color;
dsc.border_width = width;
dsc.border_opa = LV_OPA_COVER;
dsc.radius = LV_RADIUS_CIRCLE;
lv_area_t area = {CX - r, CY - r, CX + r, CY + r};
lv_draw_rect(&static_layer, &dsc, &area);
};
fill_circle(S(118.0f), c_matte_dark);
fill_circle(S(111.0f), c_matte);
for (int r = 110; r >= 72; r -= 8) {
stroke_circle(S((float) r), (r == 110) ? c_charcoal : c_ring_inner, 1);
}
stroke_circle(S(117.0f), c_dim_white, 1);
stroke_circle(S(116.0f), c_charcoal, 1);
stroke_circle(S(105.0f), c_charcoal, 1);
lv_draw_line_dsc_t static_line_dsc;
lv_draw_line_dsc_init(&static_line_dsc);
static_line_dsc.round_start = true;
static_line_dsc.round_end = true;
for (int i = 0; i < 60; i++) {
float a = (i / 60.0f) * 6.2831853f - 1.5707963f;
bool major = (i % 5 == 0);
int32_t r_out = S(major ? 107.0f : 104.0f);
int32_t r_in = S(major ? 91.0f : 99.0f);
static_line_dsc.p1.x = CX + (int32_t) (cosf(a) * r_out);
static_line_dsc.p1.y = CY + (int32_t) (sinf(a) * r_out);
static_line_dsc.p2.x = CX + (int32_t) (cosf(a) * r_in);
static_line_dsc.p2.y = CY + (int32_t) (sinf(a) * r_in);
static_line_dsc.color = major ? c_soft_white : c_tick_minor;
static_line_dsc.width = major ? 2 : 1;
lv_draw_line(&static_layer, &static_line_dsc);
}
lv_draw_label_dsc_t numeral_dsc;
lv_draw_label_dsc_init(&numeral_dsc);
numeral_dsc.color = c_soft_white;
numeral_dsc.font = id(font_retro_numeral)->get_lv_font();
numeral_dsc.align = LV_TEXT_ALIGN_CENTER;
numeral_dsc.text_local = true;
auto draw_numeral = [&](const char *text, int32_t x, int32_t y) {
numeral_dsc.text = text;
lv_area_t area = {x - S(30.0f), y - S(14.0f), x + S(30.0f), y + S(14.0f)};
lv_draw_label(&static_layer, &numeral_dsc, &area);
};
draw_numeral("12", CX, CY - S(78.0f));
draw_numeral("3", CX + S(78.0f), CY);
draw_numeral("6", CX, CY + S(78.0f));
draw_numeral("9", CX - S(78.0f), CY);
lv_draw_label_dsc_t caption_dsc;
lv_draw_label_dsc_init(&caption_dsc);
caption_dsc.color = c_dim_white;
caption_dsc.font = id(font_retro_small)->get_lv_font();
caption_dsc.align = LV_TEXT_ALIGN_CENTER;
caption_dsc.text = "RETRO";
caption_dsc.text_local = true;
int32_t caption_y = CY - S(34.0f);
lv_area_t caption_area = {CX - S(50.0f), caption_y - S(10.0f), CX + S(50.0f), caption_y + S(10.0f)};
lv_draw_label(&static_layer, &caption_dsc, &caption_area);
lv_canvas_finish_layer(canvas, &static_layer);
uint8_t *snapshot = (uint8_t *) malloc(canvas_buf->data_size);
if (snapshot != nullptr) {
memcpy(snapshot, canvas_buf->data, canvas_buf->data_size);
id(retro_static_buf) = snapshot;
}
} else {
memcpy(canvas_buf->data, id(retro_static_buf), canvas_buf->data_size);
}
lv_layer_t layer;
lv_canvas_init_layer(canvas, &layer);
lv_draw_line_dsc_t line_dsc;
lv_draw_line_dsc_init(&line_dsc);
line_dsc.round_start = true;
line_dsc.round_end = true;
auto t = id(homeassistant_time).now();
int hh24 = t.is_valid() ? t.hour : 0;
int hh = hh24 % 12;
int mm = t.is_valid() ? t.minute : 0;
int ss = t.is_valid() ? t.second : 0;
float s_pos = ss / 60.0f;
float m_pos = (mm + s_pos) / 60.0f;
float h_pos = (hh + m_pos) / 12.0f;
auto draw_hand = [&](float pos, int32_t len, int32_t width, lv_color_t color, bool shadow) {
float a = pos * 6.2831853f - 1.5707963f;
int32_t ex = CX + (int32_t) (cosf(a) * len);
int32_t ey = CY + (int32_t) (sinf(a) * len);
if (shadow) {
line_dsc.p1.x = CX + S(3.0f);
line_dsc.p1.y = CY + S(3.0f);
line_dsc.p2.x = ex + S(3.0f);
line_dsc.p2.y = ey + S(3.0f);
line_dsc.color = c_shadow;
line_dsc.width = width;
lv_draw_line(&layer, &line_dsc);
}
line_dsc.p1.x = CX;
line_dsc.p1.y = CY;
line_dsc.p2.x = ex;
line_dsc.p2.y = ey;
line_dsc.color = color;
line_dsc.width = width;
lv_draw_line(&layer, &line_dsc);
};
draw_hand(h_pos, S(53.0f), S(5.0f), c_white, true);
draw_hand(m_pos, S(76.0f), S(3.0f), c_soft_white, true);
if (id(clock_show_seconds).state) {
draw_hand(s_pos, S(92.0f), S(1.0f), c_orange, true);
float tail_pos = s_pos + 0.5f;
if (tail_pos >= 1.0f) tail_pos -= 1.0f;
draw_hand(tail_pos, S(18.0f), 1, c_orange_dark, false);
}
auto fill_circle_dyn = [&](int32_t cx, int32_t cy, int32_t r, lv_color_t color) {
lv_draw_rect_dsc_t dsc;
lv_draw_rect_dsc_init(&dsc);
dsc.bg_color = color;
dsc.bg_opa = LV_OPA_COVER;
dsc.border_opa = LV_OPA_TRANSP;
dsc.radius = LV_RADIUS_CIRCLE;
lv_area_t area = {cx - r, cy - r, cx + r, cy + r};
lv_draw_rect(&layer, &dsc, &area);
};
auto stroke_circle_dyn = [&](int32_t r, lv_color_t color, int32_t width) {
lv_draw_rect_dsc_t dsc;
lv_draw_rect_dsc_init(&dsc);
dsc.bg_opa = LV_OPA_TRANSP;
dsc.border_color = color;
dsc.border_width = width;
dsc.border_opa = LV_OPA_COVER;
dsc.radius = LV_RADIUS_CIRCLE;
lv_area_t area = {CX - r, CY - r, CX + r, CY + r};
lv_draw_rect(&layer, &dsc, &area);
};
fill_circle_dyn(CX + S(3.0f), CY + S(3.0f), S(7.0f), c_shadow);
fill_circle_dyn(CX, CY, S(7.0f), c_matte_dark);
stroke_circle_dyn(S(7.0f), c_soft_white, 1);
fill_circle_dyn(CX, CY, S(3.0f), c_orange);
lv_draw_label_dsc_t digital_dsc;
lv_draw_label_dsc_init(&digital_dsc);
digital_dsc.color = c_dim_white;
digital_dsc.font = id(font_retro_small)->get_lv_font();
digital_dsc.align = LV_TEXT_ALIGN_CENTER;
digital_dsc.text_local = true;
int display_hour = hh24;
if (id(clock_12h_format).state) {
display_hour = hh24 % 12;
if (display_hour == 0) display_hour = 12;
}
char time_buf[8];
snprintf(time_buf, sizeof(time_buf), "%02d:%02d", display_hour, mm);
digital_dsc.text = time_buf;
int32_t digital_y = CY + S(46.0f);
lv_area_t digital_area = {CX - S(50.0f), digital_y - S(10.0f), CX + S(50.0f), digital_y + S(10.0f)};
lv_draw_label(&layer, &digital_dsc, &digital_area);
lv_canvas_finish_layer(canvas, &layer);
lv_obj_invalidate(canvas);
- id: mario_draw_scene
mode: restart
parameters:
full_redraw: bool
then:
- if:
condition:
and:
- lambda: 'return id(clock_style_mode) == ${clock_style_mario};'
- lambda: 'return id(analog_clock_canvas_widget) != nullptr;'
- or:
# A full_redraw is a deliberate one-time "prepare this style now" paint (called
# when entering Mario style) and must happen even if idle_page isn't the currently
# shown page yet (e.g. boot_sound_page is still showing during startup) - otherwise
# the canvas is left with whatever garbage was in its freshly-allocated buffer until
# something else happens to trigger a redraw. Animation-frame calls (full_redraw:
# false) keep the visibility check, since there's no point animating unseen content.
- lambda: 'return full_redraw;'
- lvgl.page.is_showing: idle_page
then:
- lambda: |-
lv_obj_t *canvas = id(analog_clock_canvas_widget);
lv_display_t *mario_disp = lv_obj_get_display(canvas);
lv_display_enable_invalidation(mario_disp, false);
const int32_t SIZE = std::min(${screenwidth}, ${screenheight});
// Fills the entire canvas (not just an inset square) - on a round display this means
// decorative corners (hill tip, cloud, far ground-row ends) may clip against the bezel;
// the core gameplay area (Mario, both blocks) stays well within the visible circle.
const float SCALE = (float) SIZE / 64.0f;
const int32_t OFFSET_X = (SIZE - (int32_t) roundf(64.0f * SCALE)) / 2;
const int32_t OFFSET_Y = (SIZE - (int32_t) roundf(64.0f * SCALE)) / 2;
static const uint16_t MASK_COLOR = 0x000E;
static const uint16_t BLOCK_SPRITE[361] = {
0x000E, 0x9A40, 0x9A40, 0x9A40, 0x9A40, 0x9A40, 0x9A40, 0x9A40, 0x9A40, 0x9A40, 0x9A40, 0x9A40, 0x9A40, 0x9A40, 0x9A40, 0x9A40,
0x9A40, 0x9A40, 0x000E, 0x9A40, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4,
0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0x0000, 0x9A40, 0xE4E4, 0x0000, 0x0000, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4,
0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0x0000, 0x0000, 0xE4E4, 0x0000, 0x9A40, 0xE4E4, 0x0000, 0x0000, 0xE4E4, 0xE4E4, 0xE4E4,
0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0x0000, 0x0000, 0xE4E4, 0x0000, 0x9A40, 0xE4E4, 0xE4E4, 0xE4E4,
0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0x0000, 0x9A40,
0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4,
0xE4E4, 0x0000, 0x9A40, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4,
0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0x0000, 0x9A40, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4,
0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0x0000, 0x9A40, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4,
0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0x0000, 0x9A40, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4,
0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0x0000, 0x9A40, 0xE4E4,
0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4,
0x0000, 0x9A40, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4,
0xE4E4, 0xE4E4, 0xE4E4, 0x0000, 0x9A40, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4,
0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0x0000, 0x9A40, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4,
0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0x0000, 0x9A40, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4,
0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0x0000, 0x9A40, 0xE4E4, 0x0000,
0x0000, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0x0000, 0x0000, 0xE4E4, 0x0000,
0x9A40, 0xE4E4, 0x0000, 0x0000, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0x0000,
0x0000, 0xE4E4, 0x0000, 0x9A40, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4,
0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0xE4E4, 0x0000, 0x000E, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
};
static const uint16_t BUSH_SPRITE[189] = {
0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x0000, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E,
0x000E, 0x0000, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x0000, 0xBFE3, 0xBFE3, 0x0000,
0x000E, 0x0000, 0x000E, 0x000E, 0x000E, 0x0000, 0xBFE3, 0xBFE3, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E,
0x0000, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0x0000, 0xBFE3, 0x0000, 0x000E, 0x0000, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0x0000, 0x000E,
0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x0000, 0xBFE3, 0xBFE3, 0xBFE3, 0x0560, 0xBFE3, 0xBFE3, 0x0000, 0x000E, 0x0000, 0xBFE3,
0xBFE3, 0xBFE3, 0x0560, 0xBFE3, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x0000, 0xBFE3, 0x0560, 0x0560, 0xBFE3, 0xBFE3, 0x0560,
0xBFE3, 0xBFE3, 0x0000, 0xBFE3, 0x0560, 0x0560, 0xBFE3, 0xBFE3, 0x0560, 0x000E, 0x000E, 0x000E, 0x0000, 0x0000, 0xBFE3, 0x0560,
0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0x0560, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0x000E, 0x000E,
0x0000, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3,
0xBFE3, 0xBFE3, 0xBFE3, 0x000E, 0x000E, 0x0000, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3,
0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0x000E, 0x0000, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3,
0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3, 0xBFE3
};
static const uint16_t CLOUD1_SPRITE[156] = {
0x000E, 0x0000, 0x0000, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x0000, 0xFFFF, 0xFFFF,
0xFFFF, 0x0000, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
0x0000, 0x000E, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0xFFFF, 0x3DFF, 0xFFFF, 0xFFFF, 0x3DFF, 0xFFFF, 0xFFFF, 0x0000, 0xFFFF,
0x0000, 0x000E, 0x000E, 0x000E, 0x3DFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x000E,
0x000E, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x000E, 0xFFFF, 0xFFFF,
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x000E, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x000E, 0x000E, 0xFFFF, 0xFFFF, 0xFFFF, 0x3DFF, 0x3DFF, 0xFFFF, 0x3DFF, 0xFFFF,
0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x000E, 0x3DFF, 0x3DFF, 0x3DFF, 0xFFFF, 0xFFFF, 0x3DFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000,
0x000E, 0x000E, 0xFFFF, 0xFFFF, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0x0000,
0x0000, 0x000E, 0x0000, 0x0000, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E
};
static const uint16_t CLOUD2_SPRITE[156] = {
0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x0000, 0x0000, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E,
0x000E, 0x000E, 0x000E, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E,
0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x0000, 0xFFFF, 0x3DFF, 0xFFFF,
0xFFFF, 0x3DFF, 0xFFFF, 0xFFFF, 0x000E, 0x000E, 0x000E, 0x0000, 0x0000, 0xFFFF, 0x3DFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
0xFFFF, 0x000E, 0x000E, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x000E, 0x0000,
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x000E, 0xFFFF, 0xFFFF, 0xFFFF, 0x3DFF,
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x000E, 0x000E, 0x0000, 0xFFFF, 0xFFFF, 0x3DFF, 0xFFFF, 0xFFFF,
0xFFFF, 0x3DFF, 0x3DFF, 0xFFFF, 0x3DFF, 0x000E, 0x000E, 0x000E, 0x0000, 0xFFFF, 0xFFFF, 0x3DFF, 0x3DFF, 0x3DFF, 0xFFFF, 0xFFFF,
0x3DFF, 0xFFFF, 0x000E, 0x000E, 0x000E, 0x000E, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x000E,
0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x0000, 0x0000, 0x000E, 0x0000, 0x0000, 0x0000, 0x000E
};
static const uint16_t GROUND_SPRITE[64] = {
0xE2C2, 0xF6B6, 0xF6B6, 0xF6B6, 0x0000, 0xE2C2, 0xF6B6, 0xE2C2, 0xF6B6, 0xE2C2, 0xE2C2, 0xE2C2, 0x0000, 0xF6B6, 0xE2C2, 0x0000,
0xF6B6, 0xE2C2, 0xE2C2, 0xE2C2, 0x0000, 0xE2C2, 0x0000, 0xE2C2, 0x0000, 0xE2C2, 0xE2C2, 0xE2C2, 0x0000, 0xF6B6, 0xF6B6, 0x0000,
0xF6B6, 0x0000, 0x0000, 0xE2C2, 0x0000, 0xF6B6, 0xE2C2, 0x0000, 0xF6B6, 0xF6B6, 0xF6B6, 0x0000, 0xF6B6, 0xE2C2, 0xE2C2, 0x0000,
0xF6B6, 0xE2C2, 0xE2C2, 0xF6B6, 0xE2C2, 0xE2C2, 0xE2C2, 0x0000, 0xE2C2, 0x0000, 0x0000, 0xF6B6, 0x0000, 0x0000, 0x0000, 0xE2C2
};
static const uint16_t HILL_SPRITE[440] = {
0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E,
0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E,
0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E,
0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E,
0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E,
0x0000, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E,
0x000E, 0x000E, 0x000E, 0x000E, 0x0560, 0x0560, 0x0000, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E,
0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x0560, 0x0560, 0x0560, 0x0560, 0x0000, 0x000E, 0x000E, 0x000E,
0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x0560, 0x0560, 0x0000, 0x0560,
0x0560, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E,
0x0560, 0x0560, 0x0000, 0x0560, 0x0560, 0x0560, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E,
0x000E, 0x000E, 0x000E, 0x000E, 0x0000, 0x0560, 0x0000, 0x0560, 0x0560, 0x0560, 0x0560, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E,
0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x0000, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560,
0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x0560, 0x0560, 0x0560, 0x0560,
0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E,
0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E,
0x000E, 0x000E, 0x000E, 0x000E, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0000,
0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560,
0x0560, 0x0560, 0x0560, 0x0560, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x0560, 0x0560, 0x0560, 0x0560,
0x0560, 0x0560, 0x0000, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E,
0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0000, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0000, 0x000E,
0x000E, 0x000E, 0x000E, 0x000E, 0x0560, 0x0560, 0x0560, 0x0560, 0x0000, 0x0560, 0x0000, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560,
0x0560, 0x0560, 0x0560, 0x0000, 0x000E, 0x000E, 0x000E, 0x000E, 0x0560, 0x0560, 0x0560, 0x0560, 0x0000, 0x0560, 0x0560, 0x0560,
0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0000, 0x000E, 0x000E, 0x000E, 0x0560, 0x0560, 0x0560, 0x0560,
0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0000, 0x000E, 0x000E,
0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560,
0x0560, 0x0560, 0x0000, 0x000E, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560,
0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560, 0x0560
};
static const uint16_t MARIO_IDLE_SPRITE[208] = {
0x000E, 0x000E, 0x000E, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0xF801,
0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0x000E, 0x000E, 0x000E, 0x0000, 0x0000, 0x0000, 0xfd28,
0xfd28, 0x0000, 0xfd28, 0xfd28, 0x000E, 0x000E, 0x000E, 0x000E, 0x0000, 0xfd28, 0x0000, 0xfd28, 0xfd28, 0xfd28, 0x0000, 0xfd28,
0xfd28, 0xfd28, 0xfd28, 0x000E, 0x000E, 0x0000, 0xfd28, 0x0000, 0x0000, 0xfd28, 0xfd28, 0xfd28, 0x0000, 0xfd28, 0xfd28, 0xfd28,
0xfd28, 0x000E, 0x0000, 0x0000, 0xfd28, 0xfd28, 0xfd28, 0xfd28, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x000E, 0x000E, 0x000E,
0x000E, 0xfd28, 0xfd28, 0xfd28, 0xfd28, 0xfd28, 0xfd28, 0xfd28, 0xfd28, 0x000E, 0x000E, 0x000E, 0x000E, 0x7BCF, 0x7BCF, 0xF801,
0x7BCF, 0x7BCF, 0x7BCF, 0x7BCF, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x7BCF, 0x7BCF, 0x7BCF, 0xF801, 0x7BCF, 0x7BCF, 0xF801,
0x7BCF, 0x7BCF, 0x7BCF, 0x7BCF, 0x000E, 0x7BCF, 0x7BCF, 0x7BCF, 0x7BCF, 0xF801, 0xF801, 0xF801, 0xF801, 0x7BCF, 0x7BCF, 0x7BCF,
0x7BCF, 0x7BCF, 0xfd28, 0xfd28, 0x7BCF, 0xF801, 0xfd28, 0xF801, 0xF801, 0xfd28, 0xF801, 0x7BCF, 0xfd28, 0xfd28, 0xfd28, 0xfd28,
0xfd28, 0xfd28, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xfd28, 0xfd28, 0xfd28, 0xfd28, 0xfd28, 0xfd28, 0xF801, 0xF801,
0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xfd28, 0xfd28, 0xfd28, 0x000E, 0x000E, 0xF801, 0xF801, 0xF801, 0xF801, 0x000E,
0xF801, 0xF801, 0xF801, 0xF801, 0x000E, 0x000E, 0x000E, 0xC300, 0xC300, 0xC300, 0xC300, 0x000E, 0x000E, 0x000E, 0xC300, 0xC300,
0xC300, 0xC300, 0x000E, 0xC300, 0xC300, 0xC300, 0xC300, 0xC300, 0x000E, 0x000E, 0x000E, 0xC300, 0xC300, 0xC300, 0xC300, 0xC300
};
static const uint16_t MARIO_JUMP_SPRITE[272] = {
0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0xfd28, 0xfd28, 0xfd28,
0xfd28, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0x000E, 0xfd28, 0xfd28,
0xfd28, 0xfd28, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801,
0xfd28, 0xfd28, 0xfd28, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x0000, 0x0000, 0x0000, 0xfd28, 0xfd28, 0x0000, 0xfd28, 0xfd28,
0x7BCF, 0x7BCF, 0x7BCF, 0x7BCF, 0x000E, 0x000E, 0x000E, 0x000E, 0x0000, 0xfd28, 0x0000, 0xfd28, 0xfd28, 0xfd28, 0x0000, 0xfd28,
0xfd28, 0x7BCF, 0x7BCF, 0x7BCF, 0x7BCF, 0x000E, 0x000E, 0x000E, 0x000E, 0x0000, 0xfd28, 0x0000, 0x0000, 0xfd28, 0xfd28, 0xfd28,
0x0000, 0xfd28, 0xfd28, 0xfd28, 0x7BCF, 0x7BCF, 0x000E, 0x000E, 0x000E, 0x000E, 0x0000, 0x0000, 0xfd28, 0xfd28, 0xfd28, 0xfd28,
0x0000, 0x0000, 0x0000, 0x0000, 0x7BCF, 0x7BCF, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0xfd28, 0xfd28, 0xfd28,
0xfd28, 0xfd28, 0xfd28, 0xfd28, 0x7BCF, 0x7BCF, 0x000E, 0x000E, 0x000E, 0x000E, 0x7BCF, 0x7BCF, 0x7BCF, 0x7BCF, 0x7BCF, 0xF801,
0x7BCF, 0x7BCF, 0x7BCF, 0xF801, 0x7BCF, 0x7BCF, 0x000E, 0x000E, 0x000E, 0x000E, 0x7BCF, 0x7BCF, 0x7BCF, 0x7BCF, 0x7BCF, 0x7BCF,
0x7BCF, 0xF801, 0x7BCF, 0x7BCF, 0x7BCF, 0xF801, 0xF801, 0x000E, 0xC300, 0xC300, 0xfd28, 0xfd28, 0x7BCF, 0x7BCF, 0x7BCF, 0x7BCF,
0x7BCF, 0x7BCF, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0x000E, 0xC300, 0xC300, 0xfd28, 0xfd28, 0xfd28, 0xfd28, 0xF801,
0xF801, 0x7BCF, 0xF801, 0xF801, 0xfd28, 0xF801, 0xF801, 0xfd28, 0xF801, 0xC300, 0xC300, 0xC300, 0x000E, 0xfd28, 0xfd28, 0xC300,
0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xC300, 0xC300, 0xC300, 0x000E, 0x000E, 0xC300,
0xC300, 0xC300, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xC300, 0xC300, 0xC300, 0x000E, 0xC300,
0xC300, 0xC300, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E,
0xC300, 0xC300, 0x000E, 0xF801, 0xF801, 0xF801, 0xF801, 0xF801, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E, 0x000E
};
auto rgb565_to_hex = [](uint16_t v) -> uint32_t {
uint8_t r5 = (v >> 11) & 0x1F;
uint8_t g6 = (v >> 5) & 0x3F;
uint8_t b5 = v & 0x1F;
uint8_t r8 = (r5 << 3) | (r5 >> 2);
uint8_t g8 = (g6 << 2) | (g6 >> 4);
uint8_t b8 = (b5 << 3) | (b5 >> 2);
return ((uint32_t) r8 << 16) | ((uint32_t) g8 << 8) | b8;
};
// SCALE is fractional now (filling the full canvas, not a clean integer multiple of 64),
// so each source pixel maps to a variable-sized block: its screen-space bounds are found
// by rounding both edges independently, which tiles without gaps or overlaps.
auto blit = [&](const uint16_t *data, int sw, int sh, int32_t gx, int32_t gy) {
for (int sy = 0; sy < sh; sy++) {
int32_t by0 = OFFSET_Y + (int32_t) roundf((gy + sy) * SCALE);
int32_t by1 = OFFSET_Y + (int32_t) roundf((gy + sy + 1) * SCALE);
for (int sx = 0; sx < sw; sx++) {
uint16_t px = data[sy * sw + sx];
if (px == MASK_COLOR) continue;
lv_color_t color = lv_color_hex(rgb565_to_hex(px));
int32_t bx0 = OFFSET_X + (int32_t) roundf((gx + sx) * SCALE);
int32_t bx1 = OFFSET_X + (int32_t) roundf((gx + sx + 1) * SCALE);
for (int32_t y = by0; y < by1; y++) {
for (int32_t x = bx0; x < bx1; x++) {
lv_canvas_set_px(canvas, x, y, color, LV_OPA_COVER);
}
}
}
}
};
if (full_redraw) {
// Sky fill (whole canvas, so it extends to fill the round bezel)
lv_canvas_fill_bg(canvas, lv_color_hex(rgb565_to_hex(MASK_COLOR)), LV_OPA_COVER);
// Background scenery (static, matches the original's one-time setup() draw calls) -
// only needs to be painted once per style-entry, not every animation frame.
blit(HILL_SPRITE, 20, 22, 0, 34);
blit(CLOUD1_SPRITE, 13, 12, 0, 21);
blit(CLOUD2_SPRITE, 13, 12, 51, 7);
blit(BUSH_SPRITE, 21, 9, 43, 47);
for (int i = 0; i < 8; i++) {
blit(GROUND_SPRITE, 8, 8, i * 8, 56);
}
} else {
// Animation-frame fast path: only erase the small regions Mario/the blocks can occupy
// (their entire travel range sits over plain sky in the original layout, so a flat
// sky-colored rect fill is a safe, much cheaper stand-in for a full scene redraw).
// This uses its own layer that's finished (flushed to the canvas buffer) immediately -
// lv_draw_rect only queues a draw task, it doesn't render until lv_canvas_finish_layer
// runs, so sharing one layer with the sprite blits below (which write immediately via
// lv_canvas_set_px) would let the queued erase flush AFTER the blits and wipe them out.
lv_layer_t erase_layer;
lv_canvas_init_layer(canvas, &erase_layer);
auto erase_rect = [&](int32_t gx, int32_t gy, int32_t gw, int32_t gh) {
lv_draw_rect_dsc_t erase_dsc;
lv_draw_rect_dsc_init(&erase_dsc);
erase_dsc.bg_color = lv_color_hex(rgb565_to_hex(MASK_COLOR));
erase_dsc.bg_opa = LV_OPA_COVER;
erase_dsc.border_opa = LV_OPA_TRANSP;
int32_t bx0 = OFFSET_X + (int32_t) roundf(gx * SCALE);
int32_t bx1 = OFFSET_X + (int32_t) roundf((gx + gw) * SCALE);
int32_t by0 = OFFSET_Y + (int32_t) roundf(gy * SCALE);
int32_t by1 = OFFSET_Y + (int32_t) roundf((gy + gh) * SCALE);
lv_area_t area = {bx0, by0, bx1 - 1, by1 - 1};
lv_draw_rect(&erase_layer, &erase_dsc, &area);
};
erase_rect(23, 25, 17, 31);
erase_rect(13, 7, 19, 23);
erase_rect(32, 7, 19, 23);
lv_canvas_finish_layer(canvas, &erase_layer);
}
// Blocks (hour/minute), each with its own current bounce offset
blit(BLOCK_SPRITE, 19, 19, 13, 11 + id(hourblock_offset));
blit(BLOCK_SPRITE, 19, 19, 32, 11 + id(minuteblock_offset));
// Mario, idle or jumping sprite depending on current animation state
if (id(mario_anim_active)) {
blit(MARIO_JUMP_SPRITE, 17, 16, 23, 40 + id(mario_y_offset));
} else {
blit(MARIO_IDLE_SPRITE, 13, 16, 23, 40);
}
// Hour/minute digits, drawn on top of each block (matches the source's black text over the block sprite).
// The source displays 24-hour, non-zero-padded hour (e.g. "22:39"), zero-padded minute.
// The displayed value is only ever refreshed at the moment a block is actually hit (see
// mario_jump_animate's collision checks) or when first entering the style - not read live
// every frame - so the digits change exactly when the bounce happens, not the instant the
// jump starts.
if (full_redraw) {
auto boot_t = id(homeassistant_time).now();
if (boot_t.is_valid()) {
id(mario_displayed_hour) = boot_t.hour;
id(mario_displayed_minute) = boot_t.minute;
}
}
int hh24 = id(mario_displayed_hour);
int mm = id(mario_displayed_minute);
int display_hour = hh24;
if (id(clock_12h_format).state) {
display_hour = hh24 % 12;
if (display_hour == 0) display_hour = 12;
}
char hbuf[4];
char mbuf[4];
snprintf(hbuf, sizeof(hbuf), "%d", display_hour);
snprintf(mbuf, sizeof(mbuf), "%02d", mm);
lv_layer_t layer;
lv_canvas_init_layer(canvas, &layer);
lv_draw_label_dsc_t digit_dsc;
lv_draw_label_dsc_init(&digit_dsc);
digit_dsc.color = lv_color_hex(0x000000);
digit_dsc.font = id(font_mario_digits)->get_lv_font();
digit_dsc.align = LV_TEXT_ALIGN_CENTER;
digit_dsc.text_local = true;
// lv_draw_label only centers horizontally (align: LEFT/RIGHT/CENTER) - it top-aligns
// vertically within whatever area is given, so the area height must tightly match the
// font's actual rendered height or the text lands above center instead of on it.
const int32_t font_h = id(font_mario_digits)->get_height();
const int32_t label_half_w = (int32_t) roundf(SCALE * 10.0f);
digit_dsc.text = hbuf;
int32_t hour_cx = OFFSET_X + (int32_t) roundf((13 + 9) * SCALE);
int32_t hour_cy = OFFSET_Y + (int32_t) roundf((11 + id(hourblock_offset) + 9) * SCALE);
lv_area_t hour_area = {hour_cx - label_half_w, hour_cy - font_h / 2, hour_cx + label_half_w, hour_cy - font_h / 2 + font_h};
lv_draw_label(&layer, &digit_dsc, &hour_area);
digit_dsc.text = mbuf;
int32_t minute_cx = OFFSET_X + (int32_t) roundf((32 + 9) * SCALE);
int32_t minute_cy = OFFSET_Y + (int32_t) roundf((11 + id(minuteblock_offset) + 9) * SCALE);
lv_area_t minute_area = {minute_cx - label_half_w, minute_cy - font_h / 2, minute_cx + label_half_w, minute_cy - font_h / 2 + font_h};
lv_draw_label(&layer, &digit_dsc, &minute_area);
lv_canvas_finish_layer(canvas, &layer);
lv_display_enable_invalidation(mario_disp, true);
lv_obj_invalidate(canvas);
- id: mario_clock_tick
mode: restart
then:
- if:
condition:
and:
- lvgl.page.is_showing: idle_page
- lambda: 'return id(clock_style_mode) == ${clock_style_mario};'
- lambda: 'return !id(mario_anim_active);'
- lambda: |-
auto t = id(homeassistant_time).now();
return t.is_valid() && t.second == 0 && t.minute != id(mario_last_minute);
then:
- script.execute: mario_jump_animate
- id: mario_jump_animate
mode: restart
then:
- lambda: |-
id(mario_anim_active) = true;
auto t = id(homeassistant_time).now();
if (t.is_valid()) id(mario_last_minute) = t.minute;
id(mario_y_offset) = 0;
id(mario_direction_up) = true;
id(hourblock_offset) = 0;
id(hourblock_bouncing) = false;
id(hourblock_going_up) = true;
id(minuteblock_offset) = 0;
id(minuteblock_bouncing) = false;
id(minuteblock_going_up) = true;
- while:
condition:
lambda: 'return id(mario_anim_active);'
then:
- lambda: |-
const int MARIO_PACE = 5;
const int MARIO_JUMP_HEIGHT = 14;
const int MOVE_PACE = 2;
const int MAX_MOVE_HEIGHT = 4;
if (id(mario_direction_up)) {
id(mario_y_offset) -= MARIO_PACE;
if (-id(mario_y_offset) >= MARIO_JUMP_HEIGHT) {
id(mario_direction_up) = false;
}
} else {
id(mario_y_offset) += MARIO_PACE;
}
const int mario_x = 23, mario_w = 17, mario_h = 16;
const int mario_y = 40 + id(mario_y_offset);
auto overlaps = [&](int bx, int by, int bw, int bh) {
return mario_x < bx + bw && mario_x + mario_w > bx &&
mario_y < by + bh && mario_y + mario_h > by;
};
if (!id(hourblock_bouncing) && overlaps(13, 11 + id(hourblock_offset), 19, 19)) {
id(hourblock_bouncing) = true;
id(hourblock_going_up) = true;
id(mario_direction_up) = false;
auto hit_t = id(homeassistant_time).now();
if (hit_t.is_valid()) {
id(mario_displayed_hour) = hit_t.hour;
id(mario_displayed_minute) = hit_t.minute;
}
}
if (id(hourblock_bouncing)) {
if (id(hourblock_going_up)) {
id(hourblock_offset) -= MOVE_PACE;
if (id(hourblock_offset) <= -MAX_MOVE_HEIGHT) {
id(hourblock_offset) = -MAX_MOVE_HEIGHT;
id(hourblock_going_up) = false;
}
} else {
id(hourblock_offset) += MOVE_PACE;
if (id(hourblock_offset) >= 0) {
id(hourblock_offset) = 0;
id(hourblock_bouncing) = false;
}
}
}
if (!id(minuteblock_bouncing) && overlaps(32, 11 + id(minuteblock_offset), 19, 19)) {
id(minuteblock_bouncing) = true;
id(minuteblock_going_up) = true;
id(mario_direction_up) = false;
auto hit_t2 = id(homeassistant_time).now();
if (hit_t2.is_valid()) {
id(mario_displayed_hour) = hit_t2.hour;
id(mario_displayed_minute) = hit_t2.minute;
}
}
if (id(minuteblock_bouncing)) {
if (id(minuteblock_going_up)) {
id(minuteblock_offset) -= MOVE_PACE;
if (id(minuteblock_offset) <= -MAX_MOVE_HEIGHT) {
id(minuteblock_offset) = -MAX_MOVE_HEIGHT;
id(minuteblock_going_up) = false;
}
} else {
id(minuteblock_offset) += MOVE_PACE;
if (id(minuteblock_offset) >= 0) {
id(minuteblock_offset) = 0;
id(minuteblock_bouncing) = false;
}
}
}
if (mario_y + mario_h >= 56) {
id(mario_y_offset) = 0;
id(mario_anim_active) = false;
}
- script.execute:
id: mario_draw_scene
full_redraw: false
- delay: 25ms
- script.execute:
id: mario_draw_scene
full_redraw: false
- id: set_ui_phase
mode: restart
parameters:
phase: int
then:
- script.stop: player_controls_timeout
- script.stop: pulldown_timeout
- lambda: |-
id(ui_phase) = phase;
- script.execute: standby_screen_activity
- if:
condition:
lambda: 'return phase == ${ui_phase_standby};'
then:
- lvgl.page.show: idle_page
- component.update: weather_line
- if:
condition:
lambda: 'return phase == ${ui_phase_player_controls};'
then:
- lvgl.page.show: player_controls_page
- script.execute: player_controls_timeout
- if:
condition:
lambda: 'return phase == ${ui_phase_now_playing};'
then:
- lvgl.page.show: sendspin_page
- if:
condition:
lambda: 'return phase == ${ui_phase_playlist_menu};'
then:
- lvgl.page.show: playlist_menu_page
- script.execute: player_controls_timeout
- if:
condition:
lambda: 'return phase == ${ui_phase_pulldown};'
then:
- lvgl.page.show: pulldown_page
- script.execute: refresh_pulldown_states
- script.execute: pulldown_timeout
- id: player_controls_timeout
mode: restart
then:
- delay: !lambda |-
return (uint32_t) (id(player_controls_timeout_seconds).state * 1000.0f);
- if:
condition:
media_player.is_idle: external_media_player
then:
- script.execute:
id: set_ui_phase
phase: ${ui_phase_standby}
else:
- script.execute:
id: set_ui_phase
phase: ${ui_phase_now_playing}
- id: open_playlist_menu
then:
- script.stop: player_controls_timeout
- lambda: |-
id(ui_phase) = ${ui_phase_playlist_menu};
- lvgl.page.show:
id: playlist_menu_page
animation: MOVE_BOTTOM
time: 160ms
- script.execute: player_controls_timeout
- id: close_playlist_menu
then:
- script.stop: player_controls_timeout
- lambda: |-
id(ui_phase) = ${ui_phase_player_controls};
- lvgl.page.show:
id: player_controls_page
animation: MOVE_TOP
time: 160ms
- script.execute: player_controls_timeout
- id: refresh_playlist_labels
mode: restart
then:
- lvgl.label.update:
id: playlist_1_label
text: !lambda return id(playlist_1_name).state;
- lvgl.label.update:
id: playlist_2_label
text: !lambda return id(playlist_2_name).state;
- lvgl.label.update:
id: playlist_3_label
text: !lambda return id(playlist_3_name).state;
- lvgl.label.update:
id: playlist_4_label
text: !lambda return id(playlist_4_name).state;
- lvgl.label.update:
id: playlist_5_label
text: !lambda return id(playlist_5_name).state;
- lvgl.label.update:
id: playlist_6_label
text: !lambda return id(playlist_6_name).state;
- id: refresh_pulldown_labels
mode: restart
then:
- lvgl.label.update:
id: pulldown1_button_label
text: !lambda return id(pulldown1_label).state;
- lvgl.label.update:
id: pulldown2_button_label
text: !lambda return id(pulldown2_label).state;
- lvgl.label.update:
id: pulldown3_button_label
text: !lambda return id(pulldown3_label).state;
- lvgl.label.update:
id: pulldown4_button_label
text: !lambda return id(pulldown4_label).state;
- id: refresh_pulldown_states
then:
- lambda: |-
const uint32_t accent_u32 = ((uint32_t) id(accent_red) << 16) | ((uint32_t) id(accent_green) << 8) | (uint32_t) id(accent_blue);
const lv_color_t accent = lv_color_hex(accent_u32);
const uint32_t accent2_u32 = ((uint32_t) id(accent2_red) << 16) | ((uint32_t) id(accent2_green) << 8) | (uint32_t) id(accent2_blue);
const lv_color_t accent2 = lv_color_hex(accent2_u32);
auto apply_button = [&](lv_obj_t *btn, lv_obj_t *label, bool on) {
if (on) {
lv_obj_set_style_bg_color(btn, accent, 0);
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
} else {
lv_obj_set_style_bg_opa(btn, LV_OPA_TRANSP, 0);
}
lv_obj_set_style_border_color(btn, accent, 0);
lv_obj_set_style_text_color(label, accent2, 0);
};
apply_button(id(pulldown1_button), id(pulldown1_button_label), id(pulldown1_state).state);
apply_button(id(pulldown2_button), id(pulldown2_button_label), id(pulldown2_state).state);
apply_button(id(pulldown3_button), id(pulldown3_button_label), id(pulldown3_state).state);
apply_button(id(pulldown4_button), id(pulldown4_button_label), id(pulldown4_state).state);
- id: open_pulldown_menu
then:
- script.stop: player_controls_timeout
- script.stop: pulldown_timeout
- lambda: |-
id(ui_phase) = ${ui_phase_pulldown};
- script.execute: standby_screen_activity
- lvgl.page.show:
id: pulldown_page
animation: MOVE_BOTTOM
time: 220ms
- script.execute: refresh_pulldown_states
- script.execute: pulldown_timeout
- id: close_pulldown_menu
then:
- script.stop: pulldown_timeout
- lambda: |-
id(ui_phase) = ${ui_phase_standby};
- script.execute: standby_screen_activity
- lvgl.page.show:
id: idle_page
animation: MOVE_TOP
time: 220ms
- component.update: weather_line
- id: pulldown_timeout
mode: restart
then:
- delay: 6s
- script.execute: close_pulldown_menu
- id: play_playlist
parameters:
uri: string
then:
- if:
condition:
lambda: 'return !id(touch_swipe_detected) && !id(playlist_media_player_entity).state.empty() && !uri.empty();'
then:
- homeassistant.action:
action: media_player.play_media
data:
media_content_type: music
data_template:
entity_id: "{{ media_player_entity }}"
media_content_id: "{{ media_url }}"
variables:
media_player_entity: !lambda |-
return id(playlist_media_player_entity).state;
media_url: !lambda |-
return uri;
- id: load_standby_background
mode: restart
then:
- online_image.set_url:
id: standby_background_image
url: !lambda |-
return "https://raw.githubusercontent.com/RealDeco/xiaozhi-esphome/main/images/Other/360x360/" + id(standby_background_choice).current_option().str() + ".png";
- id: standby_screen_activity
mode: restart
then:
- script.stop: standby_screen_timeout
- if:
condition:
lambda: 'return id(screen_timeout_dimmed);'
then:
- light.turn_on:
id: Sled
brightness: !lambda 'return id(preferred_screen_brightness);'
- lambda: |-
id(screen_timeout_dimmed) = false;
- if:
condition:
and:
- switch.is_on: standby_screen_timeout_enabled
- lambda: 'return id(ui_phase) == ${ui_phase_standby};'
then:
- script.execute: standby_screen_timeout
- id: standby_screen_timeout
mode: restart
then:
- delay: !lambda |-
return (uint32_t) (id(standby_screen_timeout_seconds).state * 1000.0f / 3.0f);
- if:
condition:
and:
- switch.is_on: standby_screen_timeout_enabled
- lambda: 'return id(ui_phase) == ${ui_phase_standby};'
then:
- lambda: |-
id(preferred_screen_brightness) = id(Sled).remote_values.get_brightness();
id(screen_timeout_dimmed) = true;
- light.turn_on:
id: Sled
brightness: !lambda 'return id(preferred_screen_brightness) * 0.60f;'
else:
- script.stop: standby_screen_timeout
- delay: !lambda |-
return (uint32_t) (id(standby_screen_timeout_seconds).state * 1000.0f / 3.0f);
- if:
condition:
and:
- switch.is_on: standby_screen_timeout_enabled
- lambda: 'return id(ui_phase) == ${ui_phase_standby};'
then:
- light.turn_on:
id: Sled
brightness: !lambda 'return id(preferred_screen_brightness) * 0.25f;'
else:
- script.stop: standby_screen_timeout
- delay: !lambda |-
return (uint32_t) (id(standby_screen_timeout_seconds).state * 1000.0f / 3.0f);
- if:
condition:
and:
- switch.is_on: standby_screen_timeout_enabled
- lambda: 'return id(ui_phase) == ${ui_phase_standby};'
then:
- light.turn_off: Sled
- id: now_playing_swipe_left
then:
- script.execute:
id: album_art_slide
direction: -1
- media_player.next: sendspin_group_media_player
- id: now_playing_swipe_right
then:
- script.execute:
id: album_art_slide
direction: 1
- script.execute: media_player_previous_with_restart_check
- id: player_controls_keep_open
then:
- script.execute:
id: set_ui_phase
phase: ${ui_phase_player_controls}
- id: player_controls_toggle
then:
- delay: 75ms
- if:
condition:
lambda: 'return !id(touch_swipe_detected);'
then:
- media_player.toggle: sendspin_group_media_player
- script.execute: player_controls_keep_open
- id: player_controls_previous
then:
- delay: 75ms
- if:
condition:
lambda: 'return !id(touch_swipe_detected);'
then:
- script.execute: media_player_previous_with_restart_check
- script.execute: player_controls_keep_open
- id: player_controls_next
then:
- delay: 75ms
- if:
condition:
lambda: 'return !id(touch_swipe_detected);'
then:
- media_player.next: sendspin_group_media_player
- script.execute: player_controls_keep_open
- id: player_controls_volume_down
then:
- delay: 75ms
- if:
condition:
lambda: 'return !id(touch_swipe_detected);'
then:
- media_player.volume_down: external_media_player
- script.execute: show_rotary_volume_feedback
- script.execute: player_controls_keep_open
- id: player_controls_volume_up
then:
- delay: 75ms
- if:
condition:
lambda: 'return !id(touch_swipe_detected);'
then:
- media_player.volume_up: external_media_player
- script.execute: show_rotary_volume_feedback
- script.execute: player_controls_keep_open
- id: player_controls_mute
then:
- delay: 75ms
- if:
condition:
lambda: 'return !id(touch_swipe_detected);'
then:
- media_player.mute: external_media_player
- script.execute: player_controls_keep_open
- id: album_art_slide
mode: restart
parameters:
direction: int
then:
- lvgl.animation.stop: album_art_crossfade
- lambda: |-
id(album_art_slide_active) = true;
id(sendspin_album_art_valid) = false;
id(last_album_art_valid) = false;
lv_obj_t *outgoing = id(slide_outgoing_art);
lv_obj_t *incoming = id(slide_incoming_art);
lv_anim_del(outgoing, nullptr);
lv_anim_del(incoming, nullptr);
lv_img_set_src(outgoing, lv_img_get_src(id(incoming_art)));
lv_img_set_zoom(outgoing, LV_ZOOM_NONE);
lv_img_set_src(incoming, id(fallback_playing_image));
lv_img_set_zoom(incoming, LV_ZOOM_NONE);
lv_obj_set_x(outgoing, 0);
lv_obj_set_x(incoming, -direction * ${screenwidth});
lv_obj_clear_flag(outgoing, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(incoming, LV_OBJ_FLAG_HIDDEN);
lv_anim_t current_slide;
lv_anim_init(¤t_slide);
lv_anim_set_var(¤t_slide, outgoing);
lv_anim_set_values(¤t_slide, 0, direction * ${screenwidth});
lv_anim_set_time(¤t_slide, 300);
lv_anim_set_exec_cb(¤t_slide, [](void *obj, int32_t x) {
lv_obj_set_x((lv_obj_t *) obj, x);
});
lv_anim_start(¤t_slide);
lv_anim_t incoming_slide;
lv_anim_init(&incoming_slide);
lv_anim_set_var(&incoming_slide, incoming);
lv_anim_set_values(&incoming_slide, -direction * ${screenwidth}, 0);
lv_anim_set_time(&incoming_slide, 300);
lv_anim_set_exec_cb(&incoming_slide, [](void *obj, int32_t x) {
lv_obj_set_x((lv_obj_t *) obj, x);
});
lv_anim_start(&incoming_slide);
- delay: 300ms
- lambda: |-
lv_obj_add_flag(id(slide_outgoing_art), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(slide_incoming_art), LV_OBJ_FLAG_HIDDEN);
lv_img_set_src(id(incoming_art), id(fallback_playing_image));
lv_img_set_zoom(id(incoming_art), LV_ZOOM_NONE);
id(album_art_slide_active) = false;
id(album_art_awaiting_swipe_art) = true;
- id: refresh_artist_art_dot
then:
- lambda: |-
if (id(artist_art_valid) && !id(now_playing_art_mode)) {
lv_obj_clear_flag(id(artist_art_dot), LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(id(artist_art_dot), LV_OBJ_FLAG_HIDDEN);
}
- id: now_playing_show_artist_art
mode: restart
then:
- lvgl.animation.stop: album_art_crossfade
- lambda: |-
lv_obj_set_style_opa(id(outgoing_art), LV_OPA_TRANSP, 0);
lv_obj_t *outgoing = id(slide_outgoing_art);
lv_obj_t *incoming = id(slide_incoming_art);
lv_anim_del(outgoing, nullptr);
lv_anim_del(incoming, nullptr);
lv_img_set_src(outgoing, lv_img_get_src(id(incoming_art)));
lv_img_set_zoom(outgoing, LV_ZOOM_NONE);
lv_img_set_src(incoming, id(artist_art)->get_lv_image_dsc());
lv_img_set_zoom(incoming, LV_ZOOM_NONE);
lv_obj_set_x(outgoing, 0);
lv_obj_set_x(incoming, 0);
lv_obj_set_y(outgoing, 0);
lv_obj_set_y(incoming, -${screenheight});
lv_obj_clear_flag(outgoing, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(incoming, LV_OBJ_FLAG_HIDDEN);
lv_anim_t current_slide;
lv_anim_init(¤t_slide);
lv_anim_set_var(¤t_slide, outgoing);
lv_anim_set_values(¤t_slide, 0, ${screenheight});
lv_anim_set_time(¤t_slide, 300);
lv_anim_set_exec_cb(¤t_slide, [](void *obj, int32_t y) {
lv_obj_set_y((lv_obj_t *) obj, y);
});
lv_anim_start(¤t_slide);
lv_anim_t incoming_slide;
lv_anim_init(&incoming_slide);
lv_anim_set_var(&incoming_slide, incoming);
lv_anim_set_values(&incoming_slide, -${screenheight}, 0);
lv_anim_set_time(&incoming_slide, 300);
lv_anim_set_exec_cb(&incoming_slide, [](void *obj, int32_t y) {
lv_obj_set_y((lv_obj_t *) obj, y);
});
lv_anim_start(&incoming_slide);
id(now_playing_art_mode) = true;
id(refresh_artist_art_dot)->execute();
- delay: 300ms
- lambda: |-
lv_obj_add_flag(id(slide_outgoing_art), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(slide_incoming_art), LV_OBJ_FLAG_HIDDEN);
lv_img_set_src(id(incoming_art), id(artist_art)->get_lv_image_dsc());
lv_img_set_zoom(id(incoming_art), LV_ZOOM_NONE);
- id: now_playing_show_album_art
mode: restart
then:
- lvgl.animation.stop: album_art_crossfade
- lambda: |-
lv_obj_set_style_opa(id(outgoing_art), LV_OPA_TRANSP, 0);
lv_obj_t *outgoing = id(slide_outgoing_art);
lv_obj_t *incoming = id(slide_incoming_art);
lv_anim_del(outgoing, nullptr);
lv_anim_del(incoming, nullptr);
lv_img_set_src(outgoing, lv_img_get_src(id(incoming_art)));
lv_img_set_zoom(outgoing, LV_ZOOM_NONE);
lv_img_set_src(incoming, id(sendspin_album_art_valid) ? id(sendspin_album_art)->get_lv_image_dsc() : id(fallback_playing_image)->get_lv_image_dsc());
lv_img_set_zoom(incoming, LV_ZOOM_NONE);
lv_obj_set_x(outgoing, 0);
lv_obj_set_x(incoming, 0);
lv_obj_set_y(outgoing, 0);
lv_obj_set_y(incoming, ${screenheight});
lv_obj_clear_flag(outgoing, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(incoming, LV_OBJ_FLAG_HIDDEN);
lv_anim_t current_slide;
lv_anim_init(¤t_slide);
lv_anim_set_var(¤t_slide, outgoing);
lv_anim_set_values(¤t_slide, 0, -${screenheight});
lv_anim_set_time(¤t_slide, 300);
lv_anim_set_exec_cb(¤t_slide, [](void *obj, int32_t y) {
lv_obj_set_y((lv_obj_t *) obj, y);
});
lv_anim_start(¤t_slide);
lv_anim_t incoming_slide;
lv_anim_init(&incoming_slide);
lv_anim_set_var(&incoming_slide, incoming);
lv_anim_set_values(&incoming_slide, ${screenheight}, 0);
lv_anim_set_time(&incoming_slide, 300);
lv_anim_set_exec_cb(&incoming_slide, [](void *obj, int32_t y) {
lv_obj_set_y((lv_obj_t *) obj, y);
});
lv_anim_start(&incoming_slide);
id(now_playing_art_mode) = false;
id(refresh_artist_art_dot)->execute();
- delay: 300ms
- lambda: |-
lv_obj_add_flag(id(slide_outgoing_art), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(slide_incoming_art), LV_OBJ_FLAG_HIDDEN);
lv_img_set_src(id(incoming_art), id(sendspin_album_art_valid) ? id(sendspin_album_art)->get_lv_image_dsc() : id(fallback_playing_image)->get_lv_image_dsc());
lv_img_set_zoom(id(incoming_art), LV_ZOOM_NONE);
- id: media_player_previous_with_restart_check
then:
- lambda: |-
id(pre_previous_swipe_title) = id(sendspin_title).state;
- media_player.previous: sendspin_group_media_player
- script.execute: restore_art_if_same_title
- id: restore_art_if_same_title
mode: restart
then:
- delay: 1200ms
- if:
condition:
lambda: 'return id(sendspin_title).state == id(pre_previous_swipe_title);'
then:
- lambda: |-
id(sendspin_album_art_valid) = true;
id(album_art_awaiting_swipe_art) = false;
- script.execute: album_art_crossfade_apply
- id: album_art_crossfade_apply
mode: restart
then:
- lambda: |-
id(now_playing_art_mode) = false;
id(refresh_artist_art_dot)->execute();
- if:
condition:
lambda: 'return id(last_album_art_valid);'
then:
- lvgl.image.update:
id: outgoing_art
src: sendspin_album_art_transition
else:
- lvgl.image.update:
id: outgoing_art
src: fallback_playing_image
- lvgl.image.update:
id: incoming_art
src: sendspin_album_art
- lvgl.animation.start: album_art_crossfade
- id: haptic_click
mode: restart
then:
- if:
condition:
switch.is_on: rotary_haptic_feedback_enabled
then:
- lambda: |-
const uint32_t now = millis();
if (now - id(haptic_last_ms) < 45) return;
id(haptic_last_ms) = now;
- drv2605.play_effect:
id: haptic_driver
effect: 7
- id: show_rotary_volume_feedback
mode: restart
then:
- script.stop: hide_rotary_volume_feedback
- lambda: |-
static constexpr int ARC_WIDTH = 24;
if (id(volume_ring_overlay) == nullptr) {
id(volume_ring_overlay) = lv_obj_create(lv_layer_top());
lv_obj_remove_style_all(id(volume_ring_overlay));
lv_obj_set_size(id(volume_ring_overlay), ${screenwidth}, ${screenheight});
lv_obj_center(id(volume_ring_overlay));
lv_obj_clear_flag(id(volume_ring_overlay), LV_OBJ_FLAG_CLICKABLE);
lv_obj_clear_flag(id(volume_ring_overlay), LV_OBJ_FLAG_SCROLLABLE);
// Child 0: single arc � MAIN=dark track, INDICATOR=accent fill, KNOB=white dot
lv_obj_t *arc = lv_arc_create(id(volume_ring_overlay));
lv_obj_set_size(arc, 330, 330);
lv_obj_center(arc);
lv_arc_set_rotation(arc, 270);
lv_arc_set_bg_angles(arc, 0, 360);
lv_arc_set_range(arc, 0, 100);
lv_arc_set_value(arc, 0);
lv_obj_clear_flag(arc, LV_OBJ_FLAG_CLICKABLE);
lv_obj_clear_flag(arc, LV_OBJ_FLAG_SCROLLABLE);
// MAIN: transparent bounding box, dark full-circle track
lv_obj_set_style_bg_opa(arc, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_border_opa(arc, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_pad_all(arc, 0, LV_PART_MAIN);
lv_obj_set_style_arc_color(arc, lv_color_hex(0x202020), LV_PART_MAIN);
lv_obj_set_style_arc_width(arc, ARC_WIDTH, LV_PART_MAIN);
lv_obj_set_style_arc_opa(arc, 150, LV_PART_MAIN);
// INDICATOR: accent colour, width matches track
lv_obj_set_style_arc_width(arc, ARC_WIDTH, LV_PART_INDICATOR);
// KNOB: white circular handle at the end of the indicator
lv_obj_set_style_bg_color(arc, lv_color_hex(0xFFFFFF), LV_PART_KNOB);
lv_obj_set_style_bg_opa(arc, LV_OPA_COVER, LV_PART_KNOB);
lv_obj_set_style_radius(arc, LV_RADIUS_CIRCLE, LV_PART_KNOB);
lv_obj_set_style_pad_all(arc, 4, LV_PART_KNOB);
// Child 1: centered percentage label
id(volume_ring_label) = lv_label_create(id(volume_ring_overlay));
lv_obj_center(id(volume_ring_label));
lv_obj_set_style_text_color(id(volume_ring_label), lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_text_font(id(volume_ring_label), id(font_boot), 0);
}
const float volume = std::max(0.0f, std::min(1.0f, id(external_media_player).volume));
const int pct = (int) lroundf(volume * 100.0f);
lv_obj_t *arc = lv_obj_get_child(id(volume_ring_overlay), 0);
if (arc != nullptr) {
const uint32_t accent = ((uint32_t)id(accent_red) << 16)
| ((uint32_t)id(accent_green) << 8)
| (uint32_t)id(accent_blue);
lv_obj_set_style_arc_color(arc, lv_color_hex(accent), LV_PART_INDICATOR);
lv_arc_set_value(arc, pct);
}
const uint32_t accent4 = ((uint32_t)id(accent4_red) << 16)
| ((uint32_t)id(accent4_green) << 8)
| (uint32_t)id(accent4_blue);
lv_obj_set_style_text_color(id(volume_ring_label), lv_color_hex(accent4), 0);
char text[8];
snprintf(text, sizeof(text), "%d%%", pct);
lv_label_set_text(id(volume_ring_label), text);
lv_obj_set_style_opa(id(volume_ring_overlay), LV_OPA_COVER, 0);
lv_obj_clear_flag(id(volume_ring_overlay), LV_OBJ_FLAG_HIDDEN);
- script.execute: hide_rotary_volume_feedback
- id: hide_rotary_volume_feedback
mode: restart
then:
- delay: 900ms
- lambda: |-
if (id(volume_ring_overlay) == nullptr) return;
lv_anim_del(id(volume_ring_overlay), nullptr);
lv_anim_t fade;
lv_anim_init(&fade);
lv_anim_set_var(&fade, id(volume_ring_overlay));
lv_anim_set_values(&fade, LV_OPA_COVER, LV_OPA_TRANSP);
lv_anim_set_time(&fade, 260);
lv_anim_set_exec_cb(&fade, [](void *obj, int32_t opacity) {
lv_obj_set_style_opa((lv_obj_t *) obj, opacity, 0);
});
lv_anim_start(&fade);
- delay: 280ms
- lambda: |-
if (id(volume_ring_overlay) != nullptr) {
lv_obj_add_flag(id(volume_ring_overlay), LV_OBJ_FLAG_HIDDEN);
}
- id: album_art_show_fallback
mode: restart
then:
- if:
condition:
lambda: 'return !id(album_art_slide_active);'
then:
- if:
condition:
lambda: 'return id(last_album_art_valid);'
then:
- lvgl.image.update:
id: outgoing_art
src: sendspin_album_art_transition
else:
- lvgl.image.update:
id: outgoing_art
src: fallback_playing_image
- lvgl.image.update:
id: incoming_art
src: fallback_playing_image
- lvgl.animation.start: album_art_crossfade
- lvgl.image.update:
id: controls_album_art_widget
src: fallback_playing_image
- lvgl.image.update:
id: playlist_album_art_widget
src: fallback_playing_image
- id: play_sound
parameters:
priority: bool
sound_file: string
then:
- if:
condition:
lambda: 'return sound_file == "ready_sound";'
then:
- lambda: 'id(boot_announce_active) = true;'
- if:
condition:
lambda: return priority;
then:
- media_player.stop:
id: external_media_player
announcement: true
- lambda: |-
if ((id(external_media_player).state != media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING) || priority) {
id(external_media_player)
->make_call()
.set_media_url("audio-file://" + sound_file)
.set_announcement(true)
.perform();
}
output:
- platform: template
id: accent_color_r
type: float
write_action:
- lambda: |-
id(accent_red) = (int) lroundf(std::max(0.0f, std::min(1.0f, state)) * 255.0f);
- script.execute: apply_accent_color_debounced
- platform: template
id: accent_color_g
type: float
write_action:
- lambda: |-
id(accent_green) = (int) lroundf(std::max(0.0f, std::min(1.0f, state)) * 255.0f);
- script.execute: apply_accent_color_debounced
- platform: template
id: accent_color_b
type: float
write_action:
- lambda: |-
id(accent_blue) = (int) lroundf(std::max(0.0f, std::min(1.0f, state)) * 255.0f);
- script.execute: apply_accent_color_debounced
- platform: template
id: accent_color2_r
type: float
write_action:
- lambda: |-
id(accent2_red) = (int) lroundf(std::max(0.0f, std::min(1.0f, state)) * 255.0f);
- script.execute: apply_accent_color_debounced
- platform: template
id: accent_color2_g
type: float
write_action:
- lambda: |-
id(accent2_green) = (int) lroundf(std::max(0.0f, std::min(1.0f, state)) * 255.0f);
- script.execute: apply_accent_color_debounced
- platform: template
id: accent_color2_b
type: float
write_action:
- lambda: |-
id(accent2_blue) = (int) lroundf(std::max(0.0f, std::min(1.0f, state)) * 255.0f);
- script.execute: apply_accent_color_debounced
- platform: template
id: accent_color3_r
type: float
write_action:
- lambda: |-
id(accent3_red) = (int) lroundf(std::max(0.0f, std::min(1.0f, state)) * 255.0f);
- script.execute: apply_accent_color_debounced
- platform: template
id: accent_color3_g
type: float
write_action:
- lambda: |-
id(accent3_green) = (int) lroundf(std::max(0.0f, std::min(1.0f, state)) * 255.0f);
- script.execute: apply_accent_color_debounced
- platform: template
id: accent_color3_b
type: float
write_action:
- lambda: |-
id(accent3_blue) = (int) lroundf(std::max(0.0f, std::min(1.0f, state)) * 255.0f);
- script.execute: apply_accent_color_debounced
- platform: template
id: accent_color4_r
type: float
write_action:
- lambda: |-
id(accent4_red) = (int) lroundf(std::max(0.0f, std::min(1.0f, state)) * 255.0f);
- script.execute: apply_accent_color_debounced
- platform: template
id: accent_color4_g
type: float
write_action:
- lambda: |-
id(accent4_green) = (int) lroundf(std::max(0.0f, std::min(1.0f, state)) * 255.0f);
- script.execute: apply_accent_color_debounced
- platform: template
id: accent_color4_b
type: float
write_action:
- lambda: |-
id(accent4_blue) = (int) lroundf(std::max(0.0f, std::min(1.0f, state)) * 255.0f);
- script.execute: apply_accent_color_debounced
- platform: ledc
id: backlight_output
pin: GPIO21
inverted: false
light:
- platform: rgb
id: accent_color_picker
name: Accent color 1
icon: mdi:palette
entity_category: config
red: accent_color_r
green: accent_color_g
blue: accent_color_b
restore_mode: RESTORE_DEFAULT_ON
default_transition_length: 0s
- platform: rgb
id: accent_color_picker_2
name: Accent color 2
icon: mdi:palette
entity_category: config
red: accent_color2_r
green: accent_color2_g
blue: accent_color2_b
restore_mode: RESTORE_DEFAULT_ON
default_transition_length: 0s
- platform: rgb
id: accent_color_picker_3
name: Accent color 3
icon: mdi:palette
entity_category: config
red: accent_color3_r
green: accent_color3_g
blue: accent_color3_b
restore_mode: RESTORE_DEFAULT_ON
default_transition_length: 0s
- platform: rgb
id: accent_color_picker_4
name: Accent color 4
icon: mdi:palette
entity_category: config
red: accent_color4_r
green: accent_color4_g
blue: accent_color4_b
restore_mode: RESTORE_DEFAULT_ON
default_transition_length: 0s
- platform: monochromatic
id: Sled
name: Screen
icon: mdi:television
entity_category: config
output: backlight_output
restore_mode: ALWAYS_ON
default_transition_length: 250ms
- platform: esp32_rmt_led_strip
id: rgb_ring
name: RGB ring
icon: mdi:led-strip-variant
pin:
number: GPIO0
ignore_strapping_warning: true
num_leds: 13
chipset: WS2812
channel_colors: GRB
restore_mode: ALWAYS_OFF
default_transition_length: 250ms
touchscreen:
- platform: cst816
id: my_touchscreen
interrupt_pin: GPIO7
reset_pin: GPIO8
display: main_display
i2c_id: touchscreen_bus
on_touch:
then:
- script.execute: standby_screen_activity
- lambda: |-
id(touch_active) = true;
id(touch_start_x) = touch.x;
id(touch_start_y) = touch.y;
id(touch_last_x) = touch.x;
id(touch_last_y) = touch.y;
id(touch_start_ms) = millis();
id(touch_swipe_detected) = false;
on_update:
then:
- lambda: |-
if (!id(touch_active)) return;
for (auto &touch : touches) {
if (touch.state >= 1 && touch.state <= 2) {
id(touch_last_x) = touch.x;
id(touch_last_y) = touch.y;
const int min_dim = std::min(id(main_display).get_width(), id(main_display).get_height());
const int swipe_min_dist = std::max(16, min_dim / 8);
if (std::abs(id(touch_last_x) - id(touch_start_x)) >= swipe_min_dist ||
std::abs(id(touch_last_y) - id(touch_start_y)) >= swipe_min_dist) {
id(touch_swipe_detected) = true;
lv_obj_remove_state(id(controls_mute_button), LV_STATE_PRESSED);
lv_obj_remove_state(id(controls_previous_button), LV_STATE_PRESSED);
lv_obj_remove_state(id(controls_toggle_button), LV_STATE_PRESSED);
lv_obj_remove_state(id(controls_next_button), LV_STATE_PRESSED);
lv_obj_remove_state(id(controls_volume_down_button), LV_STATE_PRESSED);
lv_obj_remove_state(id(controls_volume_up_button), LV_STATE_PRESSED);
}
break;
}
}
on_release:
then:
- lambda: |-
if (!id(touch_active)) return;
id(touch_active) = false;
const int dx = id(touch_last_x) - id(touch_start_x);
const int dy = id(touch_last_y) - id(touch_start_y);
const int abs_x = std::abs(dx);
const int abs_y = std::abs(dy);
const uint32_t elapsed = millis() - id(touch_start_ms);
const int min_dim = std::min(id(main_display).get_width(), id(main_display).get_height());
const int swipe_min_dist = std::max(16, min_dim / 8);
const int tap_max_dist = std::max(14, min_dim / 12);
if (elapsed <= 1500 && (abs_x >= swipe_min_dist || abs_y >= swipe_min_dist)) {
if (id(ui_phase) == ${ui_phase_now_playing}) {
if (abs_x >= abs_y) {
if (dx < 0) {
id(now_playing_swipe_left)->execute();
} else {
id(now_playing_swipe_right)->execute();
}
} else if (dy < 0) {
if (id(now_playing_art_mode)) {
id(now_playing_show_album_art)->execute();
}
} else {
if (id(artist_art_valid) && !id(now_playing_art_mode)) {
id(now_playing_show_artist_art)->execute();
}
}
} else if (id(ui_phase) == ${ui_phase_player_controls} && abs_y > abs_x && dy > 0) {
id(open_playlist_menu)->execute();
} else if (id(ui_phase) == ${ui_phase_playlist_menu} && abs_y > abs_x && dy < 0) {
id(close_playlist_menu)->execute();
} else if (id(ui_phase) == ${ui_phase_standby}) {
if (abs_x >= abs_y) {
if (dx < 0) {
id(clock_style_choice).make_call().select_next(true).perform();
} else {
id(clock_style_choice).make_call().select_previous(true).perform();
}
} else if (dy > 0) {
id(open_pulldown_menu)->execute();
}
} else if (id(ui_phase) == ${ui_phase_pulldown} && abs_x < abs_y && dy < 0) {
id(close_pulldown_menu)->execute();
}
return;
}
if (elapsed <= 700 && abs_x <= tap_max_dist && abs_y <= tap_max_dist && id(ui_phase) != ${ui_phase_pulldown}) {
id(set_ui_phase)->execute(${ui_phase_player_controls});
}
display:
- platform: mipi_spi
id: main_display
model: ${display_model}
cs_pin: GPIO12
reset_pin: GPIO17
auto_clear_enabled: false
update_interval: never
dimensions:
height: ${screenheight}
width: ${screenwidth}
lvgl:
displays:
- main_display
buffer_size: 12%
color_depth: 16
bg_color: 0x000000
default_font: font_display
animations:
- id: album_art_crossfade
duration: ${album_fade_time}
widgets:
- id: outgoing_art
opa:
from: 100%
to: 0%
on_stop:
- sendspin.image.transition_finished: album_slot
pages:
- id: boot_sound_page
bg_color: 0x000000
widgets:
- label:
align: CENTER
text: "Home Connected"
text_color: 0xFFFFFF
text_font: font_boot
width: 230
long_mode: WRAP
text_align: CENTER
- arc:
id: arc_enable_stub
hidden: true
width: 1
height: 1
- canvas:
id: canvas_enable_stub
hidden: true
width: 1
height: 1
- id: idle_page
bg_color: 0x000000
widgets:
- image:
id: standby_background_widget
align: TOP_MID
x: 0
y: 0
src: standby_background_image
width: ${imagewidth}
height: ${imageheight}
antialias: false
clickable: false
hidden: true
- obj:
id: idle_center_block
align: CENTER
y: 18
width: 310
height: 300
bg_opa: TRANSP
border_opa: TRANSP
pad_all: 0
scrollable: false
widgets:
- label:
id: clock_label
align: TOP_MID
y: 18
width: 310
text: "--:--:--"
text_color: 0xFFFF00
text_font: font_7seg
text_letter_space: 0
text_align: CENTER
- label:
id: date_label
align: TOP_MID
y: 94
width: 310
text: "------"
text_color: 0xFFFFFF
text_font: font_date
text_align: CENTER
- obj:
id: weather_header
align: TOP_MID
y: 130
width: 310
height: 100
bg_opa: TRANSP
border_opa: TRANSP
pad_all: 0
scrollable: false
widgets:
- label:
id: weather_icon_glow
align: CENTER
x: -54
y: 1
text: "\U000F0590"
text_color: 0x404040
text_font: font_mdi_weather
- label:
id: weather_icon
align: CENTER
x: -55
y: 0
text: "\U000F0590"
text_color: 0xFFFFFF
text_font: font_mdi_weather
- label:
id: weather_big_temp
align: CENTER
x: 48
y: 3
text: "--"
text_color: 0xFFFF00
text_font: font_temp_big
- label:
id: weather_big_temp_unit
align: CENTER
x: 102
y: -20
text: "\u00B0C"
text_color: 0xFFFF00
text_font: font_temp_unit
- label:
id: weather_label
align: TOP_MID
x: 0
y: 224
width: 285
height: 56
text: "Weather..."
text_color: 0x66CCFF
text_font: font_display
long_mode: WRAP
text_align: CENTER
- label:
id: clock_ampm_label
x: 0
y: 0
text: ""
text_color: 0xFFFF00
text_font: font_ampm
hidden: true
- label:
id: analog_weather_icon
align: CENTER
x: -105
y: 0
text: ""
text_color: 0xFFFFFF
text_font: font_mdi_weather_small
hidden: true
- label:
id: analog_weather_temp
align: CENTER
x: 105
y: 0
text: ""
text_color: 0xFFFF00
text_font: font_display_medium
text_align: CENTER
hidden: true
- label:
id: rooms_clock_label
align: TOP_MID
y: 24
width: 310
text: "--:--"
text_color: 0xFFFF00
text_font: font_7seg_small
text_letter_space: 0
text_align: CENTER
hidden: true
- label:
id: rooms_date_label
align: TOP_MID
y: 278
width: 190
text: "------"
text_color: 0xFFFFFF
text_font: font_date
text_align: CENTER
hidden: true
- obj:
id: room1_box
align: TOP_LEFT
x: 50
y: 85
width: 100
height: 90
radius: 12
border_width: 2
border_color: 0xFFFF00
bg_color: 0x4FC3F7
bg_opa: TRANSP
pad_all: 0
scrollable: false
clickable: false
hidden: true
widgets:
- label:
id: room1_icon
align: CENTER
text: "\U000F0004"
text_color: 0xFFFFFF
text_font: font_mdi_rooms
hidden: true
- obj:
id: room2_box
align: TOP_LEFT
x: 50
y: 180
width: 100
height: 90
radius: 12
border_width: 2
border_color: 0xFFFF00
bg_color: 0xE53935
bg_opa: TRANSP
pad_all: 0
scrollable: false
clickable: false
hidden: true
widgets:
- label:
id: room2_icon
align: CENTER
text: "\U000F0004"
text_color: 0xFFFFFF
text_font: font_mdi_rooms
hidden: true
- obj:
id: room3_box
align: TOP_LEFT
x: 156
y: 85
width: 58
height: 185
radius: 12
border_width: 2
border_color: 0xFFFF00
bg_color: 0xFDD835
bg_opa: TRANSP
pad_all: 0
scrollable: false
clickable: false
hidden: true
widgets:
- label:
id: room3_icon
align: CENTER
text: "\U000F0004"
text_color: 0xFFFFFF
text_font: font_mdi_rooms
hidden: true
- obj:
id: room4_box
align: TOP_LEFT
x: 220
y: 85
width: 90
height: 58
radius: 12
border_width: 2
border_color: 0xFFFF00
bg_color: 0xBA68C8
bg_opa: TRANSP
pad_all: 0
scrollable: false
clickable: false
hidden: true
widgets:
- label:
id: room4_icon
align: CENTER
text: "\U000F0004"
text_color: 0xFFFFFF
text_font: font_mdi_rooms
hidden: true
- obj:
id: room5_box
align: TOP_LEFT
x: 220
y: 148
width: 90
height: 58
radius: 12
border_width: 2
border_color: 0xFFFF00
bg_color: 0x1A237E
bg_opa: TRANSP
pad_all: 0
scrollable: false
clickable: false
hidden: true
widgets:
- label:
id: room5_icon
align: CENTER
text: "\U000F0004"
text_color: 0xFFFFFF
text_font: font_mdi_rooms
hidden: true
- obj:
id: room6_box
align: TOP_LEFT
x: 220
y: 211
width: 90
height: 59
radius: 12
border_width: 2
border_color: 0xFFFF00
bg_color: 0x81C784
bg_opa: TRANSP
pad_all: 0
scrollable: false
clickable: false
hidden: true
widgets:
- label:
id: room6_icon
align: CENTER
text: "\U000F0004"
text_color: 0xFFFFFF
text_font: font_mdi_rooms
hidden: true
- id: player_controls_page
bg_color: 0x000000
widgets:
- image:
id: controls_album_art_widget
align: TOP_MID
x: 0
y: 0
src: fallback_playing_image
width: ${imagewidth}
height: ${imageheight}
antialias: false
clickable: false
- obj:
align: CENTER
width: 360
height: 360
bg_color: 0x000000
bg_opa: 58%
border_opa: TRANSP
radius: 0
scrollable: false
- label:
id: controls_title_label
align: TOP_MID
y: 30
width: 285
text: ""
text_color: 0xFFFFFF
text_font: font_date
long_mode: SCROLL_CIRCULAR
text_align: CENTER
- label:
id: controls_artist_label
align: TOP_MID
y: 66
width: 265
text: ""
text_color: 0x66CCFF
text_font: font_display
long_mode: SCROLL_CIRCULAR
text_align: CENTER
- label:
id: controls_album_label
align: TOP_MID
y: 92
width: 250
text: ""
text_color: 0x8EDBFF
text_font: font_display
long_mode: SCROLL_CIRCULAR
text_align: CENTER
- button:
id: controls_mute_button
align: BOTTOM_MID
y: -58
width: 58
height: 48
radius: 24
bg_color: 0x202020
bg_opa: 85%
border_color: 0x505050
border_width: 1
pressed:
bg_color: 0x66CCFF
bg_opa: COVER
border_color: 0xFFFFFF
border_width: 3
on_click:
- script.execute: player_controls_mute
widgets:
- label:
id: controls_mute_icon
align: CENTER
text: "\U000F075F"
text_color: 0xFFFFFF
text_font: font_mdi_controls
- button:
id: controls_previous_button
align: CENTER
x: -104
y: 0
width: 84
height: 84
radius: 42
bg_color: 0x202020
bg_opa: 85%
border_color: 0x505050
border_width: 1
pressed:
bg_color: 0x66CCFF
bg_opa: COVER
border_color: 0xFFFFFF
border_width: 3
on_click:
- script.execute: player_controls_previous
widgets:
- label:
align: CENTER
text: "\U000F04AE"
text_color: 0xFFFFFF
text_font: font_mdi_controls
- button:
id: controls_toggle_button
align: CENTER
y: 0
width: 104
height: 104
radius: 52
bg_color: 0xFFFF00
bg_opa: COVER
border_color: 0xFFFFFF
border_width: 2
pressed:
bg_color: 0xFFFFFF
border_color: 0x66CCFF
border_width: 4
on_click:
- script.execute: player_controls_toggle
widgets:
- label:
align: CENTER
text: "\U000F040E"
text_color: 0x101010
text_font: font_mdi_controls
- button:
id: controls_next_button
align: CENTER
x: 104
y: 0
width: 84
height: 84
radius: 42
bg_color: 0x202020
bg_opa: 85%
border_color: 0x505050
border_width: 1
pressed:
bg_color: 0x66CCFF
bg_opa: COVER
border_color: 0xFFFFFF
border_width: 3
on_click:
- script.execute: player_controls_next
widgets:
- label:
align: CENTER
text: "\U000F04AD"
text_color: 0xFFFFFF
text_font: font_mdi_controls
- button:
id: controls_volume_down_button
align: BOTTOM_MID
x: -72
y: -58
width: 58
height: 48
radius: 24
bg_color: 0x202020
bg_opa: 85%
border_color: 0x505050
border_width: 1
pressed:
bg_color: 0x66CCFF
bg_opa: COVER
border_color: 0xFFFFFF
border_width: 3
on_click:
- script.execute: player_controls_volume_down
on_long_press_repeat:
- script.execute: player_controls_volume_down
widgets:
- label:
id: controls_volume_down_icon
align: CENTER
text: "\U000F075E"
text_color: 0xFFFFFF
text_font: font_mdi_controls
- button:
id: controls_volume_up_button
align: BOTTOM_MID
x: 72
y: -58
width: 58
height: 48
radius: 24
bg_color: 0x202020
bg_opa: 85%
border_color: 0x505050
border_width: 1
pressed:
bg_color: 0x66CCFF
bg_opa: COVER
border_color: 0xFFFFFF
border_width: 3
on_click:
- script.execute: player_controls_volume_up
on_long_press_repeat:
- script.execute: player_controls_volume_up
widgets:
- label:
id: controls_volume_up_icon
align: CENTER
text: "\U000F075D"
text_color: 0xFFFFFF
text_font: font_mdi_controls
- id: sendspin_page
bg_color: 0x000000
widgets:
- image:
id: incoming_art
align: TOP_MID
x: 0
y: 0
src: fallback_playing_image
width: ${imagewidth}
height: ${imageheight}
antialias: false
clickable: false
- image:
id: outgoing_art
align: TOP_MID
x: 0
y: 0
src: fallback_playing_image
width: ${imagewidth}
height: ${imageheight}
antialias: false
clickable: false
- obj:
id: artist_art_dot
align: TOP_MID
x: 0
y: 20
width: 14
height: 14
radius: 7
bg_color: 0xFF3B30
bg_opa: COVER
border_opa: TRANSP
pad_all: 0
scrollable: false
clickable: false
hidden: true
- image:
id: slide_outgoing_art
align: TOP_MID
x: 0
y: 0
src: fallback_playing_image
width: ${imagewidth}
height: ${imageheight}
antialias: false
clickable: false
hidden: true
- image:
id: slide_incoming_art
align: TOP_MID
x: 0
y: 0
src: fallback_playing_image
width: ${imagewidth}
height: ${imageheight}
antialias: false
clickable: false
hidden: true
- obj:
id: now_playing_bar
align: BOTTOM_MID
y: -28
width: 300
height: 36
bg_color: 0x000000
bg_opa: 75%
border_opa: TRANSP
radius: 4
pad_all: 0
scrollable: false
widgets:
- label:
id: now_playing_label
align: CENTER
width: 284
text: ""
text_color: 0xFFFFFF
text_font: font_display
long_mode: SCROLL_CIRCULAR
text_align: CENTER
- id: playlist_menu_page
bg_color: 0x000000
widgets:
- image:
id: playlist_album_art_widget
align: TOP_MID
x: 0
y: 0
src: fallback_playing_image
width: ${imagewidth}
height: ${imageheight}
antialias: false
clickable: false
- obj:
align: CENTER
width: 360
height: 360
bg_color: 0x000000
bg_opa: 58%
border_opa: TRANSP
radius: 0
scrollable: false
- button:
id: playlist_1_button
align: TOP_MID
x: -74
y: 28
width: 138
height: 86
radius: 18
bg_color: 0x181800
bg_opa: 94%
border_color: 0xFFFF00
border_width: 4
shadow_color: 0xFFFF00
shadow_width: 18
shadow_opa: 45%
pressed:
bg_color: 0xFFFF00
bg_opa: COVER
border_color: 0xFFFFFF
border_width: 5
shadow_color: 0xFFFFFF
shadow_width: 24
shadow_opa: 80%
on_click:
- script.execute:
id: play_playlist
uri: !lambda return id(playlist_1_uri).state;
widgets:
- label:
id: playlist_1_label
align: CENTER
width: 116
text: "My playlist 1"
text_color: 0xFFFFFF
text_font: font_playlist
text_align: CENTER
- button:
id: playlist_2_button
align: TOP_MID
x: 74
y: 28
width: 138
height: 86
radius: 18
bg_color: 0x181800
bg_opa: 94%
border_color: 0xFFFF00
border_width: 4
shadow_color: 0xFFFF00
shadow_width: 18
shadow_opa: 45%
pressed:
bg_color: 0xFFFF00
bg_opa: COVER
border_color: 0xFFFFFF
border_width: 5
shadow_color: 0xFFFFFF
shadow_width: 24
shadow_opa: 80%
on_click:
- script.execute:
id: play_playlist
uri: !lambda return id(playlist_2_uri).state;
widgets:
- label:
id: playlist_2_label
align: CENTER
width: 116
text: "My playlist 2"
text_color: 0xFFFFFF
text_font: font_playlist
text_align: CENTER
- button:
id: playlist_3_button
align: TOP_MID
x: -74
y: 134
width: 138
height: 86
radius: 18
bg_color: 0x181800
bg_opa: 94%
border_color: 0xFFFF00
border_width: 4
shadow_color: 0xFFFF00
shadow_width: 18
shadow_opa: 45%
pressed:
bg_color: 0xFFFF00
bg_opa: COVER
border_color: 0xFFFFFF
border_width: 5
shadow_color: 0xFFFFFF
shadow_width: 24
shadow_opa: 80%
on_click:
- script.execute:
id: play_playlist
uri: !lambda return id(playlist_3_uri).state;
widgets:
- label:
id: playlist_3_label
align: CENTER
width: 116
text: "My playlist 3"
text_color: 0xFFFFFF
text_font: font_playlist
text_align: CENTER
- button:
id: playlist_4_button
align: TOP_MID
x: 74
y: 134
width: 138
height: 86
radius: 18
bg_color: 0x181800
bg_opa: 94%
border_color: 0xFFFF00
border_width: 4
shadow_color: 0xFFFF00
shadow_width: 18
shadow_opa: 45%
pressed:
bg_color: 0xFFFF00
bg_opa: COVER
border_color: 0xFFFFFF
border_width: 5
shadow_color: 0xFFFFFF
shadow_width: 24
shadow_opa: 80%
on_click:
- script.execute:
id: play_playlist
uri: !lambda return id(playlist_4_uri).state;
widgets:
- label:
id: playlist_4_label
align: CENTER
width: 116
text: "My playlist 4"
text_color: 0xFFFFFF
text_font: font_playlist
text_align: CENTER
- button:
id: playlist_5_button
align: TOP_MID
x: -74
y: 240
width: 138
height: 86
radius: 18
bg_color: 0x181800
bg_opa: 94%
border_color: 0xFFFF00
border_width: 4
shadow_color: 0xFFFF00
shadow_width: 18
shadow_opa: 45%
pressed:
bg_color: 0xFFFF00
bg_opa: COVER
border_color: 0xFFFFFF
border_width: 5
shadow_color: 0xFFFFFF
shadow_width: 24
shadow_opa: 80%
on_click:
- script.execute:
id: play_playlist
uri: !lambda return id(playlist_5_uri).state;
widgets:
- label:
id: playlist_5_label
align: CENTER
width: 116
text: "My playlist 5"
text_color: 0xFFFFFF
text_font: font_playlist
text_align: CENTER
- button:
id: playlist_6_button
align: TOP_MID
x: 74
y: 240
width: 138
height: 86
radius: 18
bg_color: 0x181800
bg_opa: 94%
border_color: 0xFFFF00
border_width: 4
shadow_color: 0xFFFF00
shadow_width: 18
shadow_opa: 45%
pressed:
bg_color: 0xFFFF00
bg_opa: COVER
border_color: 0xFFFFFF
border_width: 5
shadow_color: 0xFFFFFF
shadow_width: 24
shadow_opa: 80%
on_click:
- script.execute:
id: play_playlist
uri: !lambda return id(playlist_6_uri).state;
widgets:
- label:
id: playlist_6_label
align: CENTER
width: 116
text: "My playlist 6"
text_color: 0xFFFFFF
text_font: font_playlist
text_align: CENTER
- id: pulldown_page
bg_color: 0x000000
widgets:
- image:
id: pulldown_background_widget
align: TOP_MID
x: 0
y: 0
src: standby_background_image
width: ${imagewidth}
height: ${imageheight}
antialias: false
clickable: false
hidden: true
- label:
id: pulldown_title_label
align: TOP_MID
y: 16
width: 310
text: "Controls"
text_color: 0x66CCFF
text_font: font_display_medium
text_align: CENTER
- button:
id: pulldown1_button
align: TOP_MID
x: -72
y: 55
width: 130
height: 118
radius: 20
bg_opa: TRANSP
border_color: 0xFFFF00
border_width: 4
shadow_color: 0xFFFF00
shadow_width: 18
shadow_opa: 45%
on_click:
- homeassistant.action:
action: homeassistant.toggle
data:
entity_id: ${pulldown1_entity}
- script.execute: pulldown_timeout
widgets:
- label:
id: pulldown1_button_label
align: CENTER
width: 108
text: "Light 1"
text_color: 0xFFFFFF
text_font: font_playlist
text_align: CENTER
- button:
id: pulldown2_button
align: TOP_MID
x: 72
y: 55
width: 130
height: 118
radius: 20
bg_opa: TRANSP
border_color: 0xFFFF00
border_width: 4
shadow_color: 0xFFFF00
shadow_width: 18
shadow_opa: 45%
on_click:
- homeassistant.action:
action: homeassistant.toggle
data:
entity_id: ${pulldown2_entity}
- script.execute: pulldown_timeout
widgets:
- label:
id: pulldown2_button_label
align: CENTER
width: 108
text: "Light 2"
text_color: 0xFFFFFF
text_font: font_playlist
text_align: CENTER
- button:
id: pulldown3_button
align: TOP_MID
x: -72
y: 187
width: 130
height: 118
radius: 20
bg_opa: TRANSP
border_color: 0xFFFF00
border_width: 4
shadow_color: 0xFFFF00
shadow_width: 18
shadow_opa: 45%
on_click:
- homeassistant.action:
action: homeassistant.toggle
data:
entity_id: ${pulldown3_entity}
- script.execute: pulldown_timeout
widgets:
- label:
id: pulldown3_button_label
align: CENTER
width: 108
text: "Light 3"
text_color: 0xFFFFFF
text_font: font_playlist
text_align: CENTER
- button:
id: pulldown4_button
align: TOP_MID
x: 72
y: 187
width: 130
height: 118
radius: 20
bg_opa: TRANSP
border_color: 0xFFFF00
border_width: 4
shadow_color: 0xFFFF00
shadow_width: 18
shadow_opa: 45%
on_click:
- homeassistant.action:
action: homeassistant.toggle
data:
entity_id: ${pulldown4_entity}
- script.execute: pulldown_timeout
widgets:
- label:
id: pulldown4_button_label
align: CENTER
width: 108
text: "Light 4"
text_color: 0xFFFFFF
text_font: font_playlist
text_align: CENTER
# Factory firmware - generated by ewt-gen
# Users should import Guition_Knob_v2.yaml directly, not this file.
packages:
original: !include Guition_Knob_v2.yaml
esphome:
project:
version: "2026.10.04.57"
ota:
- platform: http_request
id: ota_http_request
update:
- platform: http_request
id: update_http_request
name: Firmware
source: https://realdeco.github.io/sendspin-guition/Guition_Knob_v2/manifest.json
http_request:
dashboard_import:
package_import_url: github://RealDeco/sendspin-guition/Guition_Knob_v2.yaml@main