Question about the jinja "apply" function

Thanks to the new jinja documentation, I learned about custom filters and the apply function. Experimenting with it, I discovered that apply doesn’t work with specific Home Assistant functions/tests, such as has_value, states, is_state, etc.

Specifically, apply doesn’t seem to pass the first argument named “value” to these functions. I wonder if this isn’t some legacy thing, as there’s no obvious visible difference in how HA-specific functions accept arguments vs. built-in functions.

Below is an example comparing the built-in float function and the special state_attr function (both accept 2 arguments).

  1. Standard function call:
{{ float('abc',5) }} {# result: 5 #}
{{ state_attr('sun.sun','elevation') }} {# result: 41.54 #}
  1. Call via apply:
{{ apply('abc',float,5) }} {# result: 5 #}
{{ apply('sun.sun',state_attr,'elevation') }} {# result: TypeError: state_attr() missing 1 required positional argument: 'name' #}

The value argument of apply (‘sun_sun’) was ignored and ‘elevation’ was passed to state_attr as entity_id. I can confirm it with:

{{ apply('sun.sun',state_attr,'sun.sun','elevation') }} {# result: 41.54 #}
{{ apply('this is ignored',state_attr,'sun.sun','elevation') }} {# result: 41.54 #}
  1. Passing arguments

Now to the more important stuff (I think). A more general way of passing arguments also works with HA-specific functions:

{{ state_attr(*['sun.sun','elevation']) }}
{{ state_attr(**{'entity_id':'sun.sun','name':'elevation'}) }}
{{ state_attr('sun.sun',*['elevation']) }}
{{ state_attr('sun.sun', **{'name':'elevation'}) }}

and similarly via the apply function, e.g.

{{ apply('ignored',state_attr,*['sun.sun','elevation']) }}
{{ apply('ignored',state_attr,'sun.sun', *['elevation']) }}
{{ apply('ignored',state_attr,'sun.sun', **{'name':'elevation'}) }}

So it seems to me, it should not be hard making these special functions also work with apply. What do you think?

There’s almost no reason to use apply for built in functions. Apply was built specifically to allow users to create macros and use them as a filter. It’s just that in order to add filters, it needs to be a function. So we have a function and a filter for it. But it is 100% useless as a function because you can call the macro directly instead of using apply. The same goes for built in functions.

All the functions you posted don’t even benefit from apply either.

e.g.

{{ 'sun.sun' | state_attr('elevation') }}
{{ state_attr('sun.sun', 'elevation') }}
{{ ['sun.sun'] | map('state_attr', 'elevation') | list }}

This is an example of a benefit of apply:

{% set entities = [] %}{# Make your list of entities #}
{% set in_label = ["RGB Group", "RGB Light"] %}
{% set not_in_label = ["RGB Control Group"] %}

{%- macro choose(entity_id, included, excluded, returns) %}
  {%- if entity_id in included and entity_id not in excluded %}
    {%- do returns(entity_id) %}
  {%- endif %}
{%- endmacro %}

{% set in_label = in_label | map('label_entities') | sum(start=[]) | unique | list %}
{% set not_in_label = not_in_label | map('label_entities') | sum(start=[]) | unique | list %}
{{ entities | map('apply', choose | as_function, in_label, not_in_label ) | reject('none') | list }}

This creates a macro called choose that allows you to write any if statements in the macro to filter a list down based on labels. It’s a “Jinja” way to avoid using namespace.

2 Likes

Which once you have the lists you can just use the set compare functions like intersect() faster and more efficiently anyway, tbh… :sunglasses:

Thanks, I was thinking like: “if this thing can work more universally and would be an easy fix, then why not?” I thought I would need this too, but you showed me better option: ['sun.sun'] | map('state_attr', 'elevation')

I wanted to write a macro that would work like map but allow several attributes, with each attribute having an optional function (+ additional arguments) to be performed on its values. I didn’t know how to call a function by name on a single element. So I passed function “pointers” through arguments and then used apply. Your “trick” works even with “pure” filters or tests like default or none, so is way better.

1 Like