跳转到内容

显示组件

display 组件封装了 ESPHome 的图形渲染和显示引擎。它支持多种不同类型的显示器,从简单的字符型显示器到可逐像素寻址的图形显示器。

对于灵活性最高的图形显示器,有两种显示内容的方式:

所有显示组件都继承以下配置变量。

  • id (可选, ID): 手动指定用于代码生成的 ID。如果存在多个显示器则此项为必需。
  • update_interval (可选, 时间): 重新绘制屏幕的间隔。默认为 1s。
  • lambda (可选, lambda): 用于在显示上渲染内容的 lambda。 更多信息请参见 显示渲染引擎。

所有 图形 显示器还继承以下配置变量。

  • auto_clear_enabled (可选, boolean): 是否在每次更新前清除显示。如果配置了 lambda 或 pages,则默认为 true,否则为 false。
  • show_test_card (可选, boolean): 是否显示测试卡。默认为 false。如果设置,lambda 配置选项中的所有代码都将被忽略。
  • rotation (可选, integer): 显示器的旋转角度(度),取值为 0、90、180 或 270 之一。默认为 0。
  • pages (可选, list): 页面配置,见下文。

ESPHome 自带的强大渲染引擎可以处理许多常见任务,例如绘制基本形状、使用你选择的字体打印文本,甚至渲染图像。

为了获得所有这些灵活性,显示器直接与 ESPHome 的 lambda 系统 相关联。因此,当你要在屏幕上写入文本或传感器数值时,你需要使用 C++ 代码来编写,所使用的 API 被设计成:

  • 简单易用,无需编程经验即可使用
  • 同时也足够灵活,可以处理更复杂的任务,例如显示一个模拟时钟。

在本节中,我们将讨论如何在 ESPHome 中使用其显示渲染引擎以及一些基本命令。请注意,这仅适用于能够单独控制每个像素的显示器。

NOTE

显示硬件较为复杂,有时表现并不如预期。如果你的显示器有问题,请参阅下面的 故障排除。

首先,一些基础知识:在 ESPHome 中设置显示器平台时,会有一个名为 lambda: 的配置选项,ESPHome 每次想要重新渲染显示时都会调用它。在每次循环中,显示器会在执行 lambda 之前被自动清除。你可以通过设置 auto_clear_enabled: false 来禁用此行为。 在 lambda 中,你可以像在 ESPHome 中任何其他 lambda 中一样编写代码。显示 lambda 还会额外获得一个名为 it 的变量,它代表渲染引擎对象。

display:
- platform: ...
# ...
lambda: |-
// Write your display rendering code here
// For example, draw a line from [x=0,y=0] to [x=50,y=50]
it.line(0, 0, 50, 50);

NOTE

Lambda 本质上是 C++ 的一个轻度修改版本。因此,别忘了在每行代码末尾加上分号(;)。否则,在编译阶段你会看到一条长长的错误信息。

如果你编译并上传上述配置,你应该能看到一条黑色(或白色,取决于显示器)的线,它从左上角开始,以 45° 的角度向下延伸几个像素。(如果它出现在其他角落,请使用 rotation: 选项将显示器旋转到你满意的角度)

这已经突出展示了在编写自定义显示代码之前你必须了解的事情之一: 左上角始终是像素坐标系统的原点。另外,这个坐标系统中的所有点都是一对整数,如 50, 50,表示向右和下方向的偏移。换句话说,x 始终代表水平轴(宽度),y 代表垂直轴(高度)。并且在渲染引擎中,惯例是始终先指定 x 坐标,然后是 y 坐标。

现在你对 ESPHome 的坐标系统有了更深的了解,让我们画一些基本形状,如直线、矩形、圆形甚至多边形:

display:
- platform: ...
# ...
lambda: |-
// Draw a line from [0,0] to [100,50]
it.line(0, 0, 100, 50);
// Draw the outline of a rectangle with the top left at [5,20], a width of 30 and a height of 42
it.rectangle(5, 20, 30, 42);
// Draw the same rectangle a few pixels apart, but this time filled
it.filled_rectangle(40, 40, 30, 42);
// Circles! Let's draw one with the center at [20,40] and a radius of 10
it.circle(20, 40, 10);
// ... and the same thing filled again
it.filled_circle(20, 75, 10);
// Ring and half-ring. First draw the circle with a hole in it
// at [75,75] with inner raduis of 20 and outer of 30
it.filled_ring(75, 75, 30, 20);
// and a "gauge": half-ring that is partially filled.
// Same position and size but 80% filled left to right
it.filled_gauge(75, 75, 30, 20, 80);
// Triangles... Let's draw the outline of a triangle from the [x,y] coordinates of its three points
// [25,5], [100,5], [80,25]
it.triangle(25, 5, 100, 5, 80, 25);
// and a filled triangle !
it.filled_triangle(115, 5, 95, 25, 125, 70);
// Regular Polygons? Let's draw a filled, pointy-topped hexagon inscribed in a circle
// centered on [170,45] with a radius of 20
it.filled_regular_polygon(170, 45, 20, EDGES_HEXAGON);
// and the outline of flat-topped octagon around it!
it.regular_polygon(170, 45, 40, EDGES_OCTAGON, VARIATION_FLAT_TOP);
// Need to rotate the polygon, or retrieve the coordinates of its vertices? Check the API!

以上所有方法都可以选择性地附带一个末尾参数,用于指定绘制的颜色。对于单色显示器,只支持 COLOR_ON(未指定颜色时的默认值)和 COLOR_OFF。

display:
- platform: ...
# ...
lambda: |-
// Turn the whole display on
it.fill(COLOR_ON);
// Turn the whole display off
it.fill(COLOR_OFF);
// Turn a single pixel off at [50,60]
it.draw_pixel_at(50, 60, COLOR_OFF);

对于彩色显示器(例如 TFT 显示器),你可以使用 Color 类。

display:
- platform: ...
# ...
lambda: |-
auto black = Color(0, 0, 0);
auto red = Color(255, 0, 0);
auto green = Color(0, 255, 0);
auto blue = Color(0, 0, 255);
auto white = Color(255, 255, 255);
it.filled_circle(20, 32, 15, black);
it.filled_circle(40, 32, 15, red);
it.filled_circle(60, 32, 15, green);
it.filled_circle(80, 32, 15, blue);
it.filled_circle(100, 32, 15, white);

此外,你还可以使用两个辅助方法获取显示器的宽度和高度:

display:
- platform: ...
# ...
lambda: |-
// Draw a circle in the middle of the display
it.filled_circle(it.get_width() / 2, it.get_height() / 2, 20);
// Turn off bottom half of the screen
it.filled_rectangle(0, it.get_height()/2, it.get_width(), it.get_height()/2, COLOR_OFF);

你可以在”参见”部分的 API 参考中查看渲染引擎的完整 API 文档。

为了显示文本,你需要准备一些字体。ESPHome 的 字体渲染器 允许你为文本使用 OpenType/TrueType/位图字体。这非常灵活,因为你可以准备多套不同大小、包含不同字形数量的字体,这在节省闪存空间方面极其方便。

在你的显示代码中,你可以通过引用字体并输入用双引号括起来的字符串来渲染静态文本:

display:
- platform: ...
# ...
lambda: |-
// Print the string "Hello World!" at [0,10]
it.print(0, 10, id(my_font), "Hello World!");

默认情况下,ESPHome 会将文本 左对齐。这意味着如果你为文本输入坐标 [0,10],文本的左上角就会位于 [0,10]。如果你想将文本绘制在显示器的右侧,有时候选择另一种 文本对齐方式 会更有用。当你输入 [0,10] 时,实际上是在告诉 ESPHome 将文本的 锚点 定位在 [0,10]。使用不同的对齐方式,例如 TOP_RIGHT,文本将位于锚点的左侧,因此,顾名思义,锚点位于文本的右上角。

display:
- platform: ...
# ...
lambda: |-
// Aligned on left by default
it.print(0, 0, id(my_font), "Left aligned");
// Aligned on right edge
it.print(it.get_width(), 0, id(my_font), TextAlign::TOP_RIGHT, "Right aligned");

与基本形状一样,你也可以为文本指定颜色:

display:
- platform: ...
# ...
lambda: |-
// Syntax is always: it.print(<x>, <y>, <font>, [color=COLOR_ON], [align=TextAlign::TOP_LEFT], <text>);
it.print(0, 0, id(my_font), COLOR_ON, "Left aligned");

对于以更高位深渲染的字体,必须在文本之后指定背景色,才能使抗锯齿生效:

display:
- platform: ...
# ...
lambda: |-
// Syntax is always: it.print(<x>, <y>, <font>, [color=COLOR_ON], [align], <text>, [color=COLOR_OFF]);
it.print(0, 0, id(my_font_with_icons), COLOR_ON, TextAlign::CENTER, "Just\U000f05d4here. Already\U000F02D1this.", COLOR_OFF);

静态文本本身并不那么令人印象深刻。我们真正想要的是在显示器上显示 动态 内容,例如传感器数值!这就需要用到 printf。printf 是源自 C 语言时代的格式化引擎,ESPHome 之所以选择它,是因为……好吧,我太懒了,不想创建一个完整的格式化引擎,而现有的东西文档更全 :)

printf 能做的事情比你实际需要的要多得多,但对于基本用法来说它非常简单。 例如,一个 printf 调用可能如下所示:

sensor:
- platform: ...
# ...
id: my_sensor
display:
- platform: ...
# ...
lambda: |-
it.printf(0, 0, id(my_font), "The sensor value is: %.1f", id(my_sensor).state);
// If the sensor has the value 30.02, the result will be: "The sensor value is: 30.0"

如你所见,当你调用 printf 时,大部分字符串会原样打印,但当遇到这种带百分号和一些字符的奇怪符号时,它会被神奇地替换为格式参数后面的参数(此处为 id(my_sensor).state)。

每次你在 printf 格式字符串中键入百分号 % 时,它会将后面的字母视为格式标签,直到遇到一个所谓的”说明符”(在此例中为 f)。你可以在 https://www.tutorialspoint.com/c_standard_library/c_function_printf.htm 上了解更多相关信息, 但对于 ESPHome 来说,你只需了解以下几点。

让我们分解 %.1f:

  • % - 启动格式字符串
  • .1 - 将小数四舍五入到小数点后 1 位。
  • f - 说明符,告诉 printf 参数的数据类型。此处为 f(loat)。

例如,如果你想以两位精度打印传感器数值,可以写 %.2f;如果不带小数(零位精度),可以写 %.0f。

另一个有趣的格式字符串是 %7.2f,对于值 20.506,它会变成右对齐的字符串 " 20.51"。

  • % - 启动格式

  • 7 - 表示数字将右对齐,如果结果少于 7 个字符,则会在左侧用空格填充。

  • .2 - 将小数四舍五入到小数点后 2 位。

  • f - 说明符:f(loat)。

你甚至可以在单个 printf 调用中使用任意多个格式化项。只需确保按正确顺序将参数放在格式字符串后面。

display:
- platform: ...
# ...
lambda: |-
// %% - literal % sign
it.printf(0, 0, id(my_font), "Temperature: %.1f°C, Humidity: %.1f%%", id(temperature).state, id(humidity).state);

要显示来自 text_sensor 的文本字符串,请在变量末尾追加 .c_str()。

display:
- platform: ...
# ...
lambda: |-
it.printf(0, 0, id(my_font), "Text to follow: %s", id(template_text).state.c_str());

使用抗锯齿字体时,你可能需要指定字符的绘制颜色以及用于抗锯齿混合的背景色。这需要 printf 的完整版本,例如:

display:
- platform: ...
# ...
lambda: |-
it.printf(10, 100, id(roboto), Color(0x123456), COLOR_OFF, display::TextAlign::BASELINE, "%f", id(heap_free).state);

下面将要介绍的最后一个用于显示的 printf 技巧是如何显示二进制传感器的数值。你 当然 可以用 if 语句来检查状态,就像下面示例开头几行的做法,但如果 你想更简洁,也可以使用 内联 if。通过 %s 打印说明符,你可以让它使用你传入的任意字符串,例如 "ON" 或 "OFF"。

binary_sensor:
- platform: ...
# ...
id: my_binary_sensor
display:
- platform: ...
# ...
lambda: |-
if (id(my_binary_sensor).state) {
it.print(0, 0, id(my_font), "state: ON");
} else {
it.print(0, 0, id(my_font), "state: OFF");
}
// Shorthand:
it.printf(0, 0, id(my_font), "State: %s", id(my_binary_sensor).state ? "ON" : "OFF");

NOTE

要在显示器上显示外部数据,例如来自 Home Assistant 实例的数据, 可以使用 Mqtt Subscribe(更多信息请参见那里的示例)。

你可以使用时间组件来显示当前时间。请参阅 时间文档 上的示例。

当你只想显示图像的一部分,或者确保你在屏幕上绘制的内容不会超出屏幕上某个特定区域时,屏幕裁剪会很有用。

使用 start_clipping(left, top, right, bottom); 开始裁剪过程,当你在该区域完成绘制后, 可以使用 end_clipping(); 停止裁剪过程。你可以根据需要嵌套任意多个 start_clipping();,只要以相同数量调用 end_clipping(); 结束即可。

binary_sensor:
- platform: ...
# ...
id: my_binary_sensor
color:
- id: my_red
red: 100%
display:
- platform: ...
# ...
lambda: |-
if (id(my_binary_sensor).state) {
it.print(0, 0, id(my_font), "state: ON");
} else {
it.print(0, 0, id(my_font), "state: OFF");
}
// Shorthand:
it.start_clipping(40,0,140,20);
it.printf(0, 0, id(my_font), id(my_red), "State: %s", id(my_binary_sensor).state ? "ON" : "OFF");
it.end_clipping();

开始裁剪后,你可以通过 extend_clipping(left, top, right, bottom); 和 shrink_clipping(left, top, right, bottom); 在前一次设置的裁剪区域内操纵裁剪区域。

使用 get_clipping(); 你可以获得一个包含最新设置的裁剪区域的 Rect 对象。

class Rect {
int16_t x; ///< X/左坐标
int16_t y; ///< Y/上坐标
int16_t w; ///< 宽度
int16_t h; ///< 高度
int16_t x2(); ///< 右坐标
int16_t y2(); ///< 下坐标
};

使用 is_clipping(); 可以告诉你裁剪是否已启用。

在 ESPHome 中使用支持 RGB 的显示器时,你可能希望使用自定义颜色。 color 组件正是为此目的而存在的:

color:
- id: my_light_red
red: 100%
green: 20%
blue: 25%
white: 0%

或者,你也可以使用 <color>_int 将颜色以 int 值形式指定:

color:
- id: my_light_red
red_int: 255
green_int: 51
blue_int: 64
white_int: 0

或者,如果你更习惯使用十六进制值,可以使用 hex:

color:
- id: my_light_red
hex: FF3340

配置变量:

  • red (可选, percentage): 红色分量的百分比。默认为 100%。
  • red_int (可选, integer): 红色分量在 0 到 255 范围内的亮度。默认为 255。
  • green (可选, percentage): 绿色分量的百分比。默认为 100%。
  • green_int (可选, integer): 绿色分量在 0 到 255 范围内的亮度。默认为 255。
  • blue (可选, percentage): 蓝色分量的百分比。默认为 100%。
  • blue_int (可选, integer): 蓝色分量在 0 到 255 范围内的亮度。默认为 255。
  • white (可选, percentage): 白色分量的百分比。默认为 100%。
  • white_int (可选, integer): 白色分量在 0 到 255 范围内的亮度。默认为 255。
  • hex (可选, string): 颜色的十六进制表示。默认为 FFFFFF。

RGB 显示器使用红、绿、蓝,而灰度显示器可能会使用白色。

某些显示器类型还允许你显示”页面”。通过页面,你可以创建绘图 lambda, 并可以在它们之间切换。例如,通过页面你可以设置 3 个屏幕,每个屏幕显示 不同的内容,并通过定时器在它们之间切换。

display:
- platform: ...
# ...
id: my_display
pages:
- id: page1
lambda: |-
it.print(0, 10, id(my_font), "This is page 1!");
- id: page2
lambda: |-
it.print(0, 10, id(my_font), "This is page 2!");

你可以通过以下三种操作在这些页面之间切换:

display.page.show_next / display.page.show_previous 操作

Section titled “display.page.show_next / display.page.show_previous 操作”

显示下一个或上一个页面,到达末尾后回绕。

on_...:
- display.page.show_next: my_display
- display.page.show_previous: my_display
# For example cycle through pages on a timer
interval:
- interval: 5s
then:
- display.page.show_next: my_display
- component.update: my_display

显示特定页面。

on_...:
- display.page.show: page1
# Templated
- display.page.show: !lambda |-
if (id(my_binary_sensor).state) {
return id(page1);
} else {
return id(page2);
}

NOTE

要在显示页面后立即触发重新绘制,请使用 component.update 操作:

# For example cycle through pages on a timer
interval:
- interval: 5s
then:
- display.page.show_next: my_display
- component.update: my_display

此条件在显示指定页面期间返回 true。

# In some trigger:
on_...:
- if:
condition:
display.is_displaying_page: page1
then:
...
- if:
condition:
display.is_displaying_page:
id: my_display
page_id: page2
then:
...

on_page_change: 当所显示的页面发生变化时,将触发此自动化。

display:
- platform: ...
# ...
on_page_change:
- from: page1
to: page2
then:
lambda: |-
ESP_LOGD("display", "Page changed from 1 to 2");
  • from (可选, ID): 页面 ID。如果设置,则仅从该页面切换时触发自动化。默认为所有页面。
  • to (可选, ID): 页面 ID。如果设置,则仅切换到该页面时触发自动化。默认为所有页面。

此外,旧页面将作为变量 from 提供,新页面作为变量 to 提供。

如果你在颜色显示器方面遇到问题,show_test_card: true 选项可以帮助你确定哪里出了问题。

  • 它将显示红色、绿色和蓝色的渐变条,从黑色到白色。
  • 与此同时,它将显示字母”R”、“G”和”B”以验证显示器的几何形状。
  • 显示器的角落周围会有一个矩形,并在 0,0 角落处有一个标记,该标记应该位于屏幕的左上角。

如果以上各点都正确显示,则显示器工作正常。 为了帮助图形显示团队确定最佳方式来帮助你,提供此选项结果的图片非常有帮助。

如果你在 GitHub 上 创建 issue 反映显示问题,请务必 包含你购买显示器的链接,以便我们验证你所使用的配置。

NOTE

如果在配置中设置了 update_interval: never,你将看不到测试卡,因为 display: 组件不会用测试卡更新显示。如果你想要看到测试卡,请将 update_interval: 设置为除 never 以外的其他值。

NOTE

对于 8 位模式的显示器,你将看到明显的色块,而不是平滑的渐变。