From 300327649e8d7e7035b3e74551cf479c7d599b15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=2E=20=C3=81rkosi=20R=C3=B3bert?= Date: Sun, 15 Sep 2024 12:00:17 +0200 Subject: [PATCH] LVGL cookbook (#4110) Co-authored-by: Keith Burzinski Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- components/lvgl/index.rst | 9 +- components/lvgl/widgets.rst | 29 +- cookbook/images/lvgl_cook_animimg_batt.gif | Bin 0 -> 8109 bytes cookbook/images/lvgl_cook_climate.png | Bin 0 -> 1672 bytes cookbook/images/lvgl_cook_clock.png | Bin 0 -> 8115 bytes cookbook/images/lvgl_cook_cover.png | Bin 0 -> 4539 bytes cookbook/images/lvgl_cook_flex_layout.png | Bin 0 -> 5015 bytes cookbook/images/lvgl_cook_font_batt.png | Bin 0 -> 243 bytes cookbook/images/lvgl_cook_font_binstat.png | Bin 0 -> 2715 bytes cookbook/images/lvgl_cook_font_roboto_mdi.png | Bin 0 -> 2278 bytes cookbook/images/lvgl_cook_gauge.png | Bin 0 -> 4154 bytes cookbook/images/lvgl_cook_gradient_styles.png | Bin 0 -> 10498 bytes cookbook/images/lvgl_cook_keypad.png | Bin 0 -> 5951 bytes cookbook/images/lvgl_cook_pagenav.png | Bin 0 -> 1312 bytes cookbook/images/lvgl_cook_remligbut.png | Bin 0 -> 1696 bytes cookbook/images/lvgl_cook_statico.png | Bin 0 -> 700 bytes cookbook/images/lvgl_cook_thermometer.png | Bin 0 -> 11532 bytes .../images/lvgl_cook_thermometer_gauge.png | Bin 0 -> 5872 bytes cookbook/images/lvgl_cook_titlebar.png | Bin 0 -> 2366 bytes cookbook/images/lvgl_cook_volume.png | Bin 0 -> 1264 bytes cookbook/images/lvgl_cook_weather.png | Bin 0 -> 8364 bytes cookbook/lvgl.rst | 2242 +++++++++++++++++ index.rst | 1 + lint.py | 1 + 24 files changed, 2276 insertions(+), 6 deletions(-) create mode 100644 cookbook/images/lvgl_cook_animimg_batt.gif create mode 100644 cookbook/images/lvgl_cook_climate.png create mode 100644 cookbook/images/lvgl_cook_clock.png create mode 100644 cookbook/images/lvgl_cook_cover.png create mode 100644 cookbook/images/lvgl_cook_flex_layout.png create mode 100644 cookbook/images/lvgl_cook_font_batt.png create mode 100644 cookbook/images/lvgl_cook_font_binstat.png create mode 100644 cookbook/images/lvgl_cook_font_roboto_mdi.png create mode 100644 cookbook/images/lvgl_cook_gauge.png create mode 100644 cookbook/images/lvgl_cook_gradient_styles.png create mode 100644 cookbook/images/lvgl_cook_keypad.png create mode 100644 cookbook/images/lvgl_cook_pagenav.png create mode 100644 cookbook/images/lvgl_cook_remligbut.png create mode 100644 cookbook/images/lvgl_cook_statico.png create mode 100644 cookbook/images/lvgl_cook_thermometer.png create mode 100644 cookbook/images/lvgl_cook_thermometer_gauge.png create mode 100644 cookbook/images/lvgl_cook_titlebar.png create mode 100644 cookbook/images/lvgl_cook_volume.png create mode 100644 cookbook/images/lvgl_cook_weather.png create mode 100644 cookbook/lvgl.rst diff --git a/components/lvgl/index.rst b/components/lvgl/index.rst index ccfff25e0..86a0a5651 100644 --- a/components/lvgl/index.rst +++ b/components/lvgl/index.rst @@ -16,6 +16,7 @@ The graphic display should be configured with ``auto_clear_enabled: false`` and For interactivity, a :doc:`Touchscreen ` (capacitive highly preferred), a :doc:`/components/sensor/rotary_encoder` or a custom keypad made up from discrete :doc:`Binary Sensors ` can be used. +Check out the detailed examples in :ref:`the Cookbook ` which demonstrate a number of ways you can integrate your environment with LVGL and ESPHome. Basics ------ @@ -146,6 +147,7 @@ The following configuration variables apply to the main ``lvgl`` component, in o align: CENTER text: 'Hello World!' +See :ref:`lvgl-cookbook-navigator` in the Cookbook for an example which demonstrates how to implement a page navigation bar at the bottom of the screen. .. _lvgl-color: @@ -176,6 +178,7 @@ You can use :ref:`fonts configured normally`, the glyphs will be For best results, set ``bpp: 4`` to get the glyphs rendered with proper anti-aliasing. +Check out :ref:`lvgl-cookbook-icontext`, :ref:`lvgl-cookbook-iconstat` and :ref:`lvgl-cookbook-iconbatt` in the Cookbook for examples which demonstrate how to use icons and text with TrueType/OpenType fonts. **Library fonts** @@ -378,6 +381,7 @@ So the precedence happens like this: state based styles override the locally spe Feel free to experiment to discover inheritance and precedence of the styles based on states between the nested widgets. +:ref:`lvgl-cookbook-theme` The Cookbook contains an example which demonstrates how to implement a gradient style for your widgets. .. _lvgl-layouts: @@ -388,6 +392,7 @@ Layouts aim to position widgets automatically, eliminating the need to specify ` The layout configuration options are applied to any parent widget or page, influencing the appearance of the children. The position and size calculated by the layout overwrites the *normal* ``x``, ``y``, ``width``, and ``height`` settings of the children. +Check out :ref:`lvgl-cookbook-flex`, :ref:`lvgl-cookbook-grid` and :ref:`lvgl-cookbook-weather` in the Cookbook for examples which demonstrate how to automate widget positioning, potentially reducing the size of your device's YAML configuration, and saving you from lots of manual calculations. The ``hidden``, ``ignore_layout`` and ``floating`` :ref:`flags ` can be used on widgets to ignore them in layout calculations. @@ -594,7 +599,7 @@ This :ref:`action ` redraws the entire screen, or optionally onl This :ref:`action ` pauses the activity of LVGL, including rendering. -- **show_snow** (*Optional*, boolean): When paused, display random colored pixels across the entire screen in order to minimize screen burn-in, to relief the tension put on each individual pixel. +- **show_snow** (*Optional*, boolean): When paused, display random colored pixels across the entire screen in order to minimize screen burn-in, to relief the tension put on each individual pixel. See :ref:`lvgl-cookbook-antiburn` for an example which demonstrates how to use this. .. code-block:: yaml @@ -801,6 +806,7 @@ The ``on_idle`` :ref:`triggers ` are activated when inactivity time - light.turn_off: display_backlight - lvgl.pause: +See :ref:`lvgl-cookbook-idlescreen` for an example which demonstrates how to implement screen saving with idle settings. See Also -------- @@ -811,6 +817,7 @@ See Also * +- :doc:`LVGL Examples in the Cookbook ` - :doc:`/components/display/index` - :doc:`/components/touchscreen/index` - :doc:`/components/sensor/rotary_encoder` diff --git a/components/lvgl/widgets.rst b/components/lvgl/widgets.rst index 460e6a759..1909bebcf 100644 --- a/components/lvgl/widgets.rst +++ b/components/lvgl/widgets.rst @@ -83,6 +83,8 @@ To apply styles to the states, you need to specify them one level above, for exa The state itself can be can be changed by interacting with the widget, or through :ref:`actions ` with ``lvgl.widget.update``. +See :ref:`lvgl-cookbook-cover` for a cookbook example which demonstrates how to use styling and properties to show different states of a Home Assistant entity. + .. _lvgl-widget-flags: In addition to visual styling, each widget supports some boolean **flags** to influence the behavior: @@ -114,7 +116,7 @@ In addition to visual styling, each widget supports some boolean **flags** to in .. note:: - LVGL only supports **integers** for numeric ``value``. Visualizer widgets can't display floats directly, but they allow scaling by 10s. + LVGL only supports **integers** for numeric ``value``. Visualizer widgets can't display floats directly, but they allow scaling by 10s. Some examples in the :doc:`Cookbook ` cover how to do that. .. _lvgl-widget-parts: @@ -264,6 +266,8 @@ The animation image is similar to the normal ``image`` widget. The main differen repeat_count: 100 duration: 300ms +See :ref:`lvgl-cookbook-animbatt` in the Cookbook for a more detailed example. + .. _lvgl-widget-arc: ``arc`` @@ -345,7 +349,9 @@ If the ``adv_hittest`` :ref:`flag ` is enabled the arc can be The ``on_value`` trigger is sent as the arc knob is dragged or changed with keys. The event is sent *continuously* while the arc knob is being dragged; this generally has a negative effect on performance. To mitigate this, consider using a :ref:`universal interaction trigger ` like ``on_release``, to get the ``x`` variable once after the interaction has completed. -The ``arc`` can be also integrated as :doc:`Number ` or :doc:`Sensor ` component. +The ``arc`` can be also integrated as a :doc:`Number ` or :doc:`Sensor ` component. + +See :ref:`lvgl-cookbook-bright` and :ref:`lvgl-cookbook-volume` for examples which demonstrate how to use a slider (or an arc) to control entities in Home Assistant. .. _lvgl-widget-bar: @@ -466,6 +472,8 @@ To have a button with a text label on it, add a child :ref:`lvgl-widget-label` w The ``button`` can be also integrated as a :doc:`Binary Sensor ` or as a :doc:`Switch ` component. +See :ref:`lvgl-cookbook-binent` for an example which demonstrates how to use a checkable button to act on a Home Assistant service. + .. _lvgl-widget-buttonmatrix: ``buttonmatrix`` @@ -596,7 +604,7 @@ The button matrix widget is a lightweight way to display multiple buttons in row .. tip:: - The Button Matrix widget supports the :ref:`key_collector` to collect the button presses as key press sequences for further automations. + The Button Matrix widget supports the :ref:`key_collector` to collect the button presses as key press sequences for further automations. Check out :ref:`lvgl-cookbook-keypad` for an example. .. _lvgl-widget-checkbox: @@ -656,7 +664,7 @@ The checkbox widget is made internally from a *tick box* and a label. When the c .. note:: - In case you configure ``default_font`` in the main section to a custom font, the checkmark will not be shown correctly when the checkbox is in the checked state. + In case you configure ``default_font`` in the main section to a custom font, the checkmark will not be shown correctly when the checkbox is in the checked state. See :ref:`lvgl-cookbook-ckboxmark` for how to easily resolve this. The ``checkbox`` can be also integrated as a :doc:`Switch ` component. @@ -988,6 +996,8 @@ The ``led`` can be also integrated as :doc:`Light ` comp If configured as a light component, ``color`` and ``brightness`` are overridden by the light at startup, according to its ``restore_mode`` setting. +Check out :ref:`lvgl-cookbook-keypad` in the Cookbook for an example which demonstrates how to change the ``led`` styling properties from an automation. + .. _lvgl-widget-line: ``line`` @@ -1139,6 +1149,8 @@ The meter widget can visualize data in very flexible ways. It can use arcs, need id: temperature_needle value: 3 +See :ref:`lvgl-cookbook-gauge`, :ref:`lvgl-cookbook-thermometer` and :ref:`lvgl-cookbook-clock` in the Cookbook for examples which demonstrate how to effectively use this widget. + .. _lvgl-widget-msgbox: ``msgboxes`` @@ -1362,6 +1374,8 @@ Normally, the slider can be adjusted either by dragging the knob, or by clicking The ``slider`` can be also integrated as :doc:`Number ` or :doc:`Sensor ` component. +See :ref:`lvgl-cookbook-bright` and :ref:`lvgl-cookbook-volume` for examples which demonstrate how to use a slider to control entities in Home Assistant. + .. _lvgl-widget-spinbox: ``spinbox`` @@ -1437,7 +1451,9 @@ The spinbox contains a numeric value (as text) which can be increased or decreas format: "Spinbox value is %f" args: [ x ] -The ``spinbox`` can be also integrated as :doc:`Number ` or :doc:`Sensor ` component. +The ``spinbox`` can be also integrated as a :doc:`Number ` or :doc:`Sensor ` component. + +See :ref:`lvgl-cookbook-climate` for an example which demonstrates how to implement a thermostat control using the spinbox. .. _lvgl-widget-spinner: @@ -1531,6 +1547,8 @@ The switch looks like a little slider and can be used to turn something on and o The ``switch`` can be also integrated as a :doc:`Switch ` component. +See :ref:`lvgl-cookbook-relay` for an example which demonstrates how to use a switch to act on a local component. + .. _lvgl-widget-tabview: ``tabview`` @@ -1781,6 +1799,7 @@ This powerful :ref:`action ` allows changing/updating any widget id: my_label_id hidden: true +Check out in the Cookbook :ref:`lvgl-cookbook-binent` for an example which demonstrates how to use a template to update the state. .. _lvgl-automation-shorthands: diff --git a/cookbook/images/lvgl_cook_animimg_batt.gif b/cookbook/images/lvgl_cook_animimg_batt.gif new file mode 100644 index 0000000000000000000000000000000000000000..a1ec7806d9f4eba88f68f784037efe8f1383581a GIT binary patch literal 8109 zcmZ?wbhEHblwnX}s9<1F{Lk&@8WQa67~pE8XTZ$J02KPk!Xg5sb%2-wq-;5efg3P_ z0TUAwGcz*_3kxeND?2+o2L}fi7Z(o?4?jP@kdTmwh={njxP*j+q@<*jl$5lzw2X|5 ztgNh@oSeM8yn=#)qN1Xbl9IBrvWkj|s;a7*nwq-0x`u{^rlzKrmX@}*wvLXDuCA`0 zo}RwGzJY;(p`oFXk&&^nv5AR^si~=%nVGq{xrK#=rKP2nm6f%%wT+F9t*xz{ot?eC zy@P{;qobpflasTvvx|$1tE;PCcvmzTGBeM3V-V`F1eQ&V$ub4yE0 zYiny;TU&d3dq+n{XJ=CQh6C&akmMvSpeEEtMD^{*txoXv_)vH&pS+i#C+O_M}ty{l-{e}%2Hg4RwY15|7n>TOS zvSsVmt=qP3+rEAKjvYI8?%cU+*RK8h_n$s}`tIGk@87@w{{8!(KY#uWS@8e=e~xs9 z|C}-&8x|aF<`CA3Ik92k;dTLKuQ?tYl@2mV7)PC1QK)phpH-#liH1>%V~;BDqMjX} z7I}8)vL7q4_#E%sW}2^e=I1BH^HbETFNtJL*6>?oCEE9fqj=@TxqiA{bG^2%y1F`I zBO41S?=gV$9yfy+0~-S)p}dDXSE9%Ob0{Q#0y8HkCnp~tpRlknFgF5o;Aq|(W_fS- z?%jL#?Ag0_@4kKefcfyifddB*9z1mD(BZ>}j~qF2^ytxJ$BrF8e*DCV6DLoeJay_6 zFr%J1bLQ;Xv**s8JAeNCg$oxhUc7ke(xuCnFJHNGgPoF(|_Wb$t7cXACeEIU# zt5>gIzkc)P&D*zc-@SVWEDk<=`0(-L$4{R=eg6FU%a<=-zkdDp?HjO+`0?Y%&!0bk z{rdI$_itc%@%QgvV2J_Bdu$Bi95NCD$XP>lG;54jOJr0_BOq%C1G5Gbm1+%O6#}d{ zfUUaGT4UI^*oIry;9!vCknspea6qm%ghuO)(RyRF-oVzR5**DMqgi7#YhcS7{G&~( z(X26=H8`*~srW|gjnT|8nmMr58@!`gV>D}wW({mvgJ(2rjAo6|tbxcH|3rNgQ&JVm VGfOfQf|H9Gu-4m#b`8oD@{u1n#P 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 literal 0 HcmV?d00001 diff --git a/cookbook/images/lvgl_cook_cover.png b/cookbook/images/lvgl_cook_cover.png new file mode 100644 index 0000000000000000000000000000000000000000..5c9fe2987167fe988d4d2efe28cfcf0c58a5a55b GIT binary patch literal 4539 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%0D2(KoskIQB==m9F_$;C81@U>R#n-J_Huy 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 literal 0 HcmV?d00001 diff --git a/cookbook/images/lvgl_cook_font_batt.png b/cookbook/images/lvgl_cook_font_batt.png new file mode 100644 index 0000000000000000000000000000000000000000..6803ee049bae1b86ed99f7393698c631da4950eb GIT binary patch literal 243 zcmeAS@N?(olHy`uVBq!ia0vp^G9b*s1SJ3FdmIK*oCO|{#S9F5M?jcysy3fAP;jZI zi(`mKXY!x_|LvK#G74Yyl`);tx}d9g`uVeS?ZnUdp7SxgWV-0eh2Q+|@^A0sJy-gv zL@7fta$C44~A=k!nTEZi|eW$4{&vLWn*LW89hjbZSA!#~$R)^y6gwy2v? o6BUm(*w3BB+vYx_kHML7C#Pj{4*P6zpo1AaUHx3vIVCg!0L2wqhyVZp literal 0 HcmV?d00001 diff --git a/cookbook/images/lvgl_cook_font_binstat.png b/cookbook/images/lvgl_cook_font_binstat.png new file mode 100644 index 0000000000000000000000000000000000000000..4315ba8cceafce83febc3b99d15b3ed9ef839e9f GIT binary patch literal 2715 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 A(f|Me literal 0 HcmV?d00001 diff --git a/cookbook/images/lvgl_cook_gauge.png b/cookbook/images/lvgl_cook_gauge.png new file mode 100644 index 0000000000000000000000000000000000000000..11379cb9d92fb2804f266c5d22b6591b3c4844df GIT binary patch literal 4154 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 TzWxJsFb+^r(3G!~wG8<$^H^HV literal 0 HcmV?d00001 diff --git a/cookbook/images/lvgl_cook_pagenav.png b/cookbook/images/lvgl_cook_pagenav.png new file mode 100644 index 0000000000000000000000000000000000000000..db7b3b55f008b75bf4ce25b1dc35119aa73e433c GIT binary patch literal 1312 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 JWt~$(69A@pIDh~E literal 0 HcmV?d00001 diff --git a/cookbook/images/lvgl_cook_thermometer.png b/cookbook/images/lvgl_cook_thermometer.png new file mode 100644 index 0000000000000000000000000000000000000000..ee38819578cb9e3f9d0f7f34c11d9df3e0998c09 GIT binary patch literal 11532 zcmW++Wmpwm7ah8hlI}*j8|m(ny3$?J-JQavySuwnr8}iNE+CD7f_(S=e$1KY%*=D< z`3_gxO zaG0KEXnAphiZYRcG?BbAk-WtpC⪻Zu*3B`h@RP3He3XW>QkLh}P<8*%EbOmtXmb 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 literal 0 HcmV?d00001 diff --git a/cookbook/images/lvgl_cook_thermometer_gauge.png b/cookbook/images/lvgl_cook_thermometer_gauge.png new file mode 100644 index 0000000000000000000000000000000000000000..6976d767d2e7529d13ab324c3c1f08f833a9107e GIT binary patch literal 5872 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 literal 0 HcmV?d00001 diff --git a/cookbook/images/lvgl_cook_volume.png b/cookbook/images/lvgl_cook_volume.png new file mode 100644 index 0000000000000000000000000000000000000000..3d42748c97934c4cf05925a6fcfc36f17a9d9aa1 GIT binary patch literal 1264 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 Rq5m+zP?A%VEdd$_{x1_sDER;Y literal 0 HcmV?d00001 diff --git a/cookbook/lvgl.rst b/cookbook/lvgl.rst new file mode 100644 index 000000000..bd9089292 --- /dev/null +++ b/cookbook/lvgl.rst @@ -0,0 +1,2242 @@ +.. _lvgl-cookbook: + +LVGL: Tips and Tricks +===================== + +.. seo:: + :description: Recipes for common use cases of LVGL Displays with ESPHome + :image: /images/lvgl.png + +Here are a couple recipes for various interesting things you can do with :doc:`/components/lvgl/index` in ESPHome. + +.. note:: + + Many of the examples below call service actions in Home Assistant; however, Home Assistant does not allow such action calls by default. For each ESPHome device which will call actions, you must explicitly enable this setting in Home Assistant. This may be done when the device is initially adopted or by using the `Configure` option in the "devices" list of the ESPHome integration. + +.. note:: + + The examples below assume you've set up LVGL correctly with your display and its input device, and you have the knowledge to set up various components in ESPHome. Some examples use absolute positioning for a screen with dimensions of ``240x320px``; if your display's dimensions differ, you'll need to adjust them in order to obtain the expected results. + +.. _lvgl-cookbook-relay: + +Local light switch +------------------ + +.. figure:: /components/lvgl/images/lvgl_switch.png + :align: left + +The easiest way to integrate an LVGL :ref:`lvgl-widget-switch` widget and a switch or light is with :ref:`automations `: + +.. code-block:: yaml + + 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 + +.. _lvgl-cookbook-binent: + +Remote light button +------------------- + +.. figure:: images/lvgl_cook_remligbut.png + :align: right + +If you'd like to control a remote light which appears as an entity in Home Assistant from a checkable (toggle) :ref:`lvgl-widget-button`, first you need to import the light state into ESPHome, and then control it using a action call: + +.. code-block:: yaml + + 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 + +.. _lvgl-cookbook-bright: + +Light brightness slider +----------------------- + +.. figure:: images/lvgl_cook_volume.png + :align: left + +You can use a :ref:`slider ` or an :ref:`arc ` to control the brightness of a dimmable light. + +We can use a sensor to retrieve the current brightness of a light, which is stored in Home Assistant as an attribute of the entity, as an integer value between ``0`` (min) and ``255`` (max). It's convenient to set the slider's ``min_value`` and ``max_value`` accordingly. + +.. code-block:: yaml + + 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: 30 + height: 220 + pad_all: 8 + min_value: 0 + max_value: 255 + on_release: + - homeassistant.action: + action: light.turn_on + data: + entity_id: light.your_dimmer + brightness: !lambda return int(x); + +Note that Home Assistant expects an integer at the ``brightness`` parameter of the ``light.turn_on`` action call, and since ESPHome uses floats, ``x`` needs to be converted. + +This is applicable to action calls like ``fan.set_percentage`` or ``valve.set_valve_position``, too; the only difference is that ``max_value`` has to be ``100``. + +.. _lvgl-cookbook-volume: + +Media player volume slider +-------------------------- + +.. figure:: images/lvgl_cook_volume.png + :align: right + +Similarly, you can use a :ref:`slider ` or an :ref:`arc ` to control the volume level of a media player, which uses float values. + +With a sensor we retrieve the current volume level of the media player, which is stored in Home Assistant as an attribute of the entity, and is a float value between ``0`` (min) and ``1`` (max). Since LVGL only handles integers, it's convenient to set the slider's possible values to be between ``0`` and ``100``. Thus a conversion is needed back and forth, meaning that when we read the value from Home Assistant we have to multiply it by ``100``, and when we set the volume through the action call, we have to divide it by ``100``: + +.. code-block:: yaml + + 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: mediaplayer_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); + +The ``adv_hittest`` option ensures that accidental touches to the screen won't cause sudden volume changes (more details in the :ref:`slider doc `). + +.. note:: + + Keep in mind that ``on_value`` is triggered *continuously* by the slider while it's being dragged. This generally has a negative effect on performance. For example, you shouldn't use this trigger to set the target temperature of a heat pump via Modbus, or set the position of motorized covers, because it will likely cause malfunctions. To mitigate this, consider using a universal widget trigger like ``on_release`` to get the ``x`` variable once after the interaction has completed. + +.. _lvgl-cookbook-gauge: + +Semicircle gauge +---------------- + +A gauge similar to what Home Assistant shows in the Energy Dashboard can accomplished with :ref:`lvgl-widget-meter` and :ref:`lvgl-widget-label` widgets: + +.. figure:: images/lvgl_cook_gauge.png + :align: center + +The trick here is to have a parent :ref:`lvgl-widget-obj` which contains the other widgets as children. We place a :ref:`lvgl-widget-meter` in the middle, which is made from an indicator ``line`` and two ``arc`` widgets. We use another, smaller :ref:`lvgl-widget-obj` on top of it to hide the indicator's central parts and place some :ref:`lvgl-widget-label` widgets to display numeric information: + +.. code-block:: yaml + + 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 + align: CENTER + scales: + - range_from: -10 + range_to: 10 + angle_range: 180 # sets the total angle to 180 = starts mid left and ends mid right + ticks: + count: 0 + indicators: + - line: + id: val_needle + width: 8 + r_mod: 12 # sets line length by this much difference from the scale default radius + value: -2 + - arc: # first half of the scale background + color: 0xFF3000 + r_mod: 10 # radius difference from the scale default radius + width: 31 + start_value: -10 + end_value: 0 + - arc: # second half of the scale background + color: 0x00FF00 + r_mod: 10 + width: 31 + start_value: 0 + end_value: 10 + - obj: # to cover the middle part of meter indicator line + height: 146 + width: 146 + radius: 73 + align: CENTER + border_width: 0 + bg_color: 0xFFFFFF + pad_all: 0 + - label: # gauge numeric indicator + id: val_text + text_font: montserrat_48 + align: CENTER + y: -5 + text: "0" + - label: # lower range indicator + text_font: montserrat_18 + align: CENTER + y: 8 + x: -90 + text: "-10" + - label: # higher range indicator + text_font: montserrat_18 + align: CENTER + y: 8 + x: 90 + text: "+10" + +.. tip:: + + The ``obj`` used to hide the middle part of the meter indicator line has ``radius`` equal to half of the ``width`` and ``height``. This results in a circle - which is actually a square with extra large rounded corners. + +.. _lvgl-cookbook-thermometer: + +Thermometer +----------- + +A thermometer with a precise gauge also made from a :ref:`lvgl-widget-meter` widget and a numeric display using :ref:`lvgl-widget-label`: + +.. figure:: images/lvgl_cook_thermometer.png + :align: center + +Whenever a new value comes from the sensor, we update the needle indicator as well as the text in the :ref:`lvgl-widget-label`. Since LVGL only handles integer values on the :ref:`lvgl-widget-meter` scale, but the sensor's value is a ``float``, we use the same approach as in the examples above; we multiply the sensor's values by ``10`` and feed this value to the :ref:`lvgl-widget-meter`. It's essentially two scales on top of each other: one to set the needle based on the multiplied value and the other to show sensor's original value in the :ref:`lvgl-widget-label`. + +.. code-block:: yaml + + 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: -100 # scale for the needle value + range_to: 400 + angle_range: 240 + rotation: 150 + indicators: + - line: + id: temperature_needle + width: 2 + color: 0xFF0000 + r_mod: -4 + - tick_style: + start_value: -10 + end_value: 40 + color_start: 0x0000bd + color_end: 0xbd0000 + width: 1 + - range_from: -10 # scale for the value labels + 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 + widgets: + - label: + id: temperature_text + text: "-.-°C" + align: CENTER + y: 45 + - label: + text: "Outdoor" + align: CENTER + y: 65 + +And here's the same sensor configuration, but instead with a semicircle gauge with a gradient background drawn by a multitude of ticks: + +.. figure:: images/lvgl_cook_thermometer_gauge.png + :align: center + +If you change the size of the widget, to obtain a uniform gradient, be sure to increase or decrease the ticks count accordingly. + +.. code-block:: yaml + + 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 + scales: + - range_from: -15 + range_to: 35 + angle_range: 180 + ticks: + count: 70 + width: 1 + length: 31 + indicators: + - tick_style: + start_value: -15 + end_value: 35 + color_start: 0x3399ff + color_end: 0xffcc66 + - range_from: -150 + range_to: 350 + angle_range: 180 + ticks: + count: 0 + indicators: + - line: + id: temperature_needle + width: 8 + r_mod: 2 + value: -150 + - obj: # to cover the middle part of meter indicator line + height: 123 + width: 123 + radius: 73 + align: CENTER + border_width: 0 + pad_all: 0 + bg_color: 0xFFFFFF + - label: + id: temperature_text + text: "--.-°C" + align: CENTER + y: -26 + - label: + text: "Outdoor" + align: CENTER + y: -6 + +.. tip:: + + You can omit the ``obj`` used to hide the middle part of meter indicator line by using a bitmap ``image`` indicator as needle, were only the part hanging above the ticks scale is visible, the rest is transparent. + +.. _lvgl-cookbook-climate: + +Climate control +--------------- + +:ref:`lvgl-widget-spinbox` is the ideal widget to control a thermostat: + +.. figure:: images/lvgl_cook_climate.png + :align: center + +First we import from Home Assistant the current target temperature of the climate component, and we update the value of the spinbox with it whenever it changes. We use two buttons labeled with minus and plus to control the spinbox, and whenever we change its value, we just simply call a Home Assistant action to set the new target temperature of the climate. + +.. code-block:: yaml + + 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 + step: 0.5 + 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: "+" + +.. _lvgl-cookbook-cover: + +Cover status and control +------------------------ + +To make a nice user interface for controlling Home Assistant covers you could use 3 buttons, which also display the state. + +.. figure:: images/lvgl_cook_cover.png + :align: center + +Just as in the previous examples, we need to get the state of the cover first. We'll use a numeric sensor to retrieve the current position of the cover and a text sensor to retrieve its current movement. We are particularly interested in the moving (*opening* and *closing*) states, because during these we'd like to change the label in the middle to show *STOP*. Otherwise, this button label will show the actual percentage of the opening. Additionally, we'll change the opacity of the labels on the *UP* and *DOWN* buttons depending on if the cover is fully open or closed. + +.. code-block:: yaml + + 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 ((0 == x.compare(std::string{"opening"})) or (0 == x.compare(std::string{"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-cookbook-theme: + +Theme and style definitions +--------------------------- + +Since LVGL uses inheritance to apply styles across the widgets, it's possible to apply them at the top level, and only make modifications on demand, if necessary. + +.. figure:: images/lvgl_cook_gradient_styles.png + :align: center + +In this example we prepare a set of gradient styles in the *theme*, and make some modifications in a *style_definition* which can be applied in a batch to the desired widgets. Theme is applied automatically, and can be overridden manually with style definitions (read further to see how). + +.. code-block:: yaml + + lvgl: + ... + theme: + label: + text_font: my_font # set all your labels to use your custom defined 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: # set some button colors to be different in pressed state + bg_color: 0x006699 + bg_grad_color: 0x00334d + checked: # set some button colors to be different in checked state + 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: # set all your buttonmatrix buttons to use your custom defined styles and font + 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 + +Note that style definitions can contain common properties too, like positioning and sizing. + +.. _lvgl-cookbook-navigator: + +Page navigation footer +---------------------- + +If using multiple pages, a navigation bar can be useful at the bottom of the screen: + +.. figure:: images/lvgl_cook_pagenav.png + :align: center + +To save from repeating the same widgets on each page, there's the *top_layer* which is the *Always on Top* transparent page above all the pages. Everything you put on this page will be on top of all the others. + +For the navigation bar we can use a :ref:`lvgl-widget-buttonmatrix`. Note how the *header_footer* style definition is being applied to the widget and its children objects, and how a few more styles are configured manually at the main widget: + +.. code-block:: yaml + + 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: + +For this example to appear correctly, use the theme and style options from :ref:`above ` and LVGL's own library :ref:`fonts `. + +.. _lvgl-cookbook-statico: + +API connection status icon +-------------------------- + +The top layer is useful to show status icons visible on all pages: + +.. figure:: images/lvgl_cook_statico.png + :align: center + +In the example below, we only show the icon when the connection with Home Assistant is established: + +.. code-block:: yaml + + api: + on_client_connected: + - if: + condition: + lambda: 'return (0 == client_info.find("Home Assistant "));' + then: + - 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 + +Of note: + +- The widget starts *hidden* at boot and it's only shown when triggered by connection with the API. +- Alignment of the widget: since the *align* option is given, the *x* and *y* options are used to position the widget relative to the calculated position. + +.. _lvgl-cookbook-titlebar: + +Title bar for each page +----------------------- + +Each page can have its own title bar: + +.. figure:: images/lvgl_cook_titlebar.png + :align: center + +To put a title bar behind the status icon, we need to add it to each page, also containing the label with a unique title: + +.. code-block:: yaml + + 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 + ... + +For this example to work, use the theme and style options from :ref:`above `. + +.. _lvgl-cookbook-flex: + +Flex layout positioning +----------------------- + +:ref:`lvgl-layouts` aim to position widgets automatically, eliminating the need to specify coordinates to position each widget. This is a great way to simplify your configuration containing many widgets as it allows you to even omit alignment options. + +.. figure:: images/lvgl_cook_flex_layout.png + :align: center + +This example illustrates a control panel for three covers, made up of labels and discrete buttons. Although a button matrix could also be suitable for this, you might still prefer fully-featured individual buttons, as they offer a wider range of customization possibilities as seen in the :ref:`lvgl-cookbook-cover` example. Here we use the **Flex** layout: + +.. code-block:: yaml + + lvgl: + ... + pages: + - id: room_page + widgets: + - obj: # a properly placed coontainer object for all these controls + align: CENTER + width: 240 + height: 256 + x: 4 + y: 4 + pad_all: 3 + pad_row: 6 + pad_column: 8 + bg_opa: TRANSP + border_opa: TRANSP + layout: # enable the FLEX layout for the children widgets + type: FLEX + flex_flow: COLUMN_WRAP # the order of the widgets starts top left + flex_align_cross: CENTER # they sould be centered + widgets: + - label: + text: "East" + - button: + id: but_cov_up_east + width: 70 # choose the button dimensions so + height: 68 # they fill the columns nincely as they flow + widgets: + - label: + id: cov_up_east + align: CENTER + text: "\U000F005D" # mdi:arrow-up + - button: + id: but_cov_stop_east + width: 70 + height: 68 + widgets: + - label: + id: cov_stop_east + align: CENTER + text: "\U000F04DB" # mdi:stop + - button: + id: but_cov_down_east + width: 70 + height: 68 + widgets: + - label: + id: cov_down_east + align: CENTER + text: "\U000F0045" # mdi:arrow-down + + - label: + text: "South" + - button: + id: but_cov_up_south + width: 70 + height: 68 + widgets: + - label: + id: cov_up_south + align: CENTER + text: "\U000F005D" + - button: + id: but_cov_stop_south + width: 70 + height: 68 + widgets: + - label: + id: cov_stop_south + align: CENTER + text: "\U000F04DB" + - button: + id: but_cov_down_south + width: 70 + height: 68 + widgets: + - label: + id: cov_down_south + align: CENTER + text: "\U000F0045" + + - label: + text: "West" + - button: + id: but_cov_up_west + width: 70 + height: 68 + widgets: + - label: + id: cov_up_west + align: CENTER + text: "\U000F005D" + - button: + id: but_cov_stop_west + width: 70 + height: 68 + widgets: + - label: + id: cov_stop_west + align: CENTER + text: "\U000F04DB" + - button: + id: but_cov_down_west + width: 70 + height: 68 + widgets: + - label: + id: cov_down_west + align: CENTER + text: "\U000F0045" + +This saved you from a considerable amount of manual calculation of widget positioning which would otherwise be required to place them manually with ``x`` and ``y``! You only need to determine a common width and height for your widgets to distribute them on the page as you prefer. (:ref:`lvgl-cookbook-icontext` below shows how to use custom icons.) + +.. _lvgl-cookbook-grid: + +Grid layout positioning +----------------------- + +But there's even more! With the **Grid** layout, you don't need to specify width and height for your widgets. All you have to do is divide the space into rows and columns; the widgets can be automatically be sized to fit into cells defined by these rows and columns. The same task from above, in a fully automated grid, looks like this: + +.. code-block:: yaml + + lvgl: + ... + pages: + - id: room_page + widgets: + - obj: # a properly placed coontainer object for all these controls + align: CENTER + width: 240 + height: 256 + pad_all: 6 + pad_row: 6 + pad_column: 8 + bg_opa: TRANSP + border_opa: TRANSP + layout: # enable the GRID layout for the children widgets + type: GRID # split the rows and the columns proportionally + grid_columns: [FR(1), FR(1), FR(1)] # equal + grid_rows: [FR(10), FR(30), FR(30), FR(30)] # like percents + widgets: + - label: + text: "East" + grid_cell_column_pos: 0 # place the widget in + grid_cell_row_pos: 0 # the corresponding cell + grid_cell_x_align: STRETCH + 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: "\U000F005D" + - 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: "\U000F04DB" + - 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: "\U000F0045" + + - label: + text: "South" + grid_cell_column_pos: 1 + grid_cell_row_pos: 0 + grid_cell_x_align: STRETCH + 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: "\U000F005D" + - 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: "\U000F04DB" + - 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: "\U000F0045" + + - label: + text: "West" + grid_cell_column_pos: 2 + grid_cell_row_pos: 0 + grid_cell_x_align: STRETCH + 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: "\U000F005D" + - 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: "\U000F04DB" + - 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: "\U000F0045" + +The big advantage here is that whenever you need to add, for example, an extra column of buttons for a new cover, you just simply append it to the ``grid_columns`` variable, and add the corresponding widgets as above. With ``STRETCH`` their sizes and positions will automatically be calculated to fill in the cells, while the parent's ``pad_all``, ``pad_row`` and ``pad_column`` can help with spacing between them. See :ref:`lvgl-cookbook-weather` further down this page for another example relying on **Grid**. + +.. _lvgl-cookbook-btlg: + +ESPHome boot screen +------------------- + +To display a boot image with a spinner animation which disappears automatically after a few moments or on touch of the screen you can use the *top layer*. The trick is to put a base :ref:`lvgl-widget-obj` full screen and child :ref:`lvgl-widget-image` widget in its middle as the last item of the widgets list, so they draw on top of all the others. To make it automatically disappear afer boot, you use ESPHome's ``on_boot`` trigger: + +.. code-block:: yaml + + esphome: + ... + on_boot: + - delay: 5s + - lvgl.widget.hide: boot_screen + + image: + - file: https://esphome.io/_static/favicon-512x512.png + id: boot_logo + resize: 200x200 + type: RGB565 + use_transparency: true + + lvgl: + ... + top_layer: + widgets: + ... # make sure it's the last one in this list: + - 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: -40 + - spinner: + align: CENTER + y: 95 + 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 + +.. _lvgl-cookbook-icontext: + +MDI icons in text +----------------- + +ESPHome's :ref:`font renderer ` allows you to use any OpenType/TrueType font file for your text. This is very flexible because you can prepare various sets of fonts at different sizes each with a different number of glyphs; this is important as it may help to conserve flash memory space. + +One example is when you'd like some MDI icons to be used in line with the text (similar to how LVGL's internal fonts and symbols coexist). You can use a font of your choice; choose the symbols/icons from MDI you want and mix them in a single sized set. + +.. figure:: images/lvgl_cook_font_roboto_mdi.png + :align: center + +In the example below, we use the default set of glyphs from RobotoCondensed-Regular and append some extra symbols to it from MDI. Then we display these inline with the text by escaping their codepoints: + +.. code-block:: yaml + + 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:: + + Follow these steps to choose your MDI icons: + + - To lookup your icons, use the `Pictogrammers `_ site. Click on the desired icon and note its codepoint (it's the hexadecimal number near the download options). + - To get the TrueType font with all the icons in it, head on to the `Pictogrammers GitHub repository `_ and from a recent version folder, download the ``materialdesignicons-webfont.ttf`` file and place it in your ESPHome config directory under a folder named ``fonts`` (to match the example above). + - To use the desired icon, prepend the copied codepoint with ``\U000``. The Unicode character escape sequence has to start with capital ``\U`` and have exactly 8 hexadecimal digits. + - To translate the escape sequence into the real glyph, make sure you enclose your strings in double quotes. + +.. _lvgl-cookbook-ckboxmark: + +Restore checkbox mark +--------------------- + +If you configure a custom font as the ``default_font`` used by LVGL and this font does not contain the `FontAwesome `__ symbols, you may observe that some widgets won't display correctly; specifically :ref:`lvgl-widget-checkbox` won't show the checkmark when it's checked. + +To work around this issue, simply import only the checkmark symbol in the desired size and apply it through :ref:`lvgl-cookbook-theme` to all the checkboxes in the configuration: + +.. code-block:: yaml + + font: + - file: 'fonts/FontAwesome5-Solid+Brands+Regular.woff' + id: fontawesome_checkmark + size: 18 + bpp: 4 + glyphs: [ + "\uF00C", # ckeckmark, for checkbox + ] + + lvgl: + ... + theme: + checkbox: + indicator: + checked: + text_font: fontawesome_checkmark + +You could of course simply apply one of the built-in ``montserrat_`` packs, but that would not be beneficial on the binary size - it would uselessly include the entire set of glyphs in the flash. + +.. _lvgl-cookbook-iconstat: + +Toggle state icon button +------------------------ + +.. figure:: images/lvgl_cook_font_binstat.png + :align: left + +A common use case for icons is a status display. For example, a checkable (toggle) button will display different icons based on the status of a light or switch. To put an icon on a button you use a :ref:`lvgl-widget-label` widget as the child of the :ref:`lvgl-widget-button`. The coloring can already be different thanks to the :ref:`lvgl-cookbook-theme` where you can set a different color for the ``checked`` state. Additionally, by using a ``text_sensor`` to import the state from Home Assistant, we can not only track the ``on`` state, but also the ``unavailable`` or ``unknown`` states to apply *disabled styles* for these cases. + +If we take our previous :ref:`lvgl-cookbook-binent` example, we can modify it like this: + +.. code-block:: yaml + + font: + - file: "custom/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 (0 == x.compare(std::string{"on"})); + disabled: !lambda return ((0 == x.compare(std::string{"unavailable"})) or (0 == x.compare(std::string{"unknown"}))); + - lvgl.label.update: + id: lbl_lightbulb + text: !lambda |- + static char buf[10]; + std::string icon; + if (0 == x.compare(std::string{"on"})) { + icon = "\U000F0335"; + } else { + icon = "\U000F0336"; + } + snprintf(buf, sizeof(buf), "%s", icon.c_str()); + return buf; + + lvgl: + ... + pages: + - id: room_page + widgets: + - button: + x: 110 + y: 40 + 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 + +.. _lvgl-cookbook-iconbatt: + +Battery status icon +------------------- + +.. figure:: images/lvgl_cook_font_batt.png + :align: left + +Another example for using MDI icons is to display battery percentage in 10 steps. We need to have a font containing the glyphs corresponding to the different battery percentage levels, and we need a sensor to import the battery status from Home Assistant into a numeric value. We use a :ref:`lambda ` to return the codepoint of the corresponding glyph based on the sensor value: + +.. code-block:: yaml + + 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 + } + 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: 40 + x: -10 + text_font: battery_icons_20 + text: "\U000F0091" # start with mdi-battery-unknown + +.. _lvgl-cookbook-animbatt: + +Battery charging animation +-------------------------- + +.. figure:: images/lvgl_cook_animimg_batt.gif + :align: left + +To have an animation illustrating a battery charging, you can use :ref:`lvgl-widget-animimg` with a set of :ref:`images rendered from MDI ` showing battery levels: + +.. code-block:: yaml + + image: + - file: mdi:battery-10 + id: batt_10 + resize: 20x20 + - file: mdi:battery-20 + id: batt_20 + resize: 20x20 + - file: mdi:battery-30 + id: batt_30 + resize: 20x20 + - file: mdi:battery-40 + id: batt_40 + resize: 20x20 + - file: mdi:battery-50 + id: batt_50 + resize: 20x20 + - file: mdi:battery-60 + id: batt_60 + resize: 20x20 + - file: mdi:battery-70 + id: batt_70 + resize: 20x20 + - file: mdi:battery-80 + id: batt_80 + resize: 20x20 + - file: mdi:battery-90 + id: batt_90 + resize: 20x20 + - file: mdi:battery + id: batt_full + resize: 20x20 + - file: mdi:battery-outline + id: batt_empty + resize: 20x20 + + lvgl: + ... + pages: + - id: battery_page + widgets: + - animimg: + align: TOP_RIGHT + y: 41 + x: -10 + 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:: + + You can use both battery examples above placed on top of each other, and switch their ``hidden`` flag depending if the charger is connected or not: + + .. code-block:: yaml + + 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 + + Use ``x``, ``y``, ``align`` widget properties for precise positioning. + +.. _lvgl-cookbook-clock: + +An analog clock +--------------- + +Using the :ref:`lvgl-widget-meter` and :ref:`lvgl-widget-label` widgets, we can create an analog clock which shows the date too. + +.. figure:: images/lvgl_cook_clock.png + :align: center + +The :ref:`lvgl-widget-meter` has three scales: one for minutes ticks and hand, ranged between ``0`` and ``60``; one for the hour ticks and the labels as majors, ranged between ``1`` and ``12``; and a higher resolution scale for the hour hand, ranged between ``0`` and ``720``, to be able to naturally position the hand in between the hours. The second scale doesn't have an indicator, while the third scale doesn't have ticks nor labels. + +The script runs at the beginning of every minute to update the line positions for each hand as well as the respective text. + +.. code-block:: yaml + + lvgl: + ... + pages: + - id: clock_page + widgets: + - obj: # clock container + height: SIZE_CONTENT + width: 240 + align: CENTER + pad_all: 0 + border_width: 0 + bg_color: 0xFFFFFF + widgets: + - meter: # clock face + height: 220 + width: 220 + align: CENTER + bg_opa: TRANSP + border_width: 0 + text_color: 0x000000 + scales: + - range_from: 0 # minutes scale + 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 + r_mod: -4 + value: 0 + - range_from: 1 # hours scale for labels + 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 # hi-res hours scale for hand + range_to: 720 + angle_range: 360 + rotation: 270 + ticks: + count: 0 + indicators: + - line: + id: hour_hand + width: 5 + color: 0xa6a6a6 + r_mod: -30 + 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]; + +.. _lvgl-cookbook-keypad: + +A numeric input keypad +---------------------- + +The :ref:`lvgl-widget-buttonmatrix` widget can work together with the :ref:`key_collector` to collect the button presses as key press sequences. It sends the ``text`` of the buttons (or ``key_code`` where configured) to the key collector. + +.. figure:: images/lvgl_cook_keypad.png + :align: center + +If you key in the correct sequence, the :ref:`lvgl-widget-led` widget will change color accordingly: + +.. code-block:: yaml + + 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 (0 != x.compare(std::string{""})); + 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 (0 == x.compare(std::string{"1234"})); + then: + - lvgl.led.update: + id: lvgl_led + color: 0x00FF00 + else: + - lvgl.led.update: + id: lvgl_led + color: 0xFF0000 + +Of note: + +- A base object ``obj`` is used as a parent for the label; this allows proper centering of the label as well as emphasizing it with shadows independently of the label's dimensions. +- ``align_to`` is used to align the label to the ``led`` vertically. +- Changing the background color of the buttons in ``pressed`` state. +- Use of the ``key_code`` configuration to send a different character to ``key_collector`` instead of the displayed symbol. + +.. _lvgl-cookbook-weather: + +Weather forecast panel +---------------------- + +Another example relying on the **Grid** layout can be a weather panel showing the forecast through the `OpenWeatherMap integration `__ of Home Assistant. + +.. figure:: images/lvgl_cook_weather.png + :align: center + +All the information displayed here could be retrieved to local ``platform: homeassistant`` sensors as desribed in several examples in this Cookbook, however, this time we take a different approach. Instead of pulling the data by ESPHome, we'll be pushing it from Home Assistant, to native :doc:`/components/text/lvgl` components. + +The weather condition icons we use are from MDI. We import just the ones corresponding to the weather conditions supported by the Weather integration in Home Assistant. For all the other labels you can use any :ref:`font ` of your choice. + +.. code-block:: yaml + + 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 + pad_column: 0 + layout: + 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 + +If you look carefully at the ``grid_columns`` variable, you'll notice that there are two thinner columns at left and right (``FR(10)``). Reason is to add some space to the labels from the edges. And that's why we had to use ``grid_cell_column_span`` for the widgets in the first row, to take up the space of multiple columns. + +These labels will appear in Home Assistant as `editable text components `__, which makes it very easy to update them with the ``text.set_value`` action. For this purpose, we add the following `automations `__ to Home Assistant: + +.. code-block:: yaml + + - 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": "Except ional", + "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": "Unavai lable", + } %} + {{ 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" + +The automations will be triggered to update the labels every time the corresponding entities change, and when the ESPHome comes alive - the reason you also need the :doc:`/components/binary_sensor/status`. Note that you'll need to adjust the entity IDs corresponding to your ESPHome node depedning on how you :ref:`configured it to use its name`. + +.. _lvgl-cookbook-idlescreen: + +Turn off screen when idle +------------------------- + +LVGL has a notion of screen inactivity -- in other words, the time since the last user interaction with the screen is tracked. This can be used to dim the display backlight or turn it off after a moment of inactivity (like a screen saver). Every use of an input device (touchscreen, rotary encoder) counts as an activity and resets the inactivity counter. Note that it's important to use the ``on_release`` trigger to accomplish this task. With a template number you can make the timeout adjustable by the users. + +.. code-block:: yaml + + 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 + +.. _lvgl-cookbook-antiburn: + +Prevent burn-in of LCD +---------------------- + +You can use this to protect and prolong the lifetime of the LCD screens, thus being more green and generating less hazardous waste. + +A common problem with wall-mounted LCD screens is that they display the same picture 99.999% of the time. Even if somebody turns off the backlight during the night or dark periods, the LCD screen keeps showing the same picture, but seen by nobody. This scenario is likely to lead to burn-in after a few years of operation. + +One way to mitigate this is to *exercise* the pixels periodically by displaying different content. ``show_snow`` option during LVGL paused state was developed with this in mind; it displays randomly colored pixels across the entire screen in order to minimize screen burn-in by exercising each individual pixel. + +In the example below, pixel training is done four times for a half an hour every night; it can be stopped by touching the screen. + +.. code-block:: yaml + + 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: + - delay: 1s + - lvgl.pause: + show_snow: true + turn_off_action: + - logger.log: "Stopping Antiburn" + - if: + condition: lvgl.is_paused + then: + - lvgl.resume: + - lvgl.widget.redraw: + - delay: 1s + - lvgl.pause: + + touchscreen: + - platform: ... + on_release: + then: + - if: + condition: lvgl.is_paused + then: + - lvgl.resume: + - lvgl.widget.redraw: + +You can combine it with the previous example to turn off the backlight, so the users don't actually notice this. + +See Also +-------- + +- :doc:`/components/lvgl/index` +- :ref:`config-lambda` +- :ref:`automation` +- :ref:`key_collector` +- `What is Image Sticking, Image Burn-in, an After Image, or a Ghost Image on an LCD? `__ +- `Image persistence `__ + +- :ghedit:`Edit` diff --git a/index.rst b/index.rst index fb936ba97..e14b79a9f 100644 --- a/index.rst +++ b/index.rst @@ -1155,6 +1155,7 @@ Cookbook .. imgtable:: Lambda Magic: Tips and Tricks, cookbook/lambda_magic, head-lightbulb-outline.svg, dark-invert + LVGL Recipes, cookbook/lvgl, lvgl.png Garage Door Template Cover, cookbook/garage-door, garage-variant.svg, dark-invert Time & Temperature on OLED Display, cookbook/display_time_temp_oled, display_time_temp_oled_2.jpg ESP32 Water Leak Detector, cookbook/leak-detector-m5stickC, leak-detector-m5stickC_main_index.jpg diff --git a/lint.py b/lint.py index 106a4017a..390cdf95a 100644 --- a/lint.py +++ b/lint.py @@ -396,6 +396,7 @@ def lint_directive_formatting(fname, content): exclude=[ "components/web_server.rst", "components/image.rst", + "cookbook/lvgl.rst", ], ) def lint_esphome_io_link(fname, match):