跳转到内容

LVGL:技巧与窍门

以下是几个有趣的食谱,展示你可以用 ESPHome 中的 Lvgl 实现的各种效果。

NOTE

以下许多示例都会在 Home Assistant 中调用服务动作;然而,Home Assistant 默认不允许此类动作 调用。对于每个会调用动作的 ESPHome 设备,你必须在 Home Assistant 中显式启用此设置。可以在设备初次被采用时设置,也可以通过 ESPHome 集成中“devices” 列表里的 Configure 选项来设置。

NOTE

以下示例假定你已经使用显示器及其输入设备正确配置了 LVGL,并且你有 在 ESPHome 中设置各种组件的知识。部分示例使用绝对定位,适用于 240x320px 尺寸的屏幕 ;如果你的显示屏尺寸不同,则需要调整以获得预期效果。

将 LVGL switch 控件与一个开关或灯光集成的最简单方式是使用 自动化:

light:
- platform: ...
id: local_light
name: 'Local light'
on_state:
- lvgl.widget.update:
id: light_switch
state:
checked: !lambda return id(local_light).current_values.is_on();
lvgl:
...
pages:
- id: main_page
widgets:
- switch:
align: CENTER
id: light_switch
on_click:
light.toggle: local_light

如果你想从一个可切换(复选)的 button 控制一个在 Home Assistant 中作为实体出现的远程灯光, 首先需要将灯光状态导入 ESPHome,然后通过动作调用来控制它:

binary_sensor:
- platform: homeassistant
id: remote_light
entity_id: light.remote_light
publish_initial_state: true
on_state:
then:
lvgl.widget.update:
id: light_btn
state:
checked: !lambda return x;
lvgl:
...
pages:
- id: room_page
widgets:
- button:
id: light_btn
align: CENTER
width: 100
height: 70
checkable: true
widgets:
- label:
align: CENTER
text: 'Remote light'
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.remote_light

你可以使用 slider 或 arc 来控制可调光灯光的亮度。

我们可以使用一个传感器来读取灯光的当前亮度,它在 Home Assistant 中作为实体的属性,以整数形式存储,取值范围在 0(最小值)和 255(最大值)之间。相应地设置滑块的 min_value 和 max_value 会很方便。

sensor:
- platform: homeassistant
id: light_brightness
entity_id: light.your_dimmer
attribute: brightness
on_value:
- lvgl.slider.update:
id: dimmer_slider
value: !lambda return x;
lvgl:
...
pages:
- id: room_page
widgets:
- slider:
id: dimmer_slider
x: 20
y: 50
width: 20
height: 220
pad_all: 8
min_value: 0
max_value: 255
ext_click_area: 20
on_release:
- homeassistant.action:
action: light.turn_on
data:
entity_id: light.your_dimmer
brightness: !lambda return int(x);

ext_click_area 选项用于将控件周围的触摸区域放大 20 像素。

NOTE

Home Assistant 在 light.turn_on 动作调用的 brightness 参数处期望一个整数,而 ESPHome 使用浮点数,因此 x 需要进行转换。

这也适用于 fan.set_percentage 或 valve.set_valve_position 之类的动作调用;唯一区别是 max_value 必须为 100。

类似地,你可以使用 slider 或 arc 来控制 媒体播放器的音量级别,后者使用的是浮点数值。

通过一个传感器,我们读取媒体播放器的当前音量级别,它在 Home Assistant 中作为实体的属性,是一个介于 0(最小值)和 1(最大值)之间的浮点值。由于 LVGL 只处理整数, 因此将滑块的可选值设为 0 到 100 会更方便。因此需要在两者之间进行转换,即从 Home Assistant 读取值时要乘以 100,通过动作调用设置音量时要除以 100 :

sensor:
- platform: homeassistant
id: media_player_volume
entity_id: media_player.your_room
attribute: volume_level
on_value:
- lvgl.slider.update:
id: slider_media_player
value: !lambda return (x * 100);
lvgl:
...
pages:
- id: mediapplayer_page
widgets:
- slider:
id: slider_media_player
x: 60
y: 50
width: 30
height: 220
pad_all: 8
min_value: 0
max_value: 100
adv_hittest: true
on_value:
- homeassistant.action:
action: media_player.volume_set
data:
entity_id: media_player.your_room
volume_level: !lambda return (x / 100);

adv_hittest 选项确保对屏幕的意外触碰不会导致音量突然变化(更多细节 参见 slider 文档)。

NOTE

请注意,on_value 会在滑块拖动过程中被持续触发。这通常会对性能 产生负面影响。例如,你不应该用这个触发器通过 Modbus 设置热泵的目标温度, 或设置电动帘盖的位置,因为很可能导致故障。为缓解此问题, 可以考虑使用像 on_release 这样的通用控件触发器,在交互完成后一次性获取 x 变量。

一个与 Home Assistant 在能量仪表盘中显示的类似的仪表,可以通过 meter 和 label 控件实现:

这里的技巧是有一个父 obj,其中包含其他控件作为子控件。我们在中间 放置一个 meter,它由一个 line 指示器和两个 arc 控件组成。我们使用 另一个更小的 obj 覆盖在上方,以隐藏指示器的中心部分,并放置一些 label 控件来显示数字信息:

sensor:
- platform: ...
id: values_between_-10_and_10
on_value:
- lvgl.indicator.update:
id: val_needle
value: !lambda return x;
- lvgl.label.update:
id: val_text
text:
format: "%.0f"
args: [ 'x' ]
lvgl:
...
pages:
- id: gauge_page
widgets:
- obj:
height: 240
width: 240
align: CENTER
bg_color: 0xFFFFFF
border_width: 0
pad_all: 4
widgets:
- meter:
height: 100%
width: 100%
border_width: 0
bg_opa: TRANSP
indicator:
bg_opa: TRANSP
align: CENTER
scales:
- range_from: -10
range_to: 10
angle_range: 180 # 将总角度设置为 180 = 从中左开始到中右结束
indicators:
- arc: # 刻度背景的前半部分
color: 0xFF3000
width: 31
start_value: -10
end_value: 0
- arc: # 刻度背景的后半部分
color: 0x00FF00
width: 31
start_value: 0
end_value: 10
- line: # 指针
id: val_needle
width: 8
value: -3
rounded: false
length: 33
radial_offset: 73
- label: # 仪表数字指示器
id: val_text
text_font: montserrat_48
align: CENTER
y: -5
text: "-3"
- label: # 下范围指示器
text_font: montserrat_18
align: CENTER
y: 8
x: -90
text: "-10"
- label: # 上范围指示器
text_font: montserrat_18
align: CENTER
y: 8
x: 90
text: "+10"

一个带有精密仪表的温度计,同样由 meter 控件和一个使用 label 的数字显示组成:

每当传感器发来新值时,我们都会更新指针指示器以及 label 中的文本。由于 LVGL 只在 meter 的刻度上处理整数值, 但传感器的值是一个 float,我们采用与上述示例相同的方法;将传感器的 值乘以 10 并送入 meter。这本质上是上下叠放的两个刻度:一个根据 乘积后的值设置指针,另一个在 label 中显示传感器的原始值。

sensor:
- platform: ...
id: outdoor_temperature
on_value:
- lvgl.indicator.update:
id: temperature_needle
value: !lambda return x * 10;
- lvgl.label.update:
id: temperature_text
text:
format: "%.1f°C"
args: [ 'x' ]
lvgl:
...
pages:
- id: meter_page
widgets:
- meter:
align: CENTER
height: 180
width: 180
scales:
- range_from: -10 # 值标签的刻度
range_to: 40
angle_range: 240
rotation: 150
ticks:
width: 1
count: 51
length: 10
color: 0x000000
major:
stride: 5
width: 2
length: 10
color: 0x404040
label_gap: 10
indicators:
- tick_style:
start_value: -10
end_value: 40
color_start: 0x0000bd
color_end: 0xbd0000
width: 1
- range_from: -100 # 指针值的刻度
range_to: 400
angle_range: 240
rotation: 150
indicators:
- line:
id: temperature_needle
width: 2
color: 0xFF0000
length: 96%
widgets:
- label:
id: temperature_text
text: "-.-°C"
align: CENTER
y: 45
- label:
text: "Outdoor"
align: CENTER
y: 65

以下是同样的传感器配置,但改为带有渐变背景的半圆仪表,通过大量刻度绘制:

如果你改变控件的大小,为了获得均匀的渐变,请相应地增加或减少刻度数量。

lvgl:
...
pages:
- id: meter_page
widgets:
- obj:
height: 240
width: 240
align: CENTER
y: -18
bg_color: 0xFFFFFF
border_width: 0
pad_all: 14
widgets:
- meter:
height: 100%
width: 100%
border_width: 0
align: CENTER
bg_opa: TRANSP
indicator:
bg_opa: TRANSP
scales:
- range_from: -15
range_to: 35
angle_range: 180
ticks:
count: 100
width: 10
length: 30
radial_offset: 2
indicators:
- tick_style:
start_value: -15
end_value: 35
color_start: 0x3399ff
color_end: 0xffcc66
- range_from: -150
range_to: 350
angle_range: 180
indicators:
- line:
id: temperature_needle2
width: 8
value: -150
rounded: false
length: 32
radial_offset: 61
- label:
id: temperature_text
text: "--.-°C"
align: CENTER
y: -26
- label:
text: "Outdoor"
align: CENTER
y: -6

spinbox 是控制恒温器的理想控件:

首先我们从 Home Assistant 导入气候组件的当前目标温度,并在它每次变化时用它的值更新 spinbox。我们使用两个带有减号和加号的按钮来控制 spinbox,每当改变其值时,我们只需 简单地调用一个 Home Assistant 动作来设置气候组件的新目标温度。

sensor:
- platform: homeassistant
id: room_thermostat
entity_id: climate.room_thermostat
attribute: temperature
on_value:
- lvgl.spinbox.update:
id: spinbox_id
value: !lambda return x;
lvgl:
...
pages:
- id: thermostat_control
widgets:
- obj:
align: BOTTOM_MID
y: -50
layout:
type: FLEX
flex_flow: ROW
flex_align_cross: CENTER
width: SIZE_CONTENT
height: SIZE_CONTENT
widgets:
- button:
id: spin_down
on_click:
- lvgl.spinbox.decrement: spinbox_id
widgets:
- label:
text: "-"
- spinbox:
id: spinbox_id
align: CENTER
text_align: CENTER
width: 50
range_from: 15
range_to: 35
selected_digit: 0
rollover: false
digits: 3
decimal_places: 1
on_value:
then:
- homeassistant.action:
action: climate.set_temperature
data:
temperature: !lambda return x;
entity_id: climate.room_thermostat
- button:
id: spin_up
on_click:
- lvgl.spinbox.increment: spinbox_id
widgets:
- label:
text: "+"

要为控制 Home Assistant 帘盖制作一个漂亮的用户界面,你可以使用 3 个按钮,它们同时还会 显示状态。

与前面的示例一样,我们首先需要获取帘盖的状态。我们将使用一个数值传感器来读取 帘盖的当前位置,并使用一个文本传感器来读取其当前运动状态。我们特别关注 运动状态(opening 和 closing),因为在运动期间我们希望将中间的标签更改为 显示 STOP。否则,该按钮标签将显示开启的百分比。此外,根据帘盖是完全打开还是完全关闭, 我们将更改 UP 和 DOWN 按钮上标签的不透明度。

sensor:
- platform: homeassistant
id: cover_myroom_pos
entity_id: cover.myroom
attribute: current_position
on_value:
- if:
condition:
lambda: |-
return x == 100;
then:
- lvgl.widget.update:
id: cov_up_myroom
text_opa: 60%
else:
- lvgl.widget.update:
id: cov_up_myroom
text_opa: 100%
- if:
condition:
lambda: |-
return x == 0;
then:
- lvgl.widget.update:
id: cov_down_myroom
text_opa: 60%
else:
- lvgl.widget.update:
id: cov_down_myroom
text_opa: 100%
text_sensor:
- platform: homeassistant
id: cover_myroom_state
entity_id: cover.myroom
on_value:
- if:
condition:
lambda: |-
return (x == "opening" or x == "closing");
then:
- lvgl.label.update:
id: cov_stop_myroom
text: "STOP"
else:
- lvgl.label.update:
id: cov_stop_myroom
text:
format: "%.0f%%"
args: [ 'id(cover_myroom_pos).get_state()' ]
lvgl:
...
pages:
- id: room_page
widgets:
- label:
x: 10
y: 6
width: 70
text: "My room"
text_align: CENTER
- button:
x: 10
y: 30
width: 70
height: 68
widgets:
- label:
id: cov_up_myroom
align: CENTER
text: "\uF077"
on_press:
then:
- homeassistant.action:
action: cover.open
data:
entity_id: cover.myroom
- button:
x: 10
y: 103
width: 70
height: 68
widgets:
- label:
id: cov_stop_myroom
align: CENTER
text: STOP
on_press:
then:
- homeassistant.action:
action: cover.stop
data:
entity_id: cover.myroom
- button:
x: 10
y: 178
width: 70
height: 68
widgets:
- label:
id: cov_down_myroom
align: CENTER
text: "\uF078"
on_press:
then:
- homeassistant.action:
action: cover.close
data:
entity_id: cover.myroom

由于 LVGL 使用继承来在控件之间应用样式,因此可以在顶层应用它们,并且只在需要时 进行修改。

在此示例中,我们在 theme 中准备了一组渐变样式,并在 style_definition 中 做一些修改,可以批量应用到所需控件上。主题会自动应用,并且可以通过样式定义手动 覆盖(继续往下阅读以了解方法)。

lvgl:
...
theme:
label:
text_font: my_font # 将所有标签设置为使用你自定义的字体
button:
bg_color: 0x2F8CD8
bg_grad_color: 0x005782
bg_grad_dir: VER
bg_opa: COVER
border_color: 0x0077b3
border_width: 1
text_color: 0xFFFFFF
pressed: # 设置一些按钮在按下状态下的颜色不同
bg_color: 0x006699
bg_grad_color: 0x00334d
checked: # 设置一些按钮在选中状态下的颜色不同
bg_color: 0x1d5f96
bg_grad_color: 0x03324A
text_color: 0xfff300
buttonmatrix:
bg_opa: TRANSP
border_color: 0x0077b3
border_width: 0
text_color: 0xFFFFFF
pad_all: 0
items: # 将所有 buttonmatrix 按钮设置为使用你自定义的样式和字体
bg_color: 0x2F8CD8
bg_grad_color: 0x005782
bg_grad_dir: VER
bg_opa: COVER
border_color: 0x0077b3
border_width: 1
text_color: 0xFFFFFF
text_font: my_font
pressed:
bg_color: 0x006699
bg_grad_color: 0x00334d
checked:
bg_color: 0x1d5f96
bg_grad_color: 0x03324A
text_color: 0x005580
switch:
bg_color: 0xC0C0C0
bg_grad_color: 0xb0b0b0
bg_grad_dir: VER
bg_opa: COVER
checked:
bg_color: 0x1d5f96
bg_grad_color: 0x03324A
bg_grad_dir: VER
bg_opa: COVER
knob:
bg_color: 0xFFFFFF
bg_grad_color: 0xC0C0C0
bg_grad_dir: VER
bg_opa: COVER
slider:
border_width: 1
border_opa: 15%
bg_color: 0xcccaca
bg_opa: 15%
indicator:
bg_color: 0x1d5f96
bg_grad_color: 0x03324A
bg_grad_dir: VER
bg_opa: COVER
knob:
bg_color: 0x2F8CD8
bg_grad_color: 0x005782
bg_grad_dir: VER
bg_opa: COVER
border_color: 0x0077b3
border_width: 1
text_color: 0xFFFFFF
style_definitions:
- id: header_footer
bg_color: 0x2F8CD8
bg_grad_color: 0x005782
bg_grad_dir: VER
bg_opa: COVER
border_opa: TRANSP
radius: 0
pad_all: 0
pad_row: 0
pad_column: 0
border_color: 0x0077b3
text_color: 0xFFFFFF
width: 100%
height: 30

请注意,样式定义也可以包含通用属性,例如定位和尺寸。

如果使用多个页面,那么在屏幕底部有一个导航栏会很有用:

为了避免在每个页面上重复相同的控件,有 top_layer,它是位于所有页面上方的 Always on Top 透明页面。你将所有内容放在这个页面上,它就会位于所有其他页面的上方。

对于导航栏,我们可以使用 buttonmatrix。注意 header_footer 样式 定义是如何应用到控件及其子对象上的,以及在主控件上是如何手动配置一些更多样式的:

lvgl:
...
top_layer:
widgets:
- buttonmatrix:
align: bottom_mid
styles: header_footer
pad_all: 0
outline_width: 0
id: top_layer
items:
styles: header_footer
rows:
- buttons:
- id: page_prev
text: "\uF053"
on_press:
then:
lvgl.page.previous:
- id: page_home
text: "\uF015"
on_press:
then:
lvgl.page.show: main_page
- id: page_next
text: "\uF054"
on_press:
then:
lvgl.page.next:

为了让此示例正确显示,请使用 上文 的主题和样式选项,以及 LVGL 自身的 库 字体。

顶层显示在所有页面上可见的状态图标很有用:

在下面的示例中,我们仅在建立与 Home Assistant 的连接时显示图标:

api:
on_client_connected:
- wait_until:
condition:
api.connected:
state_subscription_only: true
- lvgl.widget.show: lbl_hastatus
on_client_disconnected:
- if:
condition:
lambda: 'return (0 == client_info.find("Home Assistant "));'
then:
- lvgl.widget.hide: lbl_hastatus
lvgl:
...
top_layer:
widgets:
- label:
text: "\uF1EB"
id: lbl_hastatus
hidden: true
align: top_right
x: -2
y: 7
text_align: right
text_color: 0xFFFFFF

NOTE

  • 控件在启动时是隐藏的,只有在由与 API 的连接触发时才会显示。
  • 控件的对齐方式:由于提供了 align 选项,x 和 y 选项用于相对于计算出的位置来定位控件。

每个页面都可以有自己的标题栏:

为了在状态图标后面放置标题栏,我们需要将它添加到每个页面中,同时包含一个带有唯一 标题的标签:

lvgl:
...
pages:
- id: main_page
widgets:
- obj:
align: TOP_MID
styles: header_footer
widgets:
- label:
text: "ESPHome LVGL Display"
align: CENTER
text_align: CENTER
text_color: 0xFFFFFF
...
- id: second_page
widgets:
- obj:
align: TOP_MID
styles: header_footer
widgets:
- label:
text: "A second page"
align: CENTER
text_align: CENTER
text_color: 0xFFFFFF
...

为了让此示例工作,请使用 上文 的主题和样式选项。

布局 旨在自动定位控件,无需指定坐标来定位 每个控件。这是简化包含许多控件的配置的好方法,因为你可以省略 对齐选项。

此示例展示了一个三个帘盖的控制面板,由标签和独立按钮组成。虽然按钮矩阵 也可能适合,但你可能仍倾向于使用功能齐全的独立按钮,因为它们提供了更广泛的 自定义选项,如 帘盖状态与控制 示例中所示。这里我们 使用 Flex 布局:

lvgl:
...
pages:
- id: room_page
widgets:
- obj: # 用于所有控件的正确放置的容器对象
align: CENTER
width: 240
height: 256
x: 4
y: 4
pad_all: 3
bg_opa: TRANSP
border_opa: TRANSP
layout: # 为子控件启用 FLEX 布局
type: FLEX
flex_flow: COLUMN_WRAP # 控件顺序从左上角开始
flex_align_cross: CENTER # 它们应该居中
pad_row: 6
pad_column: 8
widgets:
- label:
text: "East"
- button:
id: but_cov_up_east
width: 70 # 选择按钮尺寸,使它们
height: 68 # 在流动时很好地填充列
widgets:
- label:
id: cov_up_east
align: CENTER
text: "\uF077"
text_font: montserrat_48
- button:
id: but_cov_stop_east
width: 70
height: 68
widgets:
- label:
id: cov_stop_east
align: CENTER
text: "\uF04D"
text_font: montserrat_30
- button:
id: but_cov_down_east
width: 70
height: 68
widgets:
- label:
id: cov_down_east
align: CENTER
text: "\uF078"
text_font: montserrat_48
- label:
text: "South"
- button:
id: but_cov_up_south
width: 70
height: 68
widgets:
- label:
id: cov_up_south
align: CENTER
text: "\uF077"
text_font: montserrat_48
- button:
id: but_cov_stop_south
width: 70
height: 68
widgets:
- label:
id: cov_stop_south
align: CENTER
text: "\uF04D"
text_font: montserrat_30
- button:
id: but_cov_down_south
width: 70
height: 68
widgets:
- label:
id: cov_down_south
align: CENTER
text: "\uF078"
text_font: montserrat_48
- label:
text: "West"
- button:
id: but_cov_up_west
width: 70
height: 68
widgets:
- label:
id: cov_up_west
align: CENTER
text: "\uF077"
text_font: montserrat_48
- button:
id: but_cov_stop_west
width: 70
height: 68
widgets:
- label:
id: cov_stop_west
align: CENTER
text: "\uF04D"
text_font: montserrat_30
- button:
id: but_cov_down_west
width: 70
height: 68
widgets:
- label:
id: cov_down_west
align: CENTER
text: "\uF078"
text_font: montserrat_48

这让你省去大量控件定位的手动计算,否则需要依靠 x 和 y 手动放置它们!你只需要为控件确定一个公共的宽度和高度, 然后按你喜欢的分布它们。(文本中的 MDI 图标 展示了如何使用自定义 图标。)

但还有更多!使用 Grid 布局,你不需要为控件指定宽度和高度。你 只需要将空间划分为行和列;控件可以自动调整大小以适应由这些行和列定义的单元格。 同样的任务,如果使用完全自动化的网格,看起来是这样的:

lvgl:
...
pages:
- id: room_page
widgets:
- obj: # 用于所有控件的正确放置的容器对象
align: CENTER
width: 240
height: 256
pad_all: 6
bg_opa: TRANSP
border_opa: TRANSP
layout: # 为子控件启用 GRID 布局
type: GRID # 按比例划分行和列
grid_columns: [FR(1), FR(1), FR(1)] # 等分
grid_rows: [FR(10), FR(30), FR(30), FR(30)] # 类似百分比
pad_row: 6
pad_column: 8
widgets:
- label:
text: "East"
grid_cell_column_pos: 0 # 将控件放置在
grid_cell_row_pos: 0 # 相应的单元格中
grid_cell_x_align: CENTER
grid_cell_y_align: STRETCH
- button:
id: but_cov_up_east
grid_cell_column_pos: 0
grid_cell_row_pos: 1
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
widgets:
- label:
id: cov_up_east
align: CENTER
text: "\uF077"
text_font: montserrat_48
- button:
id: but_cov_stop_east
grid_cell_column_pos: 0
grid_cell_row_pos: 2
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
widgets:
- label:
id: cov_stop_east
align: CENTER
text: "\uF04D"
text_font: montserrat_30
- button:
id: but_cov_down_east
grid_cell_column_pos: 0
grid_cell_row_pos: 3
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
widgets:
- label:
id: cov_down_east
align: CENTER
text: "\uF078"
text_font: montserrat_48
- label:
text: "South"
grid_cell_column_pos: 1
grid_cell_row_pos: 0
grid_cell_x_align: CENTER
grid_cell_y_align: STRETCH
- button:
id: but_cov_up_south
grid_cell_column_pos: 1
grid_cell_row_pos: 1
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
widgets:
- label:
id: cov_up_south
align: CENTER
text: "\uF077"
text_font: montserrat_48
- button:
id: but_cov_stop_south
grid_cell_column_pos: 1
grid_cell_row_pos: 2
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
widgets:
- label:
id: cov_stop_south
align: CENTER
text: "\uF04D"
text_font: montserrat_30
- button:
id: but_cov_down_south
grid_cell_column_pos: 1
grid_cell_row_pos: 3
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
widgets:
- label:
id: cov_down_south
align: CENTER
text: "\uF078"
text_font: montserrat_48
- label:
text: "West"
grid_cell_column_pos: 2
grid_cell_row_pos: 0
grid_cell_x_align: CENTER
grid_cell_y_align: STRETCH
- button:
id: but_cov_up_west
grid_cell_column_pos: 2
grid_cell_row_pos: 1
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
widgets:
- label:
id: cov_up_west
align: CENTER
text: "\uF077"
text_font: montserrat_48
- button:
id: but_cov_stop_west
grid_cell_column_pos: 2
grid_cell_row_pos: 2
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
widgets:
- label:
id: cov_stop_west
align: CENTER
text: "\uF04D"
text_font: montserrat_30
- button:
id: but_cov_down_west
grid_cell_column_pos: 2
grid_cell_row_pos: 3
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
widgets:
- label:
id: cov_down_west
align: CENTER
text: "\uF078"
text_font: montserrat_48

这里的主要优势是,每当你需要添加例如用于新帘盖的一列按钮时,你 只需简单地将其追加到 grid_columns 变量,并像上面一样添加相应的控件。借助 STRETCH,它们的 大小和位置会自动计算以填充单元格,而父对象的 pad_all、pad_row 和 pad_column 可以帮助它们在之间调整间距。请参见下方的 天气预报面板 以获取另一个依赖于 Grid 的示例。

要显示一个带有旋转动画的启动图像,并在几秒后自动消失或触摸 屏幕时消失,你可以使用 top layer。技巧是放置一个全屏的基础 obj 并将其中间的 image 控件作为控件列表的最后一项,这样它们就会绘制在所有其他控件之上。要让它在启动后自动消失,你可以使用 ESPHome 的 on_boot 触发器:

esphome:
...
on_boot:
- delay: 5s
- lvgl.widget.hide: boot_screen
image:
- platform: file
file: https://media.esphome.io/logo/logo.png
id: boot_logo
resize: 200x200
type: RGB565
transparency: alpha_channel
lvgl:
...
top_layer:
widgets:
... # 确保它是此列表中的最后一项:
- obj:
id: boot_screen
x: 0
y: 0
width: 100%
height: 100%
bg_color: 0xffffff
bg_opa: COVER
radius: 0
pad_all: 0
border_width: 0
widgets:
- image:
align: CENTER
src: boot_logo
y: -50
- spinner:
align: CENTER
y: 50
height: 50
width: 50
spin_time: 1s
arc_length: 60deg
arc_width: 8
indicator:
arc_color: 0x18bcf2
arc_width: 8
on_press:
- lvgl.widget.hide: boot_screen

ESPHome 的 字体渲染器 允许你为文本使用任何 OpenType/TrueType 字体文件。这非常 灵活,因为你可以准备不同大小、包含不同数量字形(glyphs)的多套字体;这很重要,因为它有助于 节省闪存空间。

一种情况是你希望一些 MDI 图标与文本内联使用(类似于 LVGL 内部字体 和符号共存的方式)。你可以使用任何你选择的字体;从 MDI 中选择想要的符号/图标,并将它们混合在 一个相同大小的集合中。

在下面的示例中,我们使用 RobotoCondensed-Regular 的默认字形集,并从 MDI 附加一些额外 符号。然后通过转义它们的代码点,将它们在文本中内联显示:

font:
- file: "fonts/RobotoCondensed-Regular.ttf"
id: roboto_icons_42
size: 42
bpp: 4
extras:
- file: "fonts/materialdesignicons-webfont.ttf"
glyphs: [
"\U000F02D1", # mdi-heart
"\U000F05D4", # mdi-airplane-landing
]
lvgl:
...
pages:
- id: main_page
widgets:
- label:
text: "Just\U000f05d4here. Already\U000F02D1this."
align: CENTER
text_align: CENTER
text_font: roboto_icons_42

TIP

按照这些步骤选择你的 MDI 图标:

  • 要查找你的图标,请使用 Pictogrammers 站点。点击想要的

  • 图标,并注意它的代码点(它是下载选项旁边的十六进制数字)。

  • 要获取包含所有图标的 TrueType 字体,请前往

  • Pictogrammers GitHub 仓库

  • 并从最近的版本文件夹中,下载 materialdesignicons-webfont.ttf 文件,并将其放在你的 ESPHome 配置目录中名为 fonts 的文件夹下(以匹配上面的示例)。

  • 要使用所需图标,请在复制的代码点前面加上 \U000。Unicode 字符转义序列必须

  • 以大写的 \U 开头,并恰好有 8 个十六进制数字。

  • 要将转义序列转换为真正的字形,请确保将字符串用双引号括起来。

如果你将自定义字体配置为 LVGL 使用的 default_font,它很可能不包含 FontAwesome 符号。你可能会观察到一些控件显示不正确; 具体来说 checkbox 在选中时不会显示对勾,而 dropdown 不会显示向下指向以打开列表的箭头。

要解决此问题,只需导入所需尺寸的对勾和箭头符号,并通过 主题与样式定义 将它们应用到配置中所有的复选框和下拉菜单:

font:
- file: 'fonts/FontAwesome5-Solid+Brands+Regular.woff'
id: fontawesome_checkmark
size: 16
bpp: 4
glyphs: [
"\uF00C", # 对勾,用于复选框
"\uF078", # 向下箭头,用于下拉菜单
]
lvgl:
...
theme:
checkbox:
indicator:
checked:
text_font: fontawesome_checkmark
dropdown:
indicator:
text_font: fontawesome_checkmark

当然你也可以直接应用内置的 montserrat_ 包之一,但这在 二进制大小上不会有益——它会在闪存中无意义地包含整个字形集。

使用图标的常见场景是状态显示。例如,一个可切换(复选)按钮会根据 灯光或开关的状态显示不同的图标。要在按钮上放置图标,你可以将一个 label 控件作为 button 的子控件。借助 主题与样式定义 中设置的 checked 状态的不同颜色, 着色已经可以不同。此外,通过使用 text_sensor 从 Home Assistant 导入状态,我们不仅可以追踪 on 状态, 还可以追踪 unavailable 或 unknown 状态,以便为这些情况应用禁用样式。

如果我们以之前的 远程灯光按钮 示例为基础,可以像这样修改:

font:
- file: "fonts/materialdesignicons-webfont.ttf"
id: mdi_42
size: 42
bpp: 4
glyphs: [
"\U000F0335", # mdi-lightbulb
"\U000F0336", # mdi-lightbulb-outline
]
text_sensor:
- platform: homeassistant
id: ts_remote_light
entity_id: light.remote_light
on_value:
then:
- lvgl.widget.update:
id: btn_lightbulb
state:
checked: !lambda return (x == "on");
disabled: !lambda return ((x == "unavailable") or (x == "unknown"));
- if:
condition:
lambda: return (x == "on");
then:
- lvgl.label.update:
id: lbl_lightbulb
text: "\U000F0335"
else:
- lvgl.label.update:
id: lbl_lightbulb
text: "\U000F0336"
lvgl:
...
pages:
- id: room_page
widgets:
- button:
align: CENTER
width: 90
height: 50
checkable: true
id: btn_lightbulb
widgets:
- label:
id: lbl_lightbulb
align: CENTER
text_font: mdi_42
text: "\U000F0336" # mdi-lightbulb-outline
on_short_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.remote_light

使用 MDI 图标的另一个示例是以 10 个等级显示电池百分比。我们需要一个包含 对应不同电池百分比等级字形的字体,并且需要一个传感器从 Home Assistant 导入电池状态 为一个数值。我们使用一个 lambda 根据传感器值返回相应 字形的代码点:

font:
- file: "fonts/materialdesignicons-webfont.ttf"
id: battery_icons_20
size: 20
bpp: 4
glyphs: [
"\U000F007A", # mdi-battery-10
"\U000F007B", # mdi-battery-20
"\U000F007C", # mdi-battery-30
"\U000F007D", # mdi-battery-40
"\U000F007E", # mdi-battery-50
"\U000F007F", # mdi-battery-60
"\U000F0080", # mdi-battery-70
"\U000F0081", # mdi-battery-80
"\U000F0082", # mdi-battery-90
"\U000F0079", # mdi-battery (full)
"\U000F008E", # mdi-battery-outline
"\U000F0091", # mdi-battery-unknown
]
sensor:
- platform: homeassistant
id: sns_battery_percentage
entity_id: sensor.device_battery
on_value:
- lvgl.label.update:
id: lbl_battery_status
text: !lambda |-
static char buf[10];
std::string icon;
if (x == 100.0) {
icon = "\U000F0079"; // mdi-battery (full)
} else if (x > 90) {
icon = "\U000F0082"; // mdi-battery-90
} else if (x > 80) {
icon = "\U000F0081"; // mdi-battery-80
} else if (x > 70) {
icon = "\U000F0080"; // mdi-battery-70
} else if (x > 60) {
icon = "\U000F007F"; // mdi-battery-60
} else if (x > 50) {
icon = "\U000F007E"; // mdi-battery-50
} else if (x > 40) {
icon = "\U000F007D"; // mdi-battery-40
} else if (x > 30) {
icon = "\U000F007C"; // mdi-battery-30
} else if (x > 20) {
icon = "\U000F007B"; // mdi-battery-20
} else if (x > 10) {
icon = "\U000F007A"; // mdi-battery-10
} else if (x > 0) {
icon = "\U000F008E"; // mdi-battery-outline
} else {
icon = "\U000F0091"; // mdi-battery-unknown (questionmark)
}
snprintf(buf, sizeof(buf), "%s", icon.c_str());
return buf;
lvgl:
...
pages:
- id: battery_page
widgets:
- label:
id: lbl_battery_status
align: TOP_RIGHT
y: 5
x: -15
text_font: battery_icons_20
text: "\U000F0091" # 从 mdi-battery-unknown 开始

要显示一个表示电池充电的动画,你可以使用 animimg 与一组 由 MDI 渲染的图像 显示电池等级:

image:
- platform: file
file: mdi:battery-10
type: BINARY
id: batt_10
resize: 20x20
- platform: file
file: mdi:battery-20
type: BINARY
id: batt_20
resize: 20x20
- platform: file
file: mdi:battery-30
type: BINARY
id: batt_30
resize: 20x20
- platform: file
file: mdi:battery-40
type: BINARY
id: batt_40
resize: 20x20
- platform: file
file: mdi:battery-50
type: BINARY
id: batt_50
resize: 20x20
- platform: file
file: mdi:battery-60
type: BINARY
id: batt_60
resize: 20x20
- platform: file
file: mdi:battery-70
type: BINARY
id: batt_70
resize: 20x20
- platform: file
file: mdi:battery-80
type: BINARY
id: batt_80
resize: 20x20
- platform: file
file: mdi:battery-90
type: BINARY
id: batt_90
resize: 20x20
- platform: file
file: mdi:battery
type: BINARY
id: batt_full
resize: 20x20
- platform: file
file: mdi:battery-outline
type: BINARY
id: batt_empty
resize: 20x20
lvgl:
...
pages:
- id: battery_page
widgets:
- animimg:
align: TOP_RIGHT
y: 5
x: -15
id: ani_battery_charging
src: [
batt_empty,
batt_10,
batt_20,
batt_30,
batt_40,
batt_50,
batt_60,
batt_70,
batt_80,
batt_90,
batt_full
]
duration: 2200ms

TIP

你可以将上述两个电池示例叠加放置,并根据充电器是否连接来切换它们的 hidden 标志:

binary_sensor:
- platform: ...
id: charger_connected
on_press:
then:
- lvgl.widget.show: ani_battery_charging
- lvgl.widget.hide: lbl_battery_status
on_release:
then:
- lvgl.widget.show: lbl_battery_status
- lvgl.widget.hide: ani_battery_charging

使用 x、y、align 控件属性进行精确定位。

使用 meter 和 label 控件,我们可以创建一个模拟时钟,它 还会显示日期。

meter 有三个刻度:一个用于分钟刻度和指针,范围在 0 到 60;一个 用于小时刻度和作为主刻度的标签,范围在 1 到 12;还有一个更高精度的小时 刻度,范围在 0 到 720,以便将指针自然地定位在小时之间。第二个刻度 没有指示器,而第三个刻度没有刻度也没有标签。

该脚本在每分钟开始时运行,以更新每个指针的线条位置以及相应的 文本。

lvgl:
...
style_definitions:
- id: date_style
text_font: unscii_8
align: center
text_color: 0x000000
bg_opa: 60%
radius: 4
pad_all: 2
pages:
- id: clock_page
widgets:
- obj: # 时钟容器
height: SIZE_CONTENT
width: 240
align: CENTER
pad_all: 0
border_width: 0
bg_color: 0xFFFFFF
widgets:
- meter: # 表盘
height: 220
width: 220
align: CENTER
bg_opa: TRANSP
border_width: 0
text_color: 0x000000
scales:
- range_from: 0 # 分钟刻度
range_to: 60
angle_range: 360
rotation: 270
ticks:
width: 1
count: 61
length: 10
color: 0x000000
indicators:
- line:
id: minute_hand
width: 3
color: 0xa6a6a6
length: 96%
value: 0
- range_from: 1 # 小时标签刻度
range_to: 12
angle_range: 330
rotation: 300
ticks:
width: 1
count: 12
length: 1
major:
stride: 1
width: 4
length: 10
color: 0xC0C0C0
label_gap: 12
- range_from: 0 # 高精度小时刻度用于指针
range_to: 720
angle_range: 360
rotation: 270
indicators:
- line:
id: hour_hand
width: 5
color: 0xa6a6a6
length: 70%
value: 0
- label:
styles: date_style
id: day_label
y: -30
- label:
id: date_label
styles: date_style
y: 30
time:
- platform: homeassistant
id: time_comp
on_time_sync:
- script.execute: time_update
on_time:
- minutes: '*'
seconds: 0
then:
- script.execute: time_update
script:
- id: time_update
then:
- lvgl.indicator.update:
id: minute_hand
value: !lambda |-
return id(time_comp).now().minute;
- lvgl.indicator.update:
id: hour_hand
value: !lambda |-
auto now = id(time_comp).now();
return std::fmod(now.hour, 12) * 60 + now.minute;
- lvgl.label.update:
id: date_label
text: !lambda |-
static const char * const mon_names[] = {"JAN", "FEB", "MAR", "APR", "MAY", "JUN",
"JUL", "AUG", "SEP", "OCT", "NOV", "DEC"};
static char date_buf[8];
auto now = id(time_comp).now();
snprintf(date_buf, sizeof(date_buf), "%s %2d", mon_names[now.month-1], now.day_of_month);
return date_buf;
- lvgl.label.update:
id: day_label
text: !lambda |-
static const char * const day_names[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};
return day_names[id(time_comp).now().day_of_week - 1];

buttonmatrix 控件可以与 Key collector 组件 一起工作,以收集按键按下作为按键序列。它会将按钮的 text (或在配置时使用的 key_code)发送给按键收集器。

如果你输入正确的序列,led 控件会相应地改变颜色:

lvgl:
...
pages:
- id: keypad_page
widgets:
- led:
id: lvgl_led
x: 30
y: 47
color: 0xFF0000
brightness: 70%
- obj:
width: 140
height: 25
align_to:
id: lvgl_led
align: OUT_RIGHT_MID
x: 17
border_width: 1
border_color: 0
border_opa: 50%
pad_all: 0
bg_opa: 80%
bg_color: 0xFFFFFF
shadow_color: 0
shadow_opa: 50%
shadow_width: 10
shadow_spread: 3
radius: 5
widgets:
- label:
id: lvgl_label
align: CENTER
text: "Enter code and \uF00C"
text_align: CENTER
- buttonmatrix:
id: lvgl_keypad
x: 20
y: 85
width: 200
height: 190
items:
pressed:
bg_color: 0xFFFF00
rows:
- buttons:
- text: 1
control:
no_repeat: true
- text: 2
control:
no_repeat: true
- text: 3
control:
no_repeat: true
- buttons:
- text: 4
control:
no_repeat: true
- text: 5
control:
no_repeat: true
- text: 6
control:
no_repeat: true
- buttons:
- text: 7
control:
no_repeat: true
- text: 8
control:
no_repeat: true
- text: 9
control:
no_repeat: true
- buttons:
- text: "\uF55A"
key_code: "*"
control:
no_repeat: true
- text: 0
control:
no_repeat: true
- text: "\uF00C"
key_code: "#"
control:
no_repeat: true
key_collector:
- source_id: lvgl_keypad
min_length: 4
max_length: 4
end_keys: "#"
end_key_required: true
back_keys: "*"
allowed_keys: "0123456789*#"
timeout: 5s
on_progress:
- if:
condition:
lambda: return (x != "");
then:
- lvgl.label.update:
id: lvgl_label
text: !lambda 'return x.c_str();'
else:
- lvgl.label.update:
id: lvgl_label
text: "Enter code and \uF00C"
on_result:
- if:
condition:
lambda: return (x == "1234");
then:
- lvgl.led.update:
id: lvgl_led
color: 0x00FF00
else:
- lvgl.led.update:
id: lvgl_led
color: 0xFF0000

NOTE

  • 基础对象 obj 用作标签的父级;这允许正确地居中标签,并通过阴影独立于标签尺寸地强调它。
  • align_to 用于在垂直方向上将标签与 led 对齐。
  • 在 pressed 状态下更改按钮的背景颜色。
  • 使用 key_code 配置将不同的字符发送给 key_collector,而不是显示的符号。

另一个依赖 Grid 布局的示例可以是显示通过 Home Assistant 的 OpenWeatherMap 集成 天气预报的面板。

这里显示的所有信息都可以通过本地 platform: homeassistant 传感器获取,正如本书中几个 示例所描述的那样;不过,这次我们采用不同的方法。不是由 ESPHome 拉取数据, 而是从 Home Assistant 推送到原生的 Lvgl 组件。

我们使用的气象状况图标来自 MDI。我们只导入 Home Assistant 的 Weather 集成 所支持的气象状况对应的图标。对于其他所有标签,你可以使用任何 字体 的选择。

binary_sensor:
- platform: status
name: Status sensor
font:
- file: "fonts/materialdesignicons-webfont.ttf"
id: icons_100
size: 100
bpp: 4
glyphs: [
"\U000F0594", # clear-night
"\U000F0590", # cloudy
"\U000F0F2F", # exceptional
"\U000F0591", # fog
"\U000F0592", # hail
"\U000F0593", # lightning
"\U000F067E", # lightning-rainy
"\U000F0595", # partlycloudy
"\U000F0596", # pouring
"\U000F0597", # rainy
"\U000F0598", # snowy
"\U000F067F", # snowy-rainy
"\U000F0599", # sunny
"\U000F059D", # windy
"\U000F059E", # windy-variant
"\U000F14E4", # sunny-off
]
lvgl:
...
pages:
- id: weather_forecast
widgets:
- obj:
align: CENTER
width: 228
height: 250
pad_all: 10
layout:
pad_column: 0
type: GRID
grid_rows: [FR(48), FR(13), FR(13), FR(13), FR(13)]
grid_columns: [FR(10), FR(40), FR(40), FR(10)]
widgets:
- label:
text: "\U000F14E4"
id: lbl_weather_forecast_condition_icon
text_font: icons_100
text_align: CENTER
grid_cell_row_pos: 0
grid_cell_column_pos: 0
grid_cell_column_span: 2
grid_cell_x_align: CENTER
grid_cell_y_align: START
- label:
text: "Unknown"
id: lbl_weather_forecast_condition_name
text_align: CENTER
grid_cell_row_pos: 0
grid_cell_column_pos: 2
grid_cell_column_span: 2
grid_cell_x_align: STRETCH
grid_cell_y_align: CENTER
- label:
text: "Feels like:"
grid_cell_row_pos: 1
grid_cell_column_pos: 1
- label:
text: "--.- °C"
id: lbl_weather_forecast_tempap
text_align: RIGHT
grid_cell_row_pos: 1
grid_cell_column_pos: 2
grid_cell_x_align: STRETCH
- label:
text: "Maximum:"
grid_cell_row_pos: 2
grid_cell_column_pos: 1
- label:
text: "--.- °C"
id: lbl_weather_forecast_temphi
text_align: RIGHT
grid_cell_row_pos: 2
grid_cell_column_pos: 2
grid_cell_x_align: STRETCH
- label:
text: "Minimum:"
grid_cell_row_pos: 3
grid_cell_column_pos: 1
- label:
text: "--.- °C"
id: lbl_weather_forecast_templo
text_align: RIGHT
grid_cell_row_pos: 3
grid_cell_column_pos: 2
grid_cell_x_align: STRETCH
- label:
text: "Now:"
grid_cell_row_pos: 4
grid_cell_column_pos: 1
- label:
text: "--.- °C"
id: lbl_weather_outdnoor_now
text_align: RIGHT
grid_cell_row_pos: 4
grid_cell_column_pos: 2
grid_cell_x_align: STRETCH
text:
- platform: lvgl
name: fr_cond_icon
widget: lbl_weather_forecast_condition_icon
mode: text
- platform: lvgl
name: fr_cond_name
widget: lbl_weather_forecast_condition_name
mode: text
- platform: lvgl
name: fr_tempap
widget: lbl_weather_forecast_tempap
mode: text
- platform: lvgl
name: fr_temphi
widget: lbl_weather_forecast_temphi
mode: text
- platform: lvgl
name: fr_templo
widget: lbl_weather_forecast_templo
mode: text
- platform: lvgl
name: wd_out_now
widget: lbl_weather_outdnoor_now
mode: text

如果你仔细查看 grid_columns 变量,你会注意到左右两侧有两条较窄的列 (FR(10) )。原因是为了给标签与边缘之间留出一些空间。这就是为什么我们在 第一行的控件中使用了 grid_cell_column_span,以占用多列的空间。

这些标签将在 Home Assistant 中显示为 可编辑文本组件,这使得使用 text.set_value 动作更新它们非常容易。为此,我们向 Home Assistant 添加以下 自动化:

- id: weather_cond_forecast
alias: 'Weather Forecast Condition'
trigger:
- platform: state
entity_id: sensor.openweathermap_forecast_condition
- platform: state
entity_id: binary_sensor.your_esphome_node_status_sensor
to: 'on'
action:
- action: text.set_value
target:
entity_id:
- text.your_esphome_node_fr_cond_icon
data:
value: >
{% set d = {
"clear-night": "\U000F0594",
"cloudy": "\U000F0590",
"exceptional": "\U000F0F2F",
"fog": "\U000F0591",
"hail": "\U000F0592",
"lightning": "\U000F0593",
"lightning-rainy": "\U000F067E",
"partlycloudy": "\U000F0595",
"pouring": "\U000F0596",
"rainy": "\U000F0597",
"snowy": "\U000F0598",
"snowy-rainy": "\U000F067F",
"sunny": "\U000F0599",
"windy": "\U000F059D",
"windy-variant": "\U000F059E",
"unknown": "\U000F14E4",
"unavailable": "\U000F14E4",
} %}
{{ d.get( states('sensor.openweathermap_forecast_condition') ) }}
- action: text.set_value
target:
entity_id:
- text.your_esphome_node_fr_cond_name
data:
value: >
{% set d = {
"clear-night": "Clear Night",
"cloudy": "Cloudy",
"exceptional": "Exceptional",
"fog": "Fog",
"hail": "Hail",
"lightning": "Lightning",
"lightning-rainy": "Lightning rainy",
"partlycloudy": "Partly cloudy",
"pouring": "Pouring",
"rainy": "Rainy",
"snowy": "Snowy",
"snowy-rainy": "Snowy rainy",
"sunny": "Sunny",
"windy": "Windy",
"windy-variant": "Windy cloudy",
"unknown": "Unknown",
"unavailable": "Unavailable",
} %}
{{ d.get( states('sensor.openweathermap_forecast_condition') ) }}
- id: weather_temp_feels_like_forecast
alias: 'Weather Temperature Feels Like'
trigger:
- platform: state
entity_id: sensor.openweathermap_feels_like_temperature
- platform: state
entity_id: binary_sensor.your_esphome_node_status_sensor
to: 'on'
action:
- action: text.set_value
target:
entity_id:
- text.your_esphome_node_fr_tempap
data:
value: "{{states('sensor.openweathermap_feels_like_temperature') | round(1)}} °C"
- id: weather_temp_forecast_temphi
alias: 'Weather Temperature Forecast Hi'
trigger:
- platform: state
entity_id: sensor.openweathermap_forecast_temperature
- platform: state
entity_id: binary_sensor.your_esphome_node_status_sensor
to: 'on'
action:
- action: text.set_value
target:
entity_id:
- text.your_esphome_node_fr_temphi
data:
value: "{{states('sensor.openweathermap_forecast_temperature') | round(1)}} °C"
- id: weather_temp_forecast_templo
alias: 'Weather Temperature Forecast Lo'
trigger:
- platform: state
entity_id: sensor.openweathermap_forecast_temperature_low
- platform: state
entity_id: binary_sensor.your_esphome_node_status_sensor
to: 'on'
action:
- action: text.set_value
target:
entity_id:
- text.your_esphome_node_fr_templo
data:
value: "{{states('sensor.openweathermap_forecast_temperature_low') | round(1)}} °C"
- id: weather_temp_outdoor_now
alias: 'Weather Temperature Now'
trigger:
- platform: state
entity_id: sensor.outdoor_temperature
- platform: state
entity_id: binary_sensor.your_esphome_node_status_sensor
to: 'on'
action:
- action: text.set_value
target:
entity_id:
- text.your_esphome_node_wd_out_now
data:
value: "{{states('sensor.outdoor_temperature') | round(1)}} °C"

每当相应的实体发生变化时,以及 ESPHome 恢复运行时,自动化将被触发以更新标签——这也是你还需要 Status 的原因。注意,你需要根据你 配置其名称使用方式 调整对应的 ESPHome 节点的实体 ID。

LVGL 有屏幕非活动的概念——换句话说,它会追踪自上次用户与屏幕交互以来的时间。 这可用于在一段时间没有活动后调暗显示背光或关闭它 (就像屏幕保护程序一样)。每次使用输入设备(触摸屏、旋转编码器)都算作一次活动并重置 非活动计数器。注意使用 on_release 触发器来完成此任务很重要。通过模板数字, 你可以让用户调整超时时间。

lvgl:
...
on_idle:
timeout: !lambda "return (id(display_timeout).state * 1000);"
then:
- logger.log: "LVGL is idle"
- light.turn_off: display_backlight
- lvgl.pause:
touchscreen:
- platform: ...
on_release:
- if:
condition: lvgl.is_paused
then:
- logger.log: "LVGL resuming"
- lvgl.resume:
- lvgl.widget.redraw:
- light.turn_on: display_backlight
light:
- platform: ...
id: display_backlight
number:
- platform: template
name: LVGL Screen timeout
optimistic: true
id: display_timeout
unit_of_measurement: "s"
initial_value: 45
restore_value: true
min_value: 10
max_value: 180
step: 5
mode: box

你可以利用这一点来保护并延长 LCD 屏幕的使用寿命,从而更加环保并减少 有害废物的产生。

壁挂式 LCD 屏幕的一个常见问题是,它们 99.999% 的时间都显示相同的图像。即使 有人在夜间或黑暗时段关闭了背光,LCD 屏幕仍然显示相同的图像, 只是没人看见。这种情况在使用几年后很可能会发生烧屏。

缓解这一问题的方法之一是定期锻炼像素,通过显示不同的内容。在 LVGL 暂停状态下使用 show_snow 选项就是为此而设计的;它会在整个屏幕上显示随机颜色的像素, 以通过锻炼每个单独的像素来最小化屏幕烧屏。

在下面的示例中,像素训练每晚进行四次,每次半小时;可以通过 触摸屏幕来停止。

time:
- platform: ...
on_time:
- hours: 2,3,4,5
minutes: 5
seconds: 0
then:
- switch.turn_on: switch_antiburn
- hours: 2,3,4,5
minutes: 35
seconds: 0
then:
- switch.turn_off: switch_antiburn
switch:
- platform: template
name: Antiburn
id: switch_antiburn
icon: mdi:television-shimmer
optimistic: true
entity_category: "config"
turn_on_action:
- logger.log: "Starting Antiburn"
- if:
condition: lvgl.is_paused
then:
- lvgl.resume:
- lvgl.widget.redraw:
- lvgl.pause:
show_snow: true
turn_off_action:
- logger.log: "Stopping Antiburn"
- if:
condition: lvgl.is_paused
then:
- lvgl.resume:
- lvgl.widget.redraw:
touchscreen:
- platform: ...
on_release:
then:
- if:
condition: lvgl.is_paused
then:
- lvgl.resume:
- lvgl.widget.redraw:

你可以将它与前面的示例结合使用来关闭背光,这样用户就不会实际注意到。