Advanced Jinja2 Templating for fun and profit

Tags: #<Tag:0x00007f4654019630> #<Tag:0x00007f46540194f0>

I’m working on building some templates for my use. I have a lot of Jinja2 experience, so I thought I would share some with the community to use as a cookbook of sorts.

Here’s the first. This is a template that prints the name of all device_trackers that are marked at home and not_home with proper human string formatting.

{% for device in dict(states.device_tracker|groupby('state'))['home'] %}{% if loop.last and loop.length > 1 %} and {% elif not loop.first and loop.length > 1 %}, {% endif %}{{ device.name }}{% endfor %} {% if dict(states.device_tracker|groupby('state'))['home']|length == 1 %}is{% else %}are{% endif %} at home.

{% for device in dict(states.device_tracker|groupby('state'))['not_home'] %}{% if loop.last and loop.length > 1 %} and {% elif not loop.first and loop.length > 1 %}, {% endif %}{{ device.name }}{% endfor %} {% if dict(states.device_tracker|groupby('state'))['not_home']|length == 1 %}is{% else %}are{% endif %} away.

Example output:

Robbie is at home.

Katie is away.

Breaking this template up to be a bit more readable (note that by doing this you will get funky spacing in the output!):

# Loop over devices with a state of not_home
{% for device in dict(states.device_tracker|groupby('state'))['not_home'] %}
  # If this not the last device add a comma. Otherwise, use the word "and". Don't do either if there's only 1 device
  {% if loop.last and loop.length > 1 %} and {% elif not loop.first and loop.length > 1 %}, {% endif %}
  # Print the device name
  {{ device.name }}
{% endfor %} {% if dict(states.device_tracker|groupby('state'))['not_home']|length == 1 %}is{% else %}are{% endif %} away.
# The line above will output 'is' or 'are' depending on how many devices are in the list.
6 Likes

thanks for sharing. but where do i put this code? in the configuration.yaml file? Where does the output appear? Can it shows as message in email notification?

Yes, it can appear in an email. If you currently have that setup you can just add it under message: in your config. You can read more about templates and notifications, here and here. :cat:

I see. I still couldn’t find the easiest way to write the code for notification when someone is home or away. So far this is what I did…

automation:
- alias: 'Ben is Home notification'
  trigger:
    platform: state
    entity_id: device_tracker.ben
    from: 'not_home'
    to: 'home'
  action:
    service: notify.email
    data:
      message: "Ben is home"
- alias: 'Luke is Home notification'
  trigger:
    platform: state
    entity_id: device_tracker.luke
    from: 'not_home'
    to: 'home'
  action:
    service: notify.email
    data:
      message: "Luke is home"
- alias: 'Ben is Away notification'
  trigger:
    platform: state
    entity_id: device_tracker.ben
    from: 'home'
    to: 'not_home'
  action:
    service: notify.email
    data:
      message: "Ben is away"
- alias: 'Luke is Away notification'
  trigger:
    platform: state
    entity_id: device_tracker.luke
    from: 'home'
    to: 'not_home'
  action:
    service: notify.email
    data:
      message: "Luke is away"

I have to repeat the same code if I want to add more devices. This is a real pain the back.

Thankyou for sharing your knowledge

I am struggling with iterating or relating to a group of devcies (not all devices)

I have a group called family that is 5 device_tracker devices PLUS another non-device_tracker object (an input_select object)

How can I iterate only through device_tracker states for the group group.family?

group.family has a state of home or not_home - which is great. I just want to iterate through only the device_tracker states in that group and identify who is home so I can use it in a notification.

Any suggestions would be appreciated.

I have tried state group.family and other variants - my issue is the jinga syntax for how to interact with the objects in a group

1 Like

Try the following single automation to replace the 4 you made:

automation:
- alias: "Presence notification"
  trigger:
    platform: state
    entity_id: device_tracker.ben, device_tracker.luke
  action:
    service: notify.email
    data:
      message: >
        {% set person = trigger.entity_id.split('.')[1] | capitalize %}
        {% if trigger.to_state.state == 'home' %}
          "{{ person }} is home"
        {% else %}
          "{{ person }} is away"
        {% endif %}

With this code, anytime you need to track an additional device, you just add it to entity_id: under trigger:.

Disclaimer: I have limited ways to test this code, so it may contain errors.

Edit: I just noticed the post I replied to is 4 months old…my bad! But oh well, it may still come in handy for somebody else :innocent:

5 Likes

@fanaticDavid

To dig up an old thread again is this still supported?

I keep getting errors in my log say trigger is not defined.

Based on the documentation, I would say it is. But I think there is a mistake in my code above: replace data with data_template

@robbiet480

sorry for digging up this old thread. but your example is really useful to me for an automation I am planning to do. You see, I have magnetic sensors on every windows and doors (interior and exterior). I want HA to send me a list of windows or doors (only the exterior) that are still open when it is raining. If I use this code…

{% for device in dict(states.sensor|groupby('state'))['open'] %}{% if loop.last and loop.length > 1 %} and {% elif not loop.first and loop.length > 1 %}, {% endif %}{{ device.name }}{% endfor %} {% if dict(states.sensor|groupby('state'))['open']|length == 1 %}is{% else %}are{% endif %} open.

…it will list down all sensors that have state = open including the interior sensors. How do I exclude the interior sensors from this list?

I created a group; group.exteriorwindows and add all the relevant sensors into this group. how do I use this group in your example?

Your help will be greatly appreciated. Thank you.

1 Like

Digging up this thread as I have the same question. I seem to be only able to iterate through the entire catergory of devices/sensors/etc. I would like to either have excludes, or more ideally, be able to iterate through a group created in Home Assistant.

For example, this works on the HA Templating tester:

{% for state in states.group -%}
  {%- if loop.first %}The {% elif loop.last %} and the {% else %}, the {% endif -%}
  {{ state.name | lower }} is {{state.state}} {{- state.attributes.unit_of_measurement}}

But if I try to drill down to a specific group, it fails:

{% for state in states.group.family -%}
  {%- if loop.first %}The {% elif loop.last %} and the {% else %}, the {% endif -%}
  {{ state.name | lower }} is {{state.state}} {{- state.attributes.unit_of_measurement}}

Any ideas @robbiet480 ?

Try here…

1 Like

Thanks I’ll take a look!

So in the end I used @robbiet480 's idea plus a little modification to iterate through only the windows of my binary_sensors and report which are open:

# Iterate through binary_sensors which are windows, and that are on (i.e. open)
{% for state in states.binary_sensor if 'Window' in state.name and state.state == 'on' %}
  # If this not the last device add a comma. Otherwise, use the word "and". Don't do either if there's only 1 device
  {%- if loop.last and loop.length-1 > 1 %} and 
    {% elif not loop.first and loop.length-1 > 1 %}, 
    {% endif -%}{{ state.name }}
  # If this is the last loop and it has looped more than once, use "are", otherwise use "is".
  {%- if loop.last %}
    {%- if loop.length-1 == 1 %} is 
      {% else %} are 
    {% endif -%} open.
  {% endif -%}
{% endfor %}

What’s weird is I had to remove 1 from loop.length as it was never smaller than 2, even with only one window open. Oh and loop.length bugs out if I try to combine the two ifs at the end. Open to suggestions if I’m doing something wrong.

I use device_class on my binary sensors to find windows and doors.

Found this thread whilst searching for info on Hassio jinja2 coding, wished you’d kept this going @robbiet480 We need a ‘cookbook of sorts’, I can’t find anything on the internet like that. I seem learn more by looking through code, working and adapting it for my own use.

I’m not sure where you are looking but there is about 8 bazillion template examples all across this forum and even in the cookbook docs

2 Likes

Every time I try something with {%- macro lalalala() -%} I get errors.
Is there any custom component needed to use macro?

you need to finish the macro, and macros need to be created in every field that it is used

{%- macro lalalala() %}
Lalalala
{%- endmacro %}
{{ lalalala() }}

Sorry for the questions. Ill try to understand the way of working. :slight_smile: want learn

When I try this in developer Tools > Template its working:

{%- macro message() -%}
  {{ ["The sun is down, so the romantic evening is started!",
  "The sunset has begun, i turn on the lights for you",
  "I think it will begun dak inside the house, let me turn o",
  "Make some tea, i will help you with the lights",
  "Sun?, sun?, wherever you are? Ill turn on the lights inside",
  "There are no lights on! so let me do it for you!"
  ] | random }}
{%- endmacro -%} 

"{{ message() }}"

When I add this into a test automation I get error:


while scanning for the next token
found character '%' that cannot start any token
  in "/config/config/automation/test/test.yaml", line 15, column 2

Here the automation to test my yaml script:

- alias: TEST BUTTON ON 1
  trigger:
    - platform: state
      entity_id: switch.newkaku_014e5906_a
      to: 'on'

  action:
  - service: script.turn_on
    entity_id: script.alexa_speak_engine
    data_template:
      variables:
#        volume: '0.5'
        message: "{{ message() }}"

{%- macro message() -%}
  {{ ["The sun is down, so the romantic evening is started!",
  "The sunset has begun, i turn on the lights for you",
  "I think it will begun dak inside the house, let me turn o",
  "Make some tea, i will help you with the lights",
  "Sun?, sun?, wherever you are? Ill turn on the lights inside",
  "There are no lights on! so let me do it for you!"
  ] | random }}
{%- endmacro -%}

"{{ message() }}"

when I remove the macro part and add a message “this is a test” I hear the message at my Alexa Echo.

You have to have the macro built into the field. You can’t place it in a different location and expect it to work anywhere in your config. This is a limitation of jinja and yaml

- alias: TEST BUTTON ON 1
  trigger:
    - platform: state
      entity_id: switch.newkaku_014e5906_a
      to: 'on'

  action:
  - service: script.turn_on
    entity_id: script.alexa_speak_engine
    data_template:
      variables:
#        volume: '0.5'
        message: >
          {%- macro message() -%}
            {{ ["The sun is down, so the romantic evening is started!",
            "The sunset has begun, i turn on the lights for you",
            "I think it will begun dak inside the house, let me turn o",
            "Make some tea, i will help you with the lights",
            "Sun?, sun?, wherever you are? Ill turn on the lights inside",
            "There are no lights on! so let me do it for you!"
            ] | random }}
          {%- endmacro -%}
          {{ message() }}

So to be honest, a macro wouldn’t be useful at this point because you aren’t calling it more than once.

- alias: TEST BUTTON ON 1
  trigger:
    - platform: state
      entity_id: switch.newkaku_014e5906_a
      to: 'on'

  action:
  - service: script.turn_on
    entity_id: script.alexa_speak_engine
    data_template:
      variables:
#        volume: '0.5'
        message: >
            {{ ["The sun is down, so the romantic evening is started!",
            "The sunset has begun, i turn on the lights for you",
            "I think it will begun dak inside the house, let me turn o",
            "Make some tea, i will help you with the lights",
            "Sun?, sun?, wherever you are? Ill turn on the lights inside",
            "There are no lights on! so let me do it for you!"
            ] | random }}
1 Like