mirror of
https://github.com/randybb/esphome-configs.git
synced 2026-10-14 08:35:12 +02:00
mcu-lvgl (CYD 4.3"): - normalized: rpi_dpi_rgb -> mipi_rgb (ESP32-8048S050 pins/timing, pclk 12.5 MHz), GT911 touch added, platformio_options and default CPU frequency dropped, demo meters and duplicate uptime sensors removed - pages: clock, status (logo, WiFi, IPv6 first, HA, version, uptime); status page shown until Home Assistant connects - same navigation bar and theme as mcu-lvgl-trainer mcu-lvgl-trainer: - status lines colored like on the other displays (connected green, waiting orange, details gray), stacked in a flex column - stopwatch format warning fixed
751 lines
22 KiB
YAML
751 lines
22 KiB
YAML
---
|
||
substitutions:
|
||
device: lvgl-trainer
|
||
name: Trainer Display
|
||
area: Basement
|
||
comment: "${area} | Pain Cave"
|
||
light_basement: light.mcu_basement_2_light
|
||
light_workbench: light.mcu_basement_2_workbench_light
|
||
light_exercise: light.exercise_room
|
||
fan: light.mcu_dimmer_exercise_room_fan
|
||
|
||
# Pages (480x320, navigation bar at the bottom):
|
||
# clock, lights, training, fan, stopwatch, status
|
||
|
||
esphome:
|
||
name: mcu-${device}
|
||
friendly_name: ${name}
|
||
area: ${area}
|
||
comment: ${comment}
|
||
|
||
esp32:
|
||
variant: esp32s3
|
||
flash_size: 16MB
|
||
framework:
|
||
type: esp-idf
|
||
|
||
packages:
|
||
common: !include common/common.yaml
|
||
ipv6_only: !include common/ipv6_only.yaml
|
||
|
||
i2c:
|
||
- id: touchscreen_bus
|
||
sda: 4
|
||
scl: 8
|
||
|
||
psram:
|
||
mode: octal
|
||
speed: 80MHz
|
||
|
||
spi:
|
||
id: display_qspi
|
||
type: quad
|
||
clk_pin: 47
|
||
data_pins: [21, 48, 40, 39]
|
||
|
||
globals:
|
||
# stopwatch: accumulated time of finished runs + start of the current run
|
||
- id: sw_running
|
||
type: bool
|
||
initial_value: 'false'
|
||
- id: sw_start
|
||
type: uint32_t
|
||
initial_value: '0'
|
||
- id: sw_accum
|
||
type: uint32_t
|
||
initial_value: '0'
|
||
|
||
output:
|
||
- platform: ledc
|
||
id: gpio_backlight_pwm
|
||
pin: 1
|
||
|
||
light:
|
||
- id: display_backlight
|
||
name: Backlight
|
||
platform: monochromatic
|
||
output: gpio_backlight_pwm
|
||
restore_mode: ALWAYS_ON
|
||
default_transition_length: 500ms
|
||
|
||
display:
|
||
- platform: mipi_spi
|
||
model: AXS15231
|
||
data_rate: 40MHz #20mhz is default
|
||
id: main_display
|
||
spi_id: display_qspi
|
||
dimensions:
|
||
height: 480
|
||
width: 320
|
||
cs_pin: 45
|
||
auto_clear_enabled: false
|
||
|
||
touchscreen:
|
||
- platform: axs15231
|
||
id: main_touch
|
||
display: main_display
|
||
i2c_id: touchscreen_bus
|
||
update_interval: 100ms
|
||
# interrupt_pin:
|
||
# number: 3
|
||
# ignore_strapping_warning: true
|
||
# no calibration/transform: the driver defaults to the native 320x480 panel and
|
||
# LVGL applies its own rotation to touch points
|
||
on_release:
|
||
- if:
|
||
condition: lvgl.is_paused
|
||
then:
|
||
- lvgl.resume:
|
||
- lvgl.widget.redraw:
|
||
- light.turn_on:
|
||
id: display_backlight
|
||
transition_length: 300ms
|
||
|
||
uart:
|
||
- id: ext_uart
|
||
tx_pin: 43
|
||
rx_pin: 44
|
||
baud_rate: 115200
|
||
stop_bits: 1
|
||
parity: NONE
|
||
|
||
binary_sensor:
|
||
- platform: homeassistant
|
||
id: basement_light
|
||
entity_id: ${light_basement}
|
||
trigger_on_initial_state: true
|
||
on_state:
|
||
- lvgl.widget.update:
|
||
id: basement_light_btn
|
||
state:
|
||
checked: !lambda return x;
|
||
- platform: homeassistant
|
||
id: workbench_light
|
||
entity_id: ${light_workbench}
|
||
trigger_on_initial_state: true
|
||
on_state:
|
||
- lvgl.widget.update:
|
||
id: workbench_light_btn
|
||
state:
|
||
checked: !lambda return x;
|
||
- platform: homeassistant
|
||
id: exercise_light
|
||
entity_id: ${light_exercise}
|
||
trigger_on_initial_state: true
|
||
on_state:
|
||
- lvgl.widget.update:
|
||
id: exercise_light_btn
|
||
state:
|
||
checked: !lambda return x;
|
||
# brightness and color temperature only make sense while the light is on
|
||
- if:
|
||
condition:
|
||
lambda: return x;
|
||
then:
|
||
- lvgl.widget.show: exercise_controls
|
||
else:
|
||
- lvgl.widget.hide: exercise_controls
|
||
- platform: homeassistant
|
||
id: fan_on
|
||
entity_id: ${fan}
|
||
trigger_on_initial_state: true
|
||
on_state:
|
||
- script.execute: update_fan_buttons
|
||
|
||
sensor:
|
||
- platform: homeassistant
|
||
id: fan_brightness
|
||
entity_id: ${fan}
|
||
attribute: brightness
|
||
on_value:
|
||
- script.execute: update_fan_buttons
|
||
|
||
text_sensor:
|
||
# light attributes read as text: they are 'None' while the light is off
|
||
- platform: homeassistant
|
||
id: exercise_brightness
|
||
entity_id: ${light_exercise}
|
||
attribute: brightness
|
||
on_value:
|
||
- if:
|
||
condition:
|
||
lambda: return parse_number<float>(x).has_value();
|
||
then:
|
||
- lvgl.slider.update:
|
||
id: exercise_brightness_slider
|
||
value: !lambda return roundf(*parse_number<float>(x) * 100.0f / 255.0f);
|
||
- platform: homeassistant
|
||
id: exercise_color_temp
|
||
entity_id: ${light_exercise}
|
||
attribute: color_temp_kelvin
|
||
on_value:
|
||
- if:
|
||
condition:
|
||
lambda: return parse_number<float>(x).has_value();
|
||
then:
|
||
- lvgl.slider.update:
|
||
id: exercise_color_temp_slider
|
||
value: !lambda return *parse_number<float>(x);
|
||
# a running automation (e.g. its delay) has current > 0
|
||
- platform: homeassistant
|
||
id: training_start_running
|
||
entity_id: automation.zwift_enable_fan
|
||
attribute: current
|
||
on_value:
|
||
- script.execute: update_training_status
|
||
- platform: homeassistant
|
||
id: training_stop_running
|
||
entity_id: automation.zwift_disable_fan
|
||
attribute: current
|
||
on_value:
|
||
- script.execute: update_training_status
|
||
|
||
script:
|
||
- id: update_fan_buttons
|
||
then:
|
||
- lambda: |-
|
||
// fan level rounded to the 10 % steps of the buttons, 0 = off
|
||
int pct = 0;
|
||
if (id(fan_on).state && !std::isnan(id(fan_brightness).state))
|
||
pct = (int) roundf(id(fan_brightness).state * 10.0f / 255.0f) * 10;
|
||
const int levels[] = {0, 50, 60, 70, 80, 90, 100};
|
||
lv_obj_t *matrix = id(fan_matrix)->obj;
|
||
for (int i = 0; i < 7; i++) {
|
||
if (levels[i] == pct)
|
||
lv_buttonmatrix_set_button_ctrl(matrix, i, LV_BUTTONMATRIX_CTRL_CHECKED);
|
||
else
|
||
lv_buttonmatrix_clear_button_ctrl(matrix, i, LV_BUTTONMATRIX_CTRL_CHECKED);
|
||
}
|
||
- lvgl.label.update:
|
||
id: fan_status_label
|
||
text: !lambda |-
|
||
if (!id(fan_on).state)
|
||
return std::string("Ventilátor vypnutý");
|
||
if (std::isnan(id(fan_brightness).state))
|
||
return std::string("Ventilátor zapnutý");
|
||
return str_sprintf("Ventilátor %.0f %%", id(fan_brightness).state * 100.0f / 255.0f);
|
||
|
||
- id: update_training_status
|
||
then:
|
||
- lvgl.label.update:
|
||
id: training_status_label
|
||
text: !lambda |-
|
||
if (id(training_start_running).state != "0" && !id(training_start_running).state.empty())
|
||
return std::string("Tréning sa spúšťa, ventilátor sa zapne o 10 min");
|
||
if (id(training_stop_running).state != "0" && !id(training_stop_running).state.empty())
|
||
return std::string("Tréning sa ukončuje, ventilátor sa vypne o 2 min");
|
||
return std::string("");
|
||
|
||
- id: update_stopwatch
|
||
then:
|
||
- lvgl.label.update:
|
||
id: stopwatch_label
|
||
text: !lambda |-
|
||
const unsigned ms = id(sw_accum) + (id(sw_running) ? millis() - id(sw_start) : 0);
|
||
const unsigned s = ms / 1000;
|
||
if (s >= 3600)
|
||
return str_sprintf("%u:%02u:%02u", s / 3600, (s / 60) % 60, s % 60);
|
||
return str_sprintf("%02u:%02u.%u", s / 60, s % 60, (ms / 100) % 10);
|
||
- lvgl.label.update:
|
||
id: stopwatch_start_label
|
||
text: !lambda |-
|
||
return std::string(id(sw_running) ? "Stop" : "Štart");
|
||
|
||
- id: update_status
|
||
then:
|
||
- lambda: |-
|
||
// same colors as the other status screens: ok green, waiting orange, details gray
|
||
static const uint32_t OK = 0x4DD966, WAIT = 0xFFB300;
|
||
auto set = [](lv_obj_t *label, const std::string &text, uint32_t color) {
|
||
lv_label_set_text(label, text.c_str());
|
||
lv_obj_set_style_text_color(label, lv_color_hex(color), LV_PART_MAIN);
|
||
};
|
||
|
||
const bool wifi_ok = wifi::global_wifi_component->is_connected();
|
||
set(id(status_wifi),
|
||
wifi_ok ? str_sprintf("WiFi: connected (%d dBm)", wifi::global_wifi_component->wifi_rssi())
|
||
: std::string("WiFi: connecting..."),
|
||
wifi_ok ? OK : WAIT);
|
||
|
||
// IPv6 first (skip link-local), then IPv4
|
||
std::string ips;
|
||
if (wifi_ok) {
|
||
char buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||
for (bool v6 : {true, false}) {
|
||
for (auto &ip : wifi::global_wifi_component->get_ip_addresses()) {
|
||
if (!ip.is_set() || ip.is_ip6() != v6)
|
||
continue;
|
||
ip.str_to(buf);
|
||
if (v6 && strncmp(buf, "fe80", 4) == 0)
|
||
continue;
|
||
if (!ips.empty())
|
||
ips += "\n";
|
||
ips += buf;
|
||
}
|
||
}
|
||
}
|
||
lv_label_set_text(id(status_ips), ips.c_str());
|
||
if (ips.empty())
|
||
lv_obj_add_flag(id(status_ips), LV_OBJ_FLAG_HIDDEN);
|
||
else
|
||
lv_obj_remove_flag(id(status_ips), LV_OBJ_FLAG_HIDDEN);
|
||
|
||
const bool api_ok = api::global_api_server->is_connected();
|
||
set(id(status_ha), api_ok ? "Home Assistant: connected" : "Home Assistant: waiting...",
|
||
api_ok ? OK : WAIT);
|
||
|
||
const uint32_t up = millis() / 1000;
|
||
lv_label_set_text(id(status_footer),
|
||
str_sprintf("ESPHome %s · up %ud %02u:%02u:%02u", ESPHOME_VERSION, up / 86400,
|
||
(up / 3600) % 24, (up / 60) % 60, up % 60).c_str());
|
||
interval:
|
||
- interval: 1s
|
||
then:
|
||
- lvgl.label.update:
|
||
id: clock_label
|
||
text: !lambda |-
|
||
auto now = id(ha_time).now();
|
||
if (!now.is_valid())
|
||
return std::string("--:--:--");
|
||
return now.strftime("%H:%M:%S");
|
||
- interval: 100ms
|
||
then:
|
||
- if:
|
||
condition:
|
||
and:
|
||
- lambda: return id(sw_running);
|
||
- lvgl.page.is_showing: stopwatch_page
|
||
then:
|
||
- script.execute: update_stopwatch
|
||
- interval: 2s
|
||
then:
|
||
- if:
|
||
condition:
|
||
lvgl.page.is_showing: status_page
|
||
then:
|
||
- script.execute: update_status
|
||
|
||
image:
|
||
- platform: file
|
||
file: assets/images/made-for-esphome-white-on-black.svg
|
||
id: esphome_logo
|
||
resize: 280x83
|
||
type: RGB565
|
||
transparency: alpha_channel
|
||
|
||
font:
|
||
- file: "gfonts://Roboto@500"
|
||
id: font_digits
|
||
size: 110
|
||
glyphs: "0123456789:.-"
|
||
- file: "gfonts://Roboto"
|
||
id: font_ui
|
||
size: 24
|
||
glyphs: &sk_glyphs "ÁÄČĎÉÍĹĽŇÓÔŔŠŤÚÝŽáäčďéíĺľňóôŕšťúýž°·"
|
||
glyphsets: [GF_Latin_Kernel]
|
||
- file: "gfonts://Roboto@500"
|
||
id: font_button
|
||
size: 34
|
||
glyphs: *sk_glyphs
|
||
glyphsets: [GF_Latin_Kernel]
|
||
- file: "gfonts://Roboto"
|
||
id: font_small
|
||
size: 18
|
||
glyphs: *sk_glyphs
|
||
glyphsets: [GF_Latin_Kernel]
|
||
|
||
lvgl:
|
||
rotation: 90
|
||
buffer_size: 100%
|
||
log_level: WARN
|
||
color_depth: 16
|
||
default_font: font_ui
|
||
bg_color: 0x000000
|
||
text_color: 0xFFFFFF
|
||
theme:
|
||
button:
|
||
bg_color: 0x2B2F36
|
||
radius: 12
|
||
checked:
|
||
bg_color: 0xE08A00
|
||
buttonmatrix:
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
items:
|
||
bg_color: 0x2B2F36
|
||
text_color: 0xFFFFFF
|
||
radius: 12
|
||
checked:
|
||
bg_color: 0xE08A00
|
||
slider:
|
||
bg_color: 0x2B2F36
|
||
indicator:
|
||
bg_color: 0xE08A00
|
||
knob:
|
||
bg_color: 0xFFFFFF
|
||
top_layer:
|
||
widgets:
|
||
- buttonmatrix:
|
||
id: nav_bar
|
||
align: bottom_mid
|
||
width: 100%
|
||
height: 56
|
||
pad_all: 4
|
||
pad_column: 6
|
||
items:
|
||
text_font: montserrat_28
|
||
rows:
|
||
- buttons:
|
||
- id: page_prev
|
||
text: ""
|
||
on_press:
|
||
then:
|
||
lvgl.page.previous:
|
||
- id: page_home
|
||
text: ""
|
||
on_press:
|
||
then:
|
||
lvgl.page.show: clock_page
|
||
- id: page_next
|
||
text: ""
|
||
on_press:
|
||
then:
|
||
lvgl.page.next:
|
||
pages:
|
||
# 1: clock (default after boot)
|
||
- id: clock_page
|
||
widgets:
|
||
- label:
|
||
id: clock_label
|
||
align: CENTER
|
||
y: -28
|
||
text_font: font_digits
|
||
text: "--:--:--"
|
||
|
||
# 2: basement lights
|
||
- id: lights_page
|
||
pad_all: 10
|
||
widgets:
|
||
- button:
|
||
id: basement_light_btn
|
||
align: TOP_LEFT
|
||
width: 148
|
||
height: 80
|
||
checkable: true
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "Pivnica"
|
||
on_click:
|
||
- homeassistant.action:
|
||
action: light.toggle
|
||
data:
|
||
entity_id: ${light_basement}
|
||
- button:
|
||
id: workbench_light_btn
|
||
align: TOP_MID
|
||
width: 148
|
||
height: 80
|
||
checkable: true
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "Pracovný stôl"
|
||
on_click:
|
||
- homeassistant.action:
|
||
action: light.toggle
|
||
data:
|
||
entity_id: ${light_workbench}
|
||
- button:
|
||
id: exercise_light_btn
|
||
align: TOP_RIGHT
|
||
width: 148
|
||
height: 80
|
||
checkable: true
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "Posilka"
|
||
on_click:
|
||
- homeassistant.action:
|
||
action: light.toggle
|
||
data:
|
||
entity_id: ${light_exercise}
|
||
- obj:
|
||
id: exercise_controls
|
||
hidden: true
|
||
y: 110
|
||
width: 460
|
||
height: 120
|
||
pad_all: 0
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
scrollbar_mode: "OFF"
|
||
widgets:
|
||
- label:
|
||
x: 4
|
||
y: 0
|
||
text_font: font_small
|
||
text: "Posilka jas"
|
||
- slider:
|
||
id: exercise_brightness_slider
|
||
x: 14
|
||
y: 32
|
||
width: 430
|
||
height: 16
|
||
min_value: 1
|
||
max_value: 100
|
||
value: 100
|
||
on_release:
|
||
- homeassistant.action:
|
||
action: light.turn_on
|
||
data:
|
||
entity_id: ${light_exercise}
|
||
brightness_pct: !lambda return (int) x;
|
||
- label:
|
||
x: 4
|
||
y: 62
|
||
text_font: font_small
|
||
text: "Posilka teplota (2700 – 6500 K)"
|
||
- slider:
|
||
id: exercise_color_temp_slider
|
||
x: 14
|
||
y: 94
|
||
width: 430
|
||
height: 16
|
||
min_value: 2700
|
||
max_value: 6500
|
||
value: 2700
|
||
on_release:
|
||
- homeassistant.action:
|
||
action: light.turn_on
|
||
data:
|
||
entity_id: ${light_exercise}
|
||
color_temp_kelvin: !lambda return (int) x;
|
||
|
||
# 3: training start / stop
|
||
- id: training_page
|
||
pad_all: 10
|
||
widgets:
|
||
- button:
|
||
align: TOP_LEFT
|
||
y: 10
|
||
width: 222
|
||
height: 150
|
||
bg_color: 0x1E7D32
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text_font: font_button
|
||
text: "Štart"
|
||
on_click:
|
||
- homeassistant.action:
|
||
action: automation.trigger
|
||
data:
|
||
entity_id: automation.zwift_enable_fan
|
||
- button:
|
||
align: TOP_RIGHT
|
||
y: 10
|
||
width: 222
|
||
height: 150
|
||
bg_color: 0xB71C1C
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text_font: font_button
|
||
text: "Stop"
|
||
on_click:
|
||
- homeassistant.action:
|
||
action: automation.trigger
|
||
data:
|
||
entity_id: automation.zwift_disable_fan
|
||
- label:
|
||
id: training_status_label
|
||
align: TOP_MID
|
||
y: 185
|
||
width: 440
|
||
text_align: CENTER
|
||
text_font: font_small
|
||
text: ""
|
||
|
||
# 4: fan
|
||
- id: fan_page
|
||
pad_all: 10
|
||
widgets:
|
||
- buttonmatrix:
|
||
id: fan_matrix
|
||
align: TOP_MID
|
||
width: 460
|
||
height: 180
|
||
pad_row: 8
|
||
pad_column: 8
|
||
one_checked: true
|
||
items:
|
||
text_font: font_button
|
||
rows:
|
||
- buttons:
|
||
- text: "Vyp"
|
||
control:
|
||
checkable: true
|
||
on_press:
|
||
- homeassistant.action:
|
||
action: light.turn_off
|
||
data:
|
||
entity_id: ${fan}
|
||
- text: "50"
|
||
control:
|
||
checkable: true
|
||
on_press:
|
||
- homeassistant.action:
|
||
action: light.turn_on
|
||
data:
|
||
entity_id: ${fan}
|
||
brightness_pct: "50"
|
||
- text: "60"
|
||
control:
|
||
checkable: true
|
||
on_press:
|
||
- homeassistant.action:
|
||
action: light.turn_on
|
||
data:
|
||
entity_id: ${fan}
|
||
brightness_pct: "60"
|
||
- text: "70"
|
||
control:
|
||
checkable: true
|
||
on_press:
|
||
- homeassistant.action:
|
||
action: light.turn_on
|
||
data:
|
||
entity_id: ${fan}
|
||
brightness_pct: "70"
|
||
- buttons:
|
||
- text: "80"
|
||
control:
|
||
checkable: true
|
||
on_press:
|
||
- homeassistant.action:
|
||
action: light.turn_on
|
||
data:
|
||
entity_id: ${fan}
|
||
brightness_pct: "80"
|
||
- text: "90"
|
||
control:
|
||
checkable: true
|
||
on_press:
|
||
- homeassistant.action:
|
||
action: light.turn_on
|
||
data:
|
||
entity_id: ${fan}
|
||
brightness_pct: "90"
|
||
- text: "100"
|
||
control:
|
||
checkable: true
|
||
on_press:
|
||
- homeassistant.action:
|
||
action: light.turn_on
|
||
data:
|
||
entity_id: ${fan}
|
||
brightness_pct: "100"
|
||
- label:
|
||
id: fan_status_label
|
||
align: TOP_MID
|
||
y: 200
|
||
text_font: font_small
|
||
text: ""
|
||
|
||
# 5: stopwatch
|
||
- id: stopwatch_page
|
||
pad_all: 10
|
||
widgets:
|
||
- label:
|
||
id: stopwatch_label
|
||
align: TOP_MID
|
||
y: 10
|
||
text_font: font_digits
|
||
text: "00:00.0"
|
||
- button:
|
||
align: TOP_LEFT
|
||
y: 150
|
||
width: 222
|
||
height: 90
|
||
widgets:
|
||
- label:
|
||
id: stopwatch_start_label
|
||
align: CENTER
|
||
text_font: font_button
|
||
text: "Štart"
|
||
on_click:
|
||
- lambda: |-
|
||
if (id(sw_running)) {
|
||
id(sw_accum) += millis() - id(sw_start);
|
||
id(sw_running) = false;
|
||
} else {
|
||
id(sw_start) = millis();
|
||
id(sw_running) = true;
|
||
}
|
||
- script.execute: update_stopwatch
|
||
- button:
|
||
align: TOP_RIGHT
|
||
y: 150
|
||
width: 222
|
||
height: 90
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text_font: font_button
|
||
text: "Reset"
|
||
on_click:
|
||
- lambda: |-
|
||
id(sw_accum) = 0;
|
||
id(sw_start) = millis();
|
||
- script.execute: update_stopwatch
|
||
|
||
# 6: status
|
||
- id: status_page
|
||
on_load:
|
||
- script.execute: update_status
|
||
widgets:
|
||
- image:
|
||
src: esphome_logo
|
||
align: TOP_MID
|
||
y: 8
|
||
- label:
|
||
align: TOP_MID
|
||
y: 96
|
||
text: "${name}"
|
||
- obj:
|
||
id: status_lines
|
||
x: 20
|
||
y: 126
|
||
width: 440
|
||
height: SIZE_CONTENT
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
pad_all: 0
|
||
scrollbar_mode: "OFF"
|
||
layout:
|
||
type: flex
|
||
flex_flow: column
|
||
pad_row: 4
|
||
widgets:
|
||
- label:
|
||
id: status_wifi
|
||
text_font: font_small
|
||
text: ""
|
||
- label:
|
||
id: status_ips
|
||
text_font: font_small
|
||
text_color: 0x8C8C8C
|
||
pad_left: 16
|
||
text: ""
|
||
- label:
|
||
id: status_ha
|
||
text_font: font_small
|
||
text: ""
|
||
- label:
|
||
id: status_footer
|
||
text_font: font_small
|
||
text_color: 0x8C8C8C
|
||
text: ""
|