LVGL - making a widget a 'global object/widget' (or something like that)

I have a bunch of the SAME label/button widgets that are used on multiple pages.

Is it possible to define a single widget somewhere within the code that can be used across multiple pages instead of defining each of the same widget with a different id: on each page?

eg: a temp sensor that populates a label widget (with the same format:/arg:) that appears on different pages. With the same id: I wouldnt need to specify each label representing this sensor with a unique id:

The reason for doing this is obviously, in automations, you only need to update the “global” widget and each of these instances in each of the pages gets updated.

I know its possible, but only if pages arent used/defined. I want to do it across pages.

ESPHome has two mechanisms for this !include and packages. It seems like !include would be best for what you are trying to do but I have not tested it.

I would recommend posting your question on the ESPHome Discord page https://discord.com/channels/429907082951524364/429907082955718657. The individuals that are writing the code are there most of the time and are very helpful. Make sure to read the rules about posting code and screen grabs into a thread instead of the main channels.

I’ve tried this and unfortunately the core code still requires a different id for the same widget regardless of which page it is on (or if appears multiple times in the same page)

I do hope there is a solution as the automations for my displays are getting unnecessarily lengthy just updating labels, LEDs and other widgets

How about the top_layer option? If your widget is on all the pages, and in the same location on all pages, it could work.

top_layer (Optional, list): A special kind of Always on Top page, which acts as a parent for widgets placed on it. It’s shown above all the pages, which may be useful for widgets which always need to be visible.

  • layout (Optional): See Layouts for details. Defaults to NONE.
  • widgets (Optional, list): A list of LVGL Widgets to be drawn on the page.
  • All other options from Style properties to be applied to this page.

Unfortunately cant do that.

Some of the widgets I want to use are images that are in the backgrounds of some pages, but not on others.

top_layer renders images on the top of everything else.

Yes read the section on substitutions. That is exactly what these are for

Your includes would look like this

     - button: !include { file: button_template.yaml, vars: { id: 2 } }   

and button_template.yaml

            height: $button_hight_double
            checkable: true
            id:  lv_button_${id}
            widgets:
              - label:
                  text_font: $icon_font
                  align: top_left
                  text: $button_${id}_icon
                  id: lv_button_${id}_icon
              - label:
                  align: bottom_left
                  text: $button_${id}_name
            on_click:
                light.toggle: $button_${id}_HAdevice

Hey thanks for this.

Only issue at the moment is that its not taking any of the styling info as any of the variables.

For instance, it places a blue LED in the top left hand corner x: 1 y: 1 instead of x: 250 y: 100 regardless of whether it is defined in the main_page or the buttons.yaml

Any suggestions?

  pages:
    - id: main_page
      skip: false
      pad_all: 0
      widgets:

        - led: !include
            file: includes/buttons.yaml
            vars: 
              id: try_this
              x: 250
              y: 100

includes/buttons.yaml

widgets:
  - led:
      id: try_this
      color: 0xFF0000
      brightness: 100%
      x: 250
      y: 100

Well, your outer LED doesn’t have any styling so has the default colour and position, and the inner led, which is a child of the outer one, is positioned outside the parent’s bounds, so is not visible. Do you actually want two LEDs?

You can already update multiple widgets with a single lvgl.<widget>.update action - just list the ids of the widgets as an array on the id: key in the action. They will all be updated with the same data.

Sorry Clyde… I don’t quite understand what you mean?

In simplistic terms, Are you saying because I have two widgets: defined I actually have two LEDs?

I tried buttons.yaml without widgets defined and it wouldnt compile unfortunately w a million errors.

Yes, you are defining one LED in the main yaml, then adding another one as a child in the included file. Each - led: entry creates a widget.

If I was a little obtuse in my earlier reply, it’s because I don’t know exactly what you are trying to do. Maybe if you explain in more detail what the end goal is we can help more.

Hey @clydebarrow

Not obtuse, I had not explained the request in enough granularity.

Let me post two examples:
excuse syntax and indentation… all code is hypothetical and just used for the sake of example

Example1: Every label and display must have a unique ID which when making updates in automations make the code lengthy with all the updates that are needed across multiple pages.

lvgl:
  displays:
    - display_id: display

  touchscreens:
    - touchscreen_id: touchscreen

  style_definitions:
    - id: style_line
      line_color: 0x0000FF
      ...
 
  theme:
    button:
    ...

  pages:
    - id: page1
      widgets:
        - image:
            id: page1_background
            src: display_background_on
        - label:
            id: page1_dallas_temp_sensor_label

    - id: page2
# these widgets are essentially the same as the widgets on page1
      widgets:
        - image:
            id: page2_background
            src: display_background_on
        - label:
            id: page2_dallas_temp_sensor_label

    - id: page3
# these widgets are essentially the same as the widgets on page1
      widgets:
        - image:
            id: page3_background
            src: display_background_on
        - label:
            id: page2_dallas_temp_sensor_label

sensor: 
  - platform: dallas_temp
    name: temp_probe
    update_interval: 1s
    id: temp1
...
    on_value:
      then:
# must independently update 6 widgets
        - lvgl.label.update:
            id: page1_dallas_temp_sensor_label
            text:
              format: "%.1f"
              args: id(temp1).state
        - lvgl.image.update:
            id: page1_background
            src: display_background_off      
        - lvgl.label.update:
            id: page2_dallas_temp_sensor_label
            text:
              format: "%.1f"
              args: id(temp1).state
        - lvgl.image.update:
            id: page2_background
            src: display_background_off      
        - lvgl.label.update:
            id: page3_dallas_temp_sensor_label
            text:
              format: "%.1f"
              args: id(temp1).state
        - lvgl.image.update:
            id: page3_background
            src: display_background_off 

Example2: If a global widget can be specified then then updating that widget is expedient, and the single widget can be used across multiple pages.

lvgl:
  displays:
    - display_id: display

  touchscreens:
    - touchscreen_id: touchscreen

  style_definitions:
    - id: style_line
      line_color: 0x0000FF
      ...
 
  theme:
    button:
    ...

# WHAT I AM SUGGESTING AS THE GLOBAL DEFINITION OF WIDGET
  widgets:
    image:
      id: global_background
      src: display_background_on
    label:
      id: global_dallas_temp_sensor_label

  pages:
    - id: page1
# using the global widget definition as stated above
      widgets:
        - image:
            id: global background
            src: display_background_on
        - label:
           id: global_dallas_temp_sensor_label

    - id: page2
      widgets:
# using the global widget definition as stated above
        - image:
            id: global background
            src: display_background_on
        - label:
           id: global_dallas_temp_sensor_label

    - id: page3
      widgets:
# using the global widget definition as stated above
        - image:
            id: global background
            src: display_background_on
        - label:
           id: global_dallas_temp_sensor_label

sensor: 
  - platform: dallas_temp
    name: temp_probe
    update_interval: 1s
    id: temp1
...
    on_value:
      then:
# Only need to update the global widget and all changes are reflected across all pages.
        - lvgl.label.update:
            id: global_dallas_temp_sensor_label
            text:
              format: "%.1f"
              args: id(temp1).state
        - lvgl.image.update:
            id: global_background
            src: display_background_off 

I do understand that in the docs, it states that this can be achieved however there is one major caveat. Cant be used with pages.

widgets (Optional, list): A list of LVGL Widgets to be drawn on the root display. May not be used if pages (below) is configured.

I was thinking that the solution provided @andrew_NH utilizing !include: may have been a solution to the problem, either I dont know how to reference the widget properly, or, it is not possible at this point and automations to update what is essentially the same widget across multiple pages will need to remain lengthy. :frowning:

I do hope this makes more sense.

Oh and a massive thank you for all you are doing for this integration.

I follow you updates in esphome github daily to see if there are any changes

2 Likes

Using !include (which is a non-standard YAML extension for ESPHome) is possible to replicate widgets in different places. It has a limitation, as far as I can tell, that it can basically only be used to replace values or value/key pairs, not list entries, which is a problem when you want to append to a widget list. Something like this works, but isn’t ideal because the included widgets have to be wrapped in an obj since the include can only replace an entire widget list, not append to it.

page_template.yaml:

- image:
    align: center
    id: page${id}_background
    src: left_img
    on_click:
      lvgl.page.next:
- label:
    align: top_mid
    id: page${id}_label
    text: Nix
- label:
    align: center
    text: Page ${id}

top-level yaml:

  pages:
    - id: page_1
      pad_all: 0
      widgets:
        - obj:
            width: 100%
            height: 100%
            widgets: !include { file: page_template.yaml, vars: { id: 1 } }

    - id: page_2
      pad_all: 0
      widgets:
        - obj:
            width: 100%
            height: 100%
            widgets: !include { file: page_template.yaml, vars: { id: 2 } }

So it may be just as easy to simply replicate the common widgets on each page, depending on how many there are.

The good news is that however they are defined, it’s easy to update all of them at once - you can update multiple widgets of the same kind at the same time with a single action:

      - lvgl.label.update:
          id: [page1_label, page2_label]
          text:
            format: "Value %d"
            args: ['(int)((float)rand() / RAND_MAX * 100)']
      - lvgl.image.update:
          src: left_img
          id: [page1_background, page2_background]

Personally for your use-case I would probably not use pages for the views that have common elements, instead layout the screen with with the common elements fixed in place, and a tabview or tileview widget for the variable part. That could either occupy the entire screen with the common stuff on top, or take up part of the screen.

Those widgets also have the benefit of responding to swipes which pages don’t.

You could still use pages below that if you have other views that don’t include the common elements of course, just using a single page for the stuff we’ve been discussing.

2 Likes

hey thanks for the help.

Learned a lot just in your one post.

Im going to play around with this over the weekend - I will let u know the result.

So Im discovering the limitations of this and some of it may be that the integration is so NEW.

So exciting!!!

Discoveries:

display.yaml

        - label:
            hidden: false
            width: 480
            height: 480
            x: 150
            y: 150
            bg_opa: TRANSP
            border_width: 0
            widgets: !include { file: elements/real_error_label.yaml }

real_error_label.yaml

- label:
    id: tank_error_label
    text_font: barlow120
    text_color: 0xFFFFFF
    width: 480
    height: 480
    bg_opa: TRANSP
    text_align: CENTER
    x: 140
    y: 100
    hidden: false
    text: "ERROR"

All discoveries are anecdotal at the moment…

1: positioning of label w x:y: produces a cumulative++random position of the label at x: 290 y: 250 (but its more like x: 350 y: 300)

2: all widgets defined in a include must be full screen, styling gets applied like its a full screen widget.

3: any !include widgets are placed at a position of 0,0 and scroll bars appear if you position your information outside the bouds of the size of the !include widget.

3: lvgl style parameters dont always work the way I thought they would. Extreme trial and error is needed.

…more to come!

next observation:

!include causae blocking delays:

[16:46:11][W][component:237]: Component lvgl took a long time for an operation (92 ms).
[16:46:11][W][component:238]: Components should block for at most 30 ms.

remove them and put the base widget code into the base yaml file and they go away.

Which is exactly what you specified. 150+140=290. Not at all random. The text itself will be centred within the rather large label (itself offset within its parent label), so depending on the font and screen size will likely be at least partially off the screen. That might not be what you wanted, but it’s what you asked for.

With regard to the behaviour of !include it is not part of the LVGL component, it’s simply a textual substitution mechanism within ESPHome’s implementation of YAML. Anything you do with !include will behave in the same way as as if you wrote the same code inline in the same place.