Best way to use common display elements in lambda?

Hi all, still getting to grips with using ESPHome. I’m currently building out my M5Stack Basic with BTC v2.1 base (contains an SHT30).

The M5Stack Basic has a nice screen display and 3 buttons. I want to have multiple different pages showing different data and I’ve made a good start on that.

However, I really want some common elements on every page and I want some common formatting. I would really love to move those common parts to a separate, reusable file.

So can anyone share the best approach for this?

I’ve tried moving some code to an included .h library file but that doesn’t get access to the it object even if I include #include "esphome.h". Though I can, at least, use that file to define common variables such as the line spacing, header depths, etc.

Here is an example from one of the pages - only showing output from the local sensor and one other (via MQTT) at the moment. But you can see the start of my standard header (showing WiFi status and date/time) and footer (that will eventually show the button actions).

- id: page1
  # Initial page. Header (Pg, status, time), Footer (buttons)
  lambda: |-

    /* ---- Header ---- */
    auto nowtime = id(sntp_time).now();
    auto wifiStatus = WiFi.status();
    it.filled_rectangle(0,0, it.get_width(), HEADER_HT, id(COLOR_CSS_DEEPSKYBLUE));
    if (WiFi.status() == WL_CONNECTED) {
      it.print(TEXT_BLOCK_1, HEADER_Y, id(icon_font), id(COLOR_CSS_BLACK), TextAlign::LEFT, "");
    } else {
      it.print(TEXT_BLOCK_1, HEADER_Y, id(icon_font), id(COLOR_CSS_RED), TextAlign::LEFT, "");
    }
    if (nowtime.is_valid()) {
      it.strftime(it.get_width()-1, HEADER_Y, id(print_font), id(COLOR_CSS_BLACK), TextAlign::TOP_RIGHT, "%Y-%m-%d %H:%M", nowtime);
    } else {
      it.print(it.get_width()-1, HEADER_Y, id(print_font), id(COLOR_CSS_BLACK), TextAlign::TOP_RIGHT, "----/--/-- --:--");
    }
    // ---- Footer ----
    it.filled_rectangle(0,it.get_height()-FOOTER_HT, it.get_width(), FOOTER_HT, id(COLOR_CSS_LIGHTBLUE));
    

    // ---- Main ----

    // Here
    it.print(TEXT_BLOCK_1, LINE_1, id(print_font), id(COLOR_CSS_WHITE), TextAlign::TOP_LEFT, "Here");
    it.print(TEXT_BLOCK_1+6, LINE_2, id(icon_font), id(COLOR_CSS_WHITE), TextAlign::TOP_CENTER, "");
    it.print(TEXT_BLOCK_1+6, LINE_3, id(icon_font), id(COLOR_CSS_WHITE), TextAlign::TOP_CENTER, "");
    it.printf(TEXT_BLOCK_1+LINE_SIZE, LINE_2, id(print_font), id(COLOR_CSS_WHITE), TextAlign::LEFT, "%.1f°C", id(temperature).state);
    it.printf(TEXT_BLOCK_1+LINE_SIZE, LINE_3, id(print_font), id(COLOR_CSS_WHITE), TextAlign::LEFT, "%.1f%%", id(humidity).state);

    // Landing (D1M04)
    double t_float = strToFloat( id(d1m04_temperature).state ); 
    double h_float = strToFloat( id(d1m04_humidity).state );
    it.print(TEXT_BLOCK_2, LINE_1, id(print_font), id(COLOR_CSS_WHITE), TextAlign::TOP_LEFT, "Landing");
    it.print(TEXT_BLOCK_2+6, LINE_2, id(icon_font), id(COLOR_CSS_WHITE), TextAlign::TOP_CENTER, "");
    it.print(TEXT_BLOCK_2+6, LINE_3, id(icon_font), id(COLOR_CSS_WHITE), TextAlign::TOP_CENTER, "");
    it.printf(TEXT_BLOCK_2+LINE_SIZE, LINE_2, id(print_font), id(COLOR_CSS_WHITE), TextAlign::LEFT, "%.1f°C", t_float);
    it.printf(TEXT_BLOCK_2+LINE_SIZE, LINE_3, id(print_font), id(COLOR_CSS_WHITE), TextAlign::LEFT, "%.1f%%", h_float);

    // Bathroom
    t_float = strToFloat( id(bathroom_temperature).state ); 
    h_float = strToFloat( id(bathroom_humidity).state );
    it.print(TEXT_BLOCK_3, LINE_1, id(print_font), id(COLOR_CSS_WHITE), TextAlign::TOP_LEFT, "Bathroom");
    it.print(TEXT_BLOCK_3+6, LINE_2, id(icon_font), id(COLOR_CSS_WHITE), TextAlign::TOP_CENTER, "");
    it.print(TEXT_BLOCK_3+6, LINE_3, id(icon_font), id(COLOR_CSS_WHITE), TextAlign::TOP_CENTER, "");
    it.printf(TEXT_BLOCK_3+LINE_SIZE, LINE_2, id(print_font), id(COLOR_CSS_WHITE), TextAlign::LEFT, "%.1f°C", t_float);
    it.printf(TEXT_BLOCK_3+LINE_SIZE, LINE_3, id(print_font), id(COLOR_CSS_WHITE), TextAlign::LEFT, "%.1f%%", h_float);

“it” should be of the type DisplayBuffer.

So when you write your function in your include like so:

void drawFooter(DisplayBuffer& it) {
   // do things here to draw the footer
}

you can call it from a lambda in the yaml like:

  lambda: |-
     drawFooter(it);
1 Like

Thank you for that. My C++ skills are pretty useless I’m afraid so I struggle with those parts. Too many years spent working with languages that help rather than hinder :slight_smile:

This will really help me move onto the next stage.

I also want to create a generic function that will allow messages to be overlaid on the display based on an incoming MQTT message. That is quite exciting because it means that I can do alarms and other notifications very simply just as I do with Telegram (where I have a bot in Node-RED but have a standard set of MQTT topics that let me send messages very easily).

I intend, if I can, to even allow simple Markdown formatting just as you can with Telegram messages.

Nice.

As I am using a “display” (four 32x8 LED panels glued together) to show messages triggered by MQTT I am interessted to see what you come up with. So please poste your results/findings.

No problem. Initial config mostly shared here so far:

Might be a bit of a gap until I can pick this up again because I may need to do some work on one of my other open source projects. However, that display is on my desk. It turns itself on every 5 minutes for a minute to show some temperature and humidity readings from around the house. But I really do want to use it for other notifications. For example, I have a wireless doorbell that is connected via Node-RED to MQTT. I get a telegram notification when the bell is rung but I want to get that as an alert. But there are plenty of other alerts that I’m interested in such as internet drop-outs, sensors dropping offline, maybe notifications about key incoming emails from important people. Definitely I want to be able to have a programmable alarm function maybe even linked back to a Google Calendar. All cool stuff. :slight_smile:

I just need to find some spare time!

Has there been any progress on this? I’m looking to get something similar done but on detecting a page change

Not by me I’m afraid. Too many other things going on and I’ve not updated my ESPHome stuff in quite a while. Would still like to do it but I’ve been much more focused on my main open source project, uibuilder for Node-RED. So my sensor platforms have all been on the back-burner.

Following a recent example from the ESPHome ready-made voice assistants, assuming that your display has id: interface_display, you can write a script that includes in its lambda references to id(interface_display)'s various drawing functions in lieu of it. Example:

script:
  - id: draw_display_borders
    then:
      - lambda: |
          id(interface_display).rectangle(0, 0, id(width), id(height), id(border));
          id(interface_display).line(id(width) * 0.25, id(height) * 0.75, id(width) * 0.25, id(height) * 0.9, id(border));
          id(interface_display).line(id(width) * 0.5, id(height) * 0.75, id(width) * 0.5, id(height) * 0.9, id(border));
          id(interface_display).line(id(width) * 0.75, id(height) * 0.75, id(width) * 0.75, id(height) * 0.9, id(border));
          id(interface_display).line(id(width) * 0.25, id(height) * 0.75, id(width), id(height) * 0.75, id(border));
          id(interface_display).line(0, id(height) * 0.9, id(width), id(height) * 0.9, id(border));

Then in your display’s lambda, execute the relevant script:

display:
  - id: interface_display
    ...
    pages:
      - id: ...
        lambda: |-
          ...
          id(draw_display_borders).execute();

I use this to draw the same set of borders on multiple pages of interface_display. Through this technique, you can also draw dynamic elements into the display using a standard layout.

2 Likes

That’s a useful idea. Thanks for sharing.

I was trying to do the same thing with Header and Footer. Ended up doing it like this:

substitutions:
  friendly_name: Inkplate Hallway
  devicename: inkplate_hallway

globals:
  - id: winter_bool
    type: bool
    restore_value: true
    initial_value: 'true'

switch:
# Virtual switch based on a global variable.
  - platform: template
    name: "Winter Mode"
    icon: mdi:snowflake
    id: winter_mode
    restore_mode: RESTORE_DEFAULT_ON
    turn_on_action:
      - globals.set:
          id: winter_bool
          value: 'true'
      - component.update: ${devicename}_display
    turn_off_action:
      - globals.set:
          id: winter_bool
          value: 'false'
      - component.update: ${devicename}_display
    lambda: |-
      return id(winter_bool);

display:
- platform: inkplate6
  id: ${devicename}_display

lambda: |-
  lambda: |-
      // -- Header --\\

	// do things you want in footer

     // -- Header end --\\
      // -- Footer --\\

	// do things you want in footer

    // -- Footer end --\\

    if (id(winter_mode).state) {

      // -- Winter -- \\

	// do things you want in "page1"

      // -- Winter end -- \\
    } else {
      // -- Summer -- \\

	// do things you want in "page2"

      // -- Summer end -- \\
      }