Recommended way to avoid duplicated logic: one template sensor referring to another

I’m very new to HA and trying to figure out best practices.
What I want to accomplish: (1) get count of lights on in the entire home and (2) get count of lights on in specific zones.
The implementation: obviously there is common code here of getting all the lights and then applying a filter for zone in the case of (2).

In order to reuse the code, is it ok to shove the result of the list of all lights in a sensor that I refer to from other sensors or it’s bad to keep such weird data in a sensor. I also cannot get it to work. Specifically

- platform: template
  sensors:
    all_lights:
      unique_id: "all_lights"
      friendly_name: "All Lights"
      icon_template: "mdi:lightbulb"
      value_template: >
        {{ states.light
            |rejectattr('attributes.is_hue_group', 'true')
            |rejectattr('attributes.entity_id', 'defined')
        }}

And then

- platform: template
  sensors:
    all_lights_on:
      unique_id: "all_lights_on"
      friendly_name: "All Lights On"
      icon_template: "mdi:lightbulb"
      value_template: >
        {{           
          states('sensor.all_lights')
            |selectattr('state','eq','on')
            |list
            |count  
        }}

I’m probably doing something very dumb. Also, not sure if I should be using scripts instead of templates for these types of things.

You can have a Template Sensor reference another Template Sensor but the reason your examples fail to work is because they have invalid templates. However, the larger problem is that what the first Template Sensor is attempting to do is impractical. An entity’s state value is limited to storing a string no greater than 255 characters in length.

In contrast, an entity’s attributes can store data of type string, integer, float, list, dict, etc up to 16 Kb in length.

Having said that, the other impractical aspect of the first Template Sensor is that it’s attempting not simply to store a list of the entity_id of all lights that are on, it appears to be attempting to store every available property of each light. That’s not recommended; the State Machine already has that data and can be readily extracted at any moment using a template.

1 Like

thanks - this is extremely helpful! so i should map into entity_ids and export those instead?

i mean technically want I want to avoid duplicating is the code and I’m trying to accomplish it via data which isn’t ideal
Is there a way to extract out implementation bits into functions and I can then reuse those in different template implementations? That would be ideal rather than have those “intermediate” and meaningless sensors that I’m creasing just for logic reuse.

Seems like this is what I want and it’s not quite supported (though maybe there are different ways to do it). I’m going to vote for this issue.

Yes.

Reusing templates

1 Like

Thank you very much for this! So I’ve been playing with macros for a bit. I had to figure out a lot of syntax since I come from a very different background. I haven’t been able to work out how to do what I want mostly due to ignorance and maybe I am not using them for their intended purpose.

Here is what I came up with. It seems like the returned value is just the string itself rather than the generator object. Any ideas? Or maybe I should try using scripts instead?

{% macro get_all_on_lights_generator() %}
{{  states.light
        |rejectattr('attributes.is_hue_group', 'true')
        |rejectattr('attributes.entity_id', 'defined')
        |selectattr('state','eq','on')
}}
{% endmacro %}

{% macro get_all_on_lights_in_zones_generator(zone_ids) %}
    {% set data = namespace(relevant_entity_ids=[])%}
    {% for zone in zone_ids %}
    {%    set data.relevant_entity_ids = data.relevant_entity_ids + area_entities(zone) %}
    {% endfor %}
    {% set entity_matches = data.relevant_entity_ids %}
    {{
       get_all_on_lights_generator()|selectattr('entity_id', 'in', entity_matchs)
    }}
{% endmacro %}

and then the usage is

{% from 'light_related_template_macros.jinja' import get_all_on_lights_generator %}
{% from 'light_related_template_macros.jinja' import get_all_on_lights_in_zones_generator %}
        {{  
            get_all_on_lights_generator()
        }}

But it looks like this is literally returning the string <generator object select_or_reject at 0x7fb.......> rather than the actual object. If I try adding a |list after it, it literally breaks up the string into characters.

I’m sure I’m doing something dumb, any ideas? Or I just cannot use templates in this way? Thanks.

P.S. the homeassistant.reload_custom_templates has been a godsent haha

You cannot pass a generator object.

Think of a generator as a temporary thing; it’s only defined within the bounds of the template where it’s created.

You can pass list, dict, string, float, etc.

1 Like

got it, should require a small change since luckily i don’t need anything else than entity ids in the functions using the main one! (ie no more attribute checks).
what is a simple way to intersect two lists (or ideally shove at least the bigger one in a set, too)?
EDIT: i found a bunch of jinja syntax online and none of it works in HA templates, that’s my issue with this it takes me ages to do trivial things aarghhh, your help much appreciated!

seems like i cannot return a proper list either
i think i’m just pushing this beyond what it was meant for
you can only return strings it seems, pls feel free to correct me

ok, i have something though it’s kinda unsatisfying since i’m changing the output to be a string which has the elements separated by comma and then doing split later to reconstitute the list… all of this is super hacky but it’s been fun playing with it.

i’m planning to have a lot more reuse of these things, that’s why i’m trying to avoid duplication. if i only had a couple of invocations none of this would be worth it but i want to have a general ability to reuse code for dashboards.

Util to intersect lists (crappy quadratic performance but for the sizes of the lists involved prob doesn’t matter)

{% macro intersect_lists(list1, list2) %}
    {% set data = namespace(result=[])%}
    {% for el in list1 %}
    {%    if el in list2 %}
    {%      set data.result = data.result + [el] %}
    {%    endif %}
    {% endfor %}
    {{ data.result }}
{% endmacro %}

Reusable functions from dashboards

{% from 'utils.jinja' import intersect_lists %}

{% macro get_all_on_lights_entity_ids() %}
{%- for i in states.light
        |rejectattr('attributes.is_hue_group', 'true')
        |rejectattr('attributes.entity_id', 'defined')
        |selectattr('state','eq','on')
        |map(attribute='entity_id')
        |list %}{{i}}{% if not loop.last %},{% endif %}{% endfor -%}
{% endmacro %}

some basic APIs to get lights on in various zones (i'll use the same patter for other stuff)
{% macro get_all_on_lights_in_zones_entity_ids(zone_ids) %}
    {% set data = namespace(in_zone_entity_ids=[])%}
    {% for zone in zone_ids %}
    {%    set data.in_zone_entity_ids = data.in_zone_entity_ids + area_entities(zone) %}
    {% endfor %}
    {{
       intersect_lists(get_all_on_lights_entity_ids().split(','), data.in_zone_entity_ids)
    }}
{% endmacro %}

and then client usage

{{ get_all_on_lights_in_zones_entity_ids(['Living Room','Gallery']) }}
{{ get_all_on_lights_in_zones_entity_ids(['Master Bedroom']) }}
{{ get_all_on_lights_entity_ids().split(",") }}

I’ll prob remove the the one that takes no zones and have a special case when the passed zone list is empty to return all. But it’s been a fun exercise. (I still need to figure out how to get rid of the empty lines)