Need help selecting entities in grouped groups, in Python script

Add this function to your python script after desc = ''

def flatten(entity_id, result=[], searched=[]):
    state = hass.states.get(entity_id)

    if state is not None:
        entity_ids = state.attributes.get("entity_id")
        if entity_ids is not None:
            searched.append(entity_id)
            for entity_id in entity_ids:
                if entity_id not in result and entity_id not in searched:
                    flatten(entity_id, result, searched)
        else:
            result.append(entity_id)

then change the beginning of your for loop from

for entity_id in hass.states.get(group).attributes['entity_id']:

to

entity_ids = []
flatten(group, entity_ids)
for entity_id in entity_ids:

the flatten function mimics expand()

3 Likes

thanks!
had been experimenting with the flatten function, but thought it would be available already, and not required the definition in the script itself.

can report success :wink:

this is so much better.

Report any issues with it. It’s a recursive function and I tried to trap against circular group configurations. You could get max recursion exceptions, hopefully you won’t.

ok I will be on the lookout.

though, the groups I use don’t have any circular reference. As posted above the top group holds a few sub groups which again hold a few groups, until the last group that only holds single entities. None of the entities (lights) are in any other group.

The whole exercise was about that, so I have been careful in the group configurations.

Yeah I wouldn’t worry about it, I tested it with this setup and it works:

all_lights:
  entities:
    - group.foo
    - group.bar
    - light.bed_light
foo:
  entities:
    - light.ceiling_lights
    - group.bar
bar:
  entities:
    - light.kitchen_lights
    - group.foo

found an issue! Or, more of a challenge really

but not in the python, in a js template referencing the now toplevel group.all_inside_lights:

  - type: custom:button-card
    entity: group.all_inside_lights
    template: button_title_counter
    triggers_update: input_boolean.run_lights_summary
    label: >
      [[[
      var i;
      var entities = entity.attributes.entity_id;
      var count = 0;
      for (i = 0; i < entities.length; i++) {
        var state = states[entities[i]].state;
        if (state == 'on') {count += 1;}
       }
      if (count == entities.length) return 'All ' + entities.length + ' lights on';
      if (count == 0) return 'No lights on';
      return 'Lights on: ' + count + ' of ' + entities.length;
      ]]]

counted the lights on before, but now, as is to be expected shows only the count of the groups (which now are the entities inside the group.all_inside_lights

so, the question is, can we also ‘flatten’ groups in JS…? some googling suggests we can, but I havent found a real JS replacement for what you made in the python_script.

If you’d have a moment and see a challenge, Id appreciate it. Guess it could be very useful for others too, having a counter iterating groups for entities
Otherwise I fear I’d see myself forced to re-introduce the verbose group…

try converting the python flatten function to JS yourself.

given the fact that this: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat would be available, shouldn’t I use that, instead of trying to literally convert the python to js?

would have been nice if something along then lines of

      [[[
      var i;
      var flattened = entity.flat(4);
      var entities = flattened.attributes.entity_id;
      var count = 0;
      for (i = 0; i < entities.length; i++) {
        var state = states[entities[i]].state;
        if (state == 'on') {count += 1;}
       }
      if (count == entities.length) return 'All ' + entities.length + ' lights on';
      if (count == 0) return 'No lights on';
      return 'Lights on: ' + count + ' of ' + entities.length;
      ]]]

would have been possible.

new territories, finding/learning available JS functions…

You don’t have nested arrays, you have nested objects with arrays. You’ll have to make this by hand.

1 Like

ok will study after dinner :wink:

thanks

Yeah your googling the wrong information. You want to google recursive functions. But if you simply went through the python script and tried to replace each piece with the correct call/syntax for JS, you’d get what you want.

finding myself in new territory trying to define a recursive function in JS, and the translation from Python…

first structural attempt would be something like this, where entity_id is the main group, entities the group members, and e an individual light that will be appended (or not):

function flatten(entity, lights_result, lights_searched){
  return entity; # ??< dont yet see what I should do here..
  }

var entities = entity.attributes.entity_id;
var lights_result = []
var lights_searched = []

if (entity && entities ) return
              searched.append(entity);  # set the first main group to be the entity
  for (e in entities) {(return e not in lights_result && e not in lights_searched) # check if an entity is in either list
                       ? flatten(e, lights_result, lights_searched) : null ;} # if no, append, if yes, do nothing

return result.append(e);

could you please comment on the most basic of errors…

Just go line by line. You almost have the first line correct.

python

def flatten(entity_id, result=[], searched=[]):

js

function flatten(entity_id, result=[], searched=[])
{

}

Now what’s the second line

python

state = hass.states.get(entity_id)

js

… You should know this. How do you get a state using the entity_id in JS?

the line would go here:

function flatten(entity_id, result=[], searched=[])
{
       # <- line goes here.
}

are we looking for

entity.attributes.entity_id

making that line

var state = entity.attributes.entity_id

?

Close, we are trying to just get the state from the state machine using the entity_id

meaning entity.entity_id, like this in Jinja

function flatten(entity_id, result=[], searched=[])
{
       var state = entity.entity_id
}

and in the next step we need to get the attributes list:

set entities =  entity.attributes.entity_id

recursively:

to finally reach the lights:

(replaced only one group per level for illustrational purposes :wink: )

making the full function like this then:

      [[[
         function flatten(entity, lights_result= [], lights_searched= []){
           var state = entity.entity_id;
           return state;
           
           if (state != None) {
             var entities = entity.attributes.entity_id;
               if (entities != None) {
                 searched.append(entities);
                   for (e in entities) {
                     (! lights_result.includes(e) && ! lights_searched.includes(e))
                       ? flatten(e, lights_result, lights_searched) : null
                                        }
                                      }
                               }
              return result.append(entities);
}

and then use this in the actual template…

[[[
  function flatten(entity, lights_result= [], lights_searched= []){
  var state = entity.entity_id;
           return state;
           
           if (state != None) {
             var entities = entity.attributes.entity_id;
               if (entities != None) {
                 searched.append(entities);
                   for (e in entities) {
                     (! lights_result.includes(e) && ! lights_searched.includes(e))
                       ? flatten(e, lights_result, lights_searched) : null
                                        }
                                      }
                               }
              return result.append(entities);
}
#   var entities = entity.attributes.entity_id; #<--- this needs to become the flattened list of lights so:
   var light_ids = []; # <-- new variable to hold the light_ids
   var entitities = flatten(entity, light_ids);  # <-- flatten group 'entity' in list 'light_ids'

   var i;
   var count = 0;
   for (i = 0; i < entities.length; i++) {
     var state = states[entities[i]].state;
     if (state == 'on') {count += 1;}
     }
   if (count == entities.length) return 'All ' + entities.length + ' lights on';
   if (count == 0) return 'No lights on';
   return 'Lights on: ' + count + ' of ' + entities.length;
]]]

That’s how I deal with it in button-card (the entity being a group). This returns the number of entities which are on in the group and works for nested groups.

        if (!entity) return 0;
        function loop(list, value) {
          list.forEach(entity => { // This entity is different from the button-card entity, I should have named it differently...
            if (states[entity])
              if (entity.split('.')[0] === "group")
                value = loop(states[entity].attributes.entity_id, value);
              else if (["open", "on"].includes(states[entity].state))
                value++;
          });
          return value;
        }
        return loop(entity.attributes.entity_id, 0);
1 Like

ok, ha thanks.
thats a totally different approach isn’t it? how do you get the length of all entities in the group in this format? is that loop.length ?

    label: >
      [[[
        if (!entity) return 0;
        function loop(list, value) {
          list.forEach(entity => { // This entity is different from the button-card entity, I should have named it differently...
            if (states[entity])
              if (entity.split('.')[0] === "group")
                value = loop(states[entity].attributes.entity_id, value);
              else if (["open", "on"].includes(states[entity].state))
                value++;
          });
          return value;
        }
        return loop(entity.attributes.entity_id, 0);
        ]]]

For the length:

        if (!entity) return 0;
        const loop = (list, max) => {
          list.forEach(entity => {
            if (states[entity])
              if (entity.split('.')[0] === "group")
                max = loop(states[entity].attributes.entity_id, max);
              else {
                max++
              }
          });
          return max;
        }
        return loop(entity.attributes.entity_id, 0);
1 Like

can this be combined in 1 go? because, in fact I need them both, the ‘on’ number, and the total number:

   if (count == entities.length) return 'All ' + entities.length + ' lights on';
   if (count == 0) return 'No lights on';
   return 'Lights on: ' + count + ' of ' + entities.length;

btw, I notice this updates immediately. How come the top template doesn’t work like that? (using groups in groups)
I ask, because in my FR just posted on the GitHub, I mention this, and your template seems to already do that…?