Label_entities(): allow multiple labels selection (intersection)

Proposition: allow getting entities having multiple labels on them (intersection).

Example:

{{ label_entities(['bedroom', 'evening_lights']) }}

It should return entities that have BOTH the bedroom and evening_lights labels at the same time.

  • some_light1 with a label bedroom – NO, would not match
  • some_light2 with labels bedroom and daytime – NO, would not match
  • some_light3 with labels bedroom, evening_lights - YES, would match

In other worlds, it would be a logical AND of the passed in label names.

This is a counter-proposition to Allow for multiple labels/areas in label_entities/area_entities – which would be a logical OR on the passed in label names.


Original reply, as this FR is extracted from that one:

I’m always hesitant with this kind of API. Two aspects…

Api Extension
What if we wanted to add some kind of extra parameter to the function?

  • Let’s say we’d want to optionally include disabled entities. It could be label_entities('kitchen', 'bedroom', true) (the true would be include_disabled=true parameter). It’s kinda possible, but…
  • …it’s ok to add a boolean parameter, but impossible to add a string parameter: like label_entities('kitchen', 'bedroom', 'on') → that’s with an example as if we wanted to add a parameter that would allow us to quickly filter by value (the value_filter='on' parameter).

Solution: do not use variadic parameters, pass an array for a list instead: label_entities(['bedroom', 'kitchen'], 'on').

Intersection vs merge
With the proposed API, we have to decide what is more important: to get entities from both labels (“merge”, “sum of both”) vs find entities that have both both labels at the same time (“intersection”).

  • You’re proposing a merge, which is good for use cases like finding entities from both bedroom and kitchen.
  • Another use case would be finding an intersection, like finding the entities that have both labels bedroom and evening_lights at the same time.

Solution: I don’t know which use case is more popular/important in this case, but for me personally, I’d go with intersection (as implementing it manually per case would be much harder).

As for merging, you can already use an operator, like like label_entities('bedroom') + label_entities('kitchen').


To sum up… If I can humbly make a slight counter-proposition:

  • don’t use variadic parameters, allow to pass an array: label_entities(['bedroom', 'evening_lights']) – that would give the entities which have both labels at the same time (intersection).
  • don’t add a function for merging results → there is already a + operator for that.

EDIT: instead of overloading the label_entities() function, they could be all separated into 3 dedicated functions, like label_entities() + label_entities_any() + label_entities_all(). See below.

1 Like

I could not agree more! I just added some labels and many have multiple. I was very surprised when trying to use the filter, that any entity that had either chosen label was listed. the expected behavior was that it would filter and display only entities that had BOTH selected labels.

1 Like

I am using these as work-arounds for the time being

Select all entities labelled ‘bedroom’ OR ‘evening_lights’

label_entities_any(['bedroom', 'evening_lights'])
{%- macro label_entities_any(label_list) -%}
  {{- label_list | map('label_entities') | sum(start=[]) | sort |join(",") -}}
{%- endmacro -%}

Select all entities labelled ‘bedroom’ AND ‘evening_lights’

label_entities_all(['bedroom', 'evening_lights'])
{%- macro label_entities_all(label_list) -%}
  {%- set ns = namespace(entities=[]) -%}
  {%- set ns.entities = label_entities(label_list.pop(0)) -%}
  {%- for l in label_list -%}
    {%- set ns.entities = ns.entities|select('in',label_entities(l)|list )  -%}
  {%- endfor -%}
  {{- ns.entities|sort|join(",") -}}
{%- endmacro -%}
3 Likes

label_entities() + label_entities_any() + label_entities_all() → I like your naming + separation of functions instead of overloading → probably the best possible option for implementing both FRs :+1:

1 Like

Would be great if this can also be a feature in the call service section.

For example, I would wanna do a call service to dim all my lamps but only in the living room (I have many lamps across my home). All lamps have label “Lamp” and all my living room entities has the label “Living Room”.

If I now select both those labels in my call there should be an option for matching all labels or any labels.

Would even be easier if you select Living Room as an area and Lamp as a label to have the option to match areas and labels.

I tried using this in an automation/service call (see the action part below) and it does select the correct devices (I changed the code to devices), but I do get an error because for some reason it is parsed as a list of lists or something. Do you have an idea as to why that is?

Example:

action: homeassistant.toggle
target:
  device_id: |-
    {%- macro label_devices_all(labels) -%}
      {%- set ns = namespace(devices=[]) -%}
      {%- set ns.devices = label_devices(labels.pop(0)) -%}
      {%- for label in labels -%}
        {%- set ns.devices = ns.devices | select('in', label_devices(label) | list)  -%}
      {%- endfor -%}
      {{- ns.devices | list -}}
    {%- endmacro -%} {{ label_devices_all(['a', 'b', 'c']) }}

Error: unhashable type: ‘Wrapper’

params:
  domain: homeassistant
  service: toggle
  service_data: {}
  target:
    device_id:
      - - imadeviceid
        - imanotherdeviceid
running_script: false

You can do it like this:

{{ ['bedroom', 'evening_lights'] | map('label_entities') | sum(start=[]) }}

You’re missing the first square bracket. :slight_smile:

1 Like

No, it’s there (now)
Thanks

1 Like

You have two dashes before imadeviceid.

Race condition! :slight_smile:

1 Like

You can use the same approach as my post above

action: homeassistant.toggle
target:
  device_id: "{{ ['a', 'b', 'c'] | map('label_devices') | sum(start=[]) }}"

oh, wait… I totally misread, you want an AND, not an OR…

This should work for the AND use case

{% set labels = ['a', 'b', 'c'] %}
{% set ns = namespace(in_all_labels=label_devices(labels[0])) %}
{% for l in labels[1:] %}
  {% set ns.in_all_labels = ns.in_all_labels | select('in', label_devices(l)) | list %}
{% endfor %}
{{ ns.in_all_labels }}

Which is basically the same as what you were using above, but I’m not sure why a macro would be useful. (unless of course you want to use it in multiple places, then you can add it to custom_templates)

2 Likes

i know, they are the generated output by the above code (maybe i made it not clear enough, the second block of code is what the trace tells me it tried running, while the first block is what i typed)

sadly this gives the same error, ill paste it as a picture, maybe i don’t know what exactly i should copy:

It’s really weird that it creates a list in a list. That’s not the output of the template.

Hello,

Please forgive my ignorance, but I’m struggling to implement this. I’m looking to turn all my lights on labeled both holiday and indoor. I’m not getting an error, but it’s also not turning anything on.

Also, is there a way to turn on all lights EXCEPT a certain intersection of labels? I’m assuming so, but I dont know enough about your script to know what to change.

Apologies again for hijacking, but this looks promising.

So, i’m an idiot. Surprise!

Your solution worked perfectly! I just didn’t make the connection that your script was calling label_devices() and not label_entities() like the title. I’m sorry for the mistake and thank you so very much!

I’m still curious though, what changes could I make if I wanted all the lights to go off EXCEPT those labeled both ‘holiday’ and ‘outdoor’?