Fun with lovelace_gen and jinja2 macros to avoid whitespace indentation problems

I’ve been using the wonderful lovelace_gen by @thomasloven to assemble my YAML lovelace configuration from a series of files. This is really handy!

Recently, I was playing around with this, and as you might expect, you find yourself doing the same thing over and over for different UI elements. And I wanted to avoid having to repeat all that boilerplate.

And then I remembered that YAML includes the ability to represent data as JSON objects… Hmm… I thought - this was a way to use jinja2 macros in a general way without having to worry about the whitespace indentation level at any particular point.

Let me illustrate with the first part of a lovelace page configuration. It demonstrates 3 ways of representing the same type of lovelace card specification. It begins with a jinja2 macro definition that’s used for the last alternative:

# lovelace_gen
{% macro camera(_entity, _live='auto') -%}
  {
   "type": "picture-entity",
   "camera_view": "{{_live}}",
   "entity": "{{_entity}}"
  }
{%- endmacro %}

title: Weather
badges: []
panel: true
cards:
  - type: vertical-stack
    cards:
      - type: horizontal-stack
        cards:
          - type: picture-entity
            entity: camera.cam8
          - {
"type": "picture-entity",
 "entity": "camera.cam7"
 }
          - {{ camera('camera.cam9') }}

Here there are 3 cameras in picture-entity cards, within a horizontal-stack element. The first is defined in the usual way.

The second one just represents the dictionary in JSON syntax. The indentation of the JSON structure is irrelevant! Often you see this style syntax all collapsed on a single line.

The third one uses a macro to generate the same JSON syntax.

So in this way, it ought to be easy to write some jinja2 macros to generate complex lovelace card definitions and incorporate their use in arbitrary places in your lovelace configuration. I started down this path while I was playing around with the powerful custom button-card as a way to augment or replace the templating capability there.

5 Likes

Of course! Why didn’t I think of that?

Mind if I add this trick to the readme next time I update it?

2 Likes

Please do! This suddenly came to me in the shower one morning, and I rushed to try it out with success. I think it really opens up a bunch of options to help manage lovelace configuration YAML.

Thanks for all the wonderful work you’ve done with all those lovelace cards! I’m glad this is helpful in some small way.

Is this still relevant? I did not see it in the lovelace_gen readme.

Lovelacegen is a custom integration. It still works.

I meant the JSON trick mentioned above. As it is not in the readme…

I’m still using this, though I’ve not upgraded to the 2023.4.x version of Home Assistant yet, though not in a big/pervasive way.

I’m not sure how this new templating capabiliy might affect the sequencing of various components being loaded in Home Assistant. I’d like to have a solution to the Lovelace dashboard errors that get puked out before lovelace_gen is loaded as the various web UIs try to re-render after a Home Assistant restart.

that will always work, yaml and json are interchangeable. That’s yaml functionality, not lovelace_gen functionality.

e.g.

foo:
  bar:
    foobar: barfoo

can always be written as

foo: {'bar': {'foobar':'barfoo'}}

This can be done in normal yaml with any section in HA or outside HA’s ecosystem.

1 Like

Can I ask how this works with something like:

                tap_action:
                  action: |-
                    [[[
                      return states['[[entity]]'].state == 'unavailable' ? 'none' : 'call-service';
                    ]]]
                  service: |-
                    [[[
                      return (states['[[entity]]'].attributes.preset_mode == "temporary" || states['[[entity]]'].attributes.preset_mode == "permanent") ? 'climate.set_preset_mode' : 'climate.set_hvac_mode';
                    ]]]
                  service_data: |-
                    [[[
                      if (states['[[entity]]'].attributes.preset_mode == "temporary" || states['[[entity]]'].attributes.preset_mode == "permanent") {
                        return "{ 'preset_mode': 'none', 'entity_id': '[[entity]]' }";
                      } else if (states['[[entity]]'].state != "unavailable") {
                        if ('[[state]]' == "on") {
                          return "{ 'hvac_mode': 'off', 'entity_id': '[[entity]]' }";
                        } else {
                          return "{ 'hvac_mode': 'heat', 'entity_id': '[[entity]]' }";
                        }
                      }
                    ]]]

It doesn’t work - it’s looks like it’s passing the whole string rather than ‘interpreting’ the JSON.

Thanks
Andy

that’s because you’re returning a string. "blah" is a string blah is the variable blah.

Brilliant, thanks for your help.

This is brilliant.
Any ideas how to include !include in the json? tried everything and can’t get it to work:

The yaml (works):

        tap_action:
          action: fire-dom-event
          browser_mod:
            service: browser_mod.popup
            data:
              dismissable: true
              autoclose: false
              title: FireTV
              content:
                type: custom:stack-in-card
                cards: 
                  - !include
                    - {{popupfile}}
                    - {{params}}

I want to pass that whole block to another !include and use it as a variable!
If I could wrap it as a json macro somehow…
Stuck,

You don’t use includes, you just use JSON as is wherever you need it. Yaml accepts JSON.

Sorry, I wasn’t clear enough.

I have 3 relevant files:

  1. My main view: porch.yaml
  2. A helper that produces a row of buttons: footer-grid.yaml
  3. A view used in a popup: porch-tv2.yaml

The idea is that I !include footer-grid.yaml and pass it a json object via a variable that tells it what buttons I want and what to do when they are pressed. It works (mostly) and looks like this:

{% set footer_data = [
        {"icon": "mdi:home", "background": "aliceblue", 
          "tap_action": {"action": "navigate", "navigation_path": "/"}
        },
        {"icon": "mdi:air-conditioner", "entity":"climate.porch_ac_windfree_3_0e", "margin": "8px",
          "tap_action": {
            "action": "call-service",
            "service": "climate.toggle",
            "service_data": {
              "entity_id": "climate.porch_ac_windfree_3_0e"
            }
          }
        },
        {"icon": "si:prime", "size": "40px", "color": "rgb(0,168,225)",
          "tap_action": {
              "action": "fire-dom-event", 
            }
        },
        {"icon": "mdi:lightbulb-outline", "entity":"light.porch_lights","margin": "8px"},
        {"icon": "mdi:home", "background": "aliceblue" }
        ] 
      %}
      - !include
          - ../components/footer-grid.yaml
          - footer_data: {{footer_data}}
            layout_margin: "0px 0 0px 0"

In footer-grid.yaml, the relevant code is:

{% for item in footer_data %}
  - type: custom:button-card
    icon: {{ item.icon or "mdi:help" }}
    show_name: false
    entity: {{item.entity}}
    {% if item.tap_action %}
    tap_action:
      {{ item.tap_action }}
      {% set tap_action = item.tap_action %}
      {% if tap_action.action == "fire-dom-event" %}
          # can't make this work
      {% endif %}
    {% endif %}
    card_mod:
      style:
         ....card_mod stuff...
{% endfor %}

The loop iterates through the passed in array and creates buttons. Where needed it creates the passed in tap_action stuff.

Where I am stuck is one button (the one with the si:prime icon), needs to trigger a popup that !includes porch-tv2.yaml. The code for the tap_action is:

tap_action:
      action: fire-dom-event
      browser_mod:
          service: browser_mod.popup
          data:
            dismissable: true
            autoclose: false
            title: FireTV
            content:
              type: custom:stack-in-card
              cards:
                - !include
                  - porch-tv2.yaml              #{{popupfile}}
                  - {"nopopup": true, "nofooter": true} #{{params}}

If I just test that in porch.yaml, it works great. But I need to figure out a way to pass it to the !included footer-grid.yaml.

So, for my climate button, the json is:

{"icon": "mdi:air-conditioner", "entity":"climate.porch_ac_windfree_3_0e", "margin": "8px",
          "tap_action": {
            "action": "call-service",
            "service": "climate.toggle",
            "service_data": {
              "entity_id": "climate.porch_ac_windfree_3_0e"
            }
          }
        },

I need the equivalent json for the popup code above. But the !include screws everything up.

Finally, I thought I could create a macro (for the popup tap_action) and pass that over – but couldn;t get that to work either.

Sorry it’s probably still unclear!