Script to find all ungrouped items

And in case you somehow miss it, here’s a script to find all dead items as requested

so this line takes care it doesnt find itself, or at least report that?

is funny, because in my system the python_script has another name, but it doesn’t report itself either…

just as a tester, also related to Script to find items that no longer exist ive added this line to the python to see what happens:

caught = '{}\n'.format(entity_ids)
hass.states.set('sensor.catchall', '', {
    'custom_ui_state_card': 'state-card-value_only',
    'text': caught
    })

and created a group with this sensor.

the groups shows up, (albeit being empty…) and the sensor is created with the correct entities. now how to have it show up…

5511

seems the list [ ] is bugging this somehow? If I let is scan some more, this is showing up:

54
cool. back to the formatting table :wink: trying to get each entity on a new line, and a color per domain, see other thread for the code to do so if your interested.

since we’re in the dev-tools in the thread, please allow this side questions:

before I had this simple but cool service_templates:

#    action:
#      - service: group.set_visibility
#        entity_id:
#          - group.developer_links
#        data_template:
#          visible: "{{ is_state('input_select.mode', 'Developer') }}"

and

#        {% if is_state('input_select.mode', 'Developer') %}
#          script.dev_on
#        {% else %}
#          script.dev_off
#        {%endif%}

combining the ideas and techniques of these, ive conjured up this:

  - alias: 'set Dev mode'
    id: 'set Dev mode'
    initial_state: 'on'
    trigger:
      platform: state
      entity_id: input_select.mode
    condition: []
    action:
      service_template: >
        script.dev_{{ is_state('input_select.mode', 'Developer')|replace ('True','on')|replace('False','off') }}

@petro would the above be the best way to implement the replace ? It does work as intended. Cool thing is it allows me to have the automation remain the same, while adding or deleting services to the scripts dev_on /off.

Tried it in one statement like replace ('True'/'False', 'On'/'Off') or (['True','False'],['on','off'] ) but no luck yet. Cant find the right docs on this…

With what you’re doing, you could probably get away with a simple if statement instead of trying to replace string values.

script.dev_{{ 'on' if is_state('input_select.mode', 'Developer') else 'off' }}
1 Like

beautiful! brilliant this.
cut and pasted already…

but just for educational purposes, would the replace template be possible?

I tested out the replace template in the template editor and it worked, not sure why it wasn’t working for you. Only reason I would steer clear from using it is the fact that the simple if statement is easier to read at a glance and it has less operations.

which of the 2? replace (‘True’/‘False’, ‘On’/‘Off’) or ([‘True’,‘False’],[‘on’,‘off’] )

This worked for me

yes, here too.

maybe I wasn’t precise enough but i meant to ask if

| replace('True','on') | replace('False','off')

could be formulated in a better, possibly shorter way,

tried the other 2 but had no such luck…

No, you gotta do it that way or with regex but regex is limited in HA, also regex can be slow. I think your best option is the if statement check to be honest. It’s straight to the point, less code, and easier to read.

quite. ok thanks!

Here’s an updated version of the script. The major update this time is to create the view/custom_ui sensor the same way I do in the find_ghosts script.

If you leave the use_custom_ui set at false, you will get a list of hyperlinks (I’m trying to cancel any navigation attempt, but it’s not working so far). If you set it to True, and have the state-card-value_only card setup, you will get a text list, which can be formatted if you choose.

def scan_for_new_entities(hass, logger, data):
  ignore = data.get("domains_to_ignore","zone,automation,script,zwave")
  domains_to_ignore=ignore.split(",")
  target_group=data.get("target_group","catchall")
  show_as_view = data.get("show_as_view", False)
  show_if_empty = data.get("show_if_empty", False)
  min_items_to_show = data.get("min_items_to_show", 1)
  use_custom_ui = data.get("use_custom_ui", False)
  
  logger.info("ignoring {} domain(s)".format(len(domains_to_ignore)))
  logger.info("Targetting group {}".format(target_group))

  entity_ids=[]
  groups=[]
  
  for s in hass.states.all():
    state=hass.states.get(s.entity_id)
    domain = state.entity_id.split(".")[0]
    
    if (domain not in domains_to_ignore):
      if (domain != "group"):
        if (("hidden" not in state.attributes) or
             (state.attributes["hidden"] == False)):  
          entity_ids.append(state.entity_id)
      else:
        if (("view" not in state.attributes) or 
          (state.attributes["view"] == False)):
          entity_ids.append(state.entity_id)
        
    if (domain == "group") and (state.entity_id != target_group):
      groups.append(state.entity_id)

  logger.info("==== Entity count ====")
  logger.info("{0} entities".format(len(entity_ids)))
  logger.info("{0} groups".format(len(groups)))

  high_order=0
  for groupname in groups:
    group = hass.states.get(groupname)
    if int(group.attributes["order"]) > high_order:
      high_order=int(group.attributes["order"])
    for a in group.attributes["entity_id"]:
      if a in entity_ids:
        entity_ids.remove(a)

  visible=False
 
  if (len(entity_ids)) > min_items_to_show or show_if_empty:
    visible=True
  else:
    return
  
  result_ids = []
  
  if (use_custom_ui):
    summary = ""
    for e in entity_ids:
      summary = "{}{}\n".format(summary, e)
    
    hass.states.set('sensor.orphan_items', len(entity_ids), {
        'custom_ui_state_card': 'state-card-value_only',
        'text': summary, 'unit_of_measurement': 'orphans'
    })
    
    result_ids.append("sensor.orphan_items")

  else:
    counter=0
    for e in entity_ids:
      name = "weblink.orphan{}".format(counter)
      hass.states.set(name, "javascript:return false", {"friendly_name":e})
      result_ids.append(name)
      counter = counter +1

  service_data = {'object_id': target_group, 'name': 'Orphan Items',
                    'view': show_as_view, 'icon': 'mdi:magnify',
                    'control': 'hidden', 'entities': result_ids,
                    'visible': True}

  hass.services.call('group', 'set', service_data, False)


scan_for_new_entities(hass, logger, data)
1 Like

had a little go at it and formatted the output some further:

48

would really like the orphans sensor to list the entities separately but then I can t find the correct syntax in python, since i need the other {} to in the same line.

orphans:

 caught = '!- {}\n\n*==== Entity count ====\n!- {} entities to list \n+- {} groups processed\n*==== Settings ====\n/- targetting group: {}\n$- ignoring domains: {}'.format(entity_ids,len(entity_ids),len(groups),target_group, domains_to_ignore) #    caught="\n".join(entity_ids)
hass.states.set('sensor.orphans_sensor', len(entity_ids), {
    'custom_ui_state_card': 'state-card-value_only',
    'text': caught,
    'unit_of_measurement': 'Orphans'
    })

and Ghosts:

  busted=""
  for e in results:
    busted = "!- {}{}\n\n\n*==== Entity count ====\n!- {} entities to list\n#- {} real entities\n+- {} grouped entities\n+- {} groups processed".format(busted,e,len(results),len(real_entities),len(grouped_entities),len(processed_groups) ) #"{}\n".format(e) #

  hass.states.set('sensor.ghosts_sensor', len(results), {
      'custom_ui_state_card': 'state-card-value_only',
      'text': busted,
      'unit_of_measurement': 'ghosts'
  })

could you check and see if we can format as above, but with caught="\n".join(entity_ids)

this might need some extra error checking…oops:

I’ll take a look at this tomorrow if that’s okay. I’m pretty sure we can do what you want

cool, and of course thats ok!

might I add 2 small questions:

ive tried to have the orphans script to read its settings from data: under the script calling in the yaml like this:

  scan_orphans:
    alias: Scan ungrouped Orphans
    sequence:
      - service: python_script.scan_orphans
        data:
          domains_to_ignore:
            - scene
            - zwave
            - weblink

and change the lines:

def scan_for_orphans(hass, logger, data):
    ignore = data.get("domains_to_ignore", "scene, zwave, weblink")
    domains_to_ignore = ignore.replace(" ", "").split(",") 

to

def scan_for_orphans(hass, logger, data):
    ignore = data.get("domains_to_ignore",[])
    domains_to_ignore = ignore.replace(" ", "").split(",")

but the script errors out on an invalid action in the domains_to_ignore line with the replace.
also the script def isnt correct then (scan_for_orphans(hass, logger, data,) ) as it obviously needs the process_groups_entities like the ghosts script? Added that too, but no luck yet. so if you could please peek t that too?

second: as you know, and is reported out, the ghosts script targets target_group=data.get("target_group","ghosts")

while the orphans script targets:
target_group = data.get("target_group", "group.orphans")

as I report that out on the card, I wanted them both to show group.ghosts and group.orphans and change the setting in the ghosts script. It runs alright (i think, but doesnt show the group ghosts any longer ) but errors with an invalid slug on the ghosts group. Where do I need to change the script or service_data to accept that?

Log Details (ERROR)
Fri Jun 29 2018 07:33:45 GMT+0200 (CEST)

Invalid service data for group.set: invalid slug group.ghosts (try groupghosts) for dictionary value @ data['object_id']. Got 'group.ghosts'

30

this gives no slug error:

57

this is almost resolved, creating an extra and final text to show ghost_card using the original text busted:

  busted=""
  for e in results:
    busted = "{}{}\n".format(busted,e,) #"{}\n".format(e) 
  ghost_card = "!- {}\n*==== Entity count ====\n!- {} entities to list\n#- {} real entities\n+- {} grouped entities\n+- {} groups processed\n*==== Settings ====\n/- targetting group: {}\n$- ignoring {} domains: {}\n$- ignoring {} items: {}".format(busted,len(results),len(real_entities),len(grouped_entities),len(processed_groups),target_group,len(ignore_domains), ignore_domains, len(ignore_items), ignore_items ) #"{}\n".format(e) #
  hass.states.set('sensor.ghosts_sensor', len(results), {
      'custom_ui_state_card': 'state-card-value_only',
      'text': ghost_card,
      'unit_of_measurement': 'ghosts'
  })

only need to get the formatting right of the shown entitities, now only displaying the first entity in red…
getting there.

changed

  service_data = {'object_id': target_group,'name': 'Ghosts','view': show_as_view,
                   'icon': 'mdi:ghost','control': 'hidden','entities': entity_ids,
                   'visible': True}

to

  service_data = {'object_id': 'ghosts','name': 'Ghosts','view': show_as_view,
                   'icon': 'mdi:ghost','control': 'hidden','entities': entity_ids,
                   'visible': True}

and now the group is displayed again, and on the card the full group name is shown also. bingo

a bit a pity it can’t do that automatically reading the service_data of the

target_group=data.get("target_group","group.ghosts")
line though, I always try to keep the syntax as automatic as possible and prevent hard-coding names. will see if anything else is possible.

about the ungrouped formatting without the list notation:

here too (as in the other Ghosts sensor) would an intermediary variable enable a solution, deleted my earlier post, this is what does the trick:

left_overs=""
for a in entity_ids:
    left_overs = "{}!- {}\n".format(left_overs,a)
     #  left_overs = "{}{}\n".format(entity_ids,a) #"{}\n".format(e)  # "{}{}\n".format(busted,e,)  
     # left_overs = "\n".join(entity_ids) #"{}\n".format(e)  # "{}{}\n".format(busted,e,)


service_data = {'object_id': 'orphans', 'name': 'Ungrouped Orphans',
                'view': show_as_view, 'icon': 'mdi:magnify',
                'control': 'hidden', 'entities': entity_ids,
                'visible': visible}

hass.services.call('group', 'set', service_data, False)

left_over_card = '*=========== Orphans ===========\n{}\n*========== Entity count =========\n!- {} entities to list\n+- {} groups processed\n*=========== Settings ===========\n/- targetting group: {}\n$- ignoring {} domains: {}'.format(left_overs,len(entity_ids),len(groups),target_group,len(domains_to_ignore), domains_to_ignore)  # caught="\n".join(entity_ids)

hass.states.set('sensor.orphans_sensor', len(entity_ids), {
    'custom_ui_state_card': 'state-card-value_only',
    'text': left_over_card,
    'unit_of_measurement': 'Orphans'
    })

24

Hi Marius,
If you are submitting the domains_to_ignore as an array under data, these 2 lines of code should simply be

    domains_to_ignore = data.get("domains_to_ignore",[])

You don’t need to split the text into an array.

As for the work you’re doing on the display, you’ve made many changes from what my code was. If you can send me three things, I can take a look at it for you

  1. Your current code
  2. The text you are currently getting
  3. The text you want to get instead

But basically, what I would suggest doing is use intermediate variables to construct each section of the block, then use one last variable to combine everything.

So you would be able to build the list of entities, the list of configuration options, etc, then assemble them into the order you want.

See this page on StackOverflow for options on breaking up a long line

thanks @NigelL for having a look, will send you the scripts in a message.

ok will try that, but was hesitant because of the ignore.replace thats in the python currently. Wouldn’t that be needed when read from the data array?

this would be correct:

def scan_for_orphans(hass, logger, data):
    domains_to_ignore = data.get("domains_to_ignore",[])
    items_to_ignore = data.get("items_to_ignore",[])  

no need to change anything in the service_data? Suppose I need to add an extra bit of code for the items to ignore ? the other script uses a

def process_group_entities(group, grouped_entities, hass, logger, process_group_entities,\
                             processed_groups, ignore_domains, ignore_items)

so I believed that to be necessary here too…

thanks to @sebk-666 I can now wrap the python just as i wanted it. Always simple if you know how, but thats why this community is so great, not a day goes by without learning something:

left_over_card = '*=========== Orphans ===========\n' \
                 '{} ' \
                 '*========== Entity count =========\n' \
                 '!- {} entities to list\n' \
                 '+- {} groups processed\n'\
                 '*=========== Settings ===========\n'\
                 '/- targetting group: {}\n'\
                 '$- ignoring {} domains: {}'\
                 .format(left_overs,
                         len(entity_ids),
                         len(groups),
                         target_group,
                         len(domains_to_ignore), 
                         domains_to_ignore)



hass.states.set('sensor.orphans_sensor', len(entity_ids), {
        'custom_ui_state_card': 'state-card-value_only',
        'text': left_over_card,
    #        'unit_of_measurement': 'Orphans'
        })

13

as for the Ghosts script, here is the result of the side-sessions with @NigelL. some further tweaking, formatting to go with the Ghosts script, a fine tool in the dev’s toolbox. Kudos to @NigelL!

##########################################################################################
# Script to find all ungrouped but existing entities
#
# https://community.home-assistant.io/t/script-to-find-all-ungrouped-items/55774/117
# original author: https://community.home-assistant.io/u/NigelL
# customizing, further tweaking and pushing it a bit by https://community.home-assistant.io/u/Mariusthvdb/
# ##########################################################################################
# set ignore_domains in the yaml file calling the script
#  scan_orphans:
#    alias: Scan ungrouped Orphans
#    sequence:
#      - service: python_script.scan_orphans
#        data:
#          ignore_items:
#            - sensor.ghosts_sensor
#            - sensor.orphans_sensor
#            - sensor.orphans_badge
#          ignore_domains:
#            - scene
#            - zwave
#            - weblink
##########################################################################################
# Codes for text_colors declared in 
# Custom card: /custom_ui/state-card-value_only.html
##########################################################################################
#      case "*": return "bold";
#      case "/": return "italic";
#      case "!": return "red";
#      case "+": return "green";
#      case "=": return "yellow";
#      case "%": return "grey";
#      case "$": return "brown";
#      case "#": return "blue";
#      default:  return "normal";
##########################################################################################
##########################################################################################

def scan_orphans(hass, logger, data):
    target_group = data.get("target_group", "group.orphans")
    show_as_view = data.get("show_as_view", False)
    use_custom_ui = data.get("use_custom_ui", True)
    ignore_items = data.get("ignore_items",[])
    ignore_domains = data.get("ignore_domains", [])
    show_if_empty = data.get("show_if_empty", False)
    min_items_to_show = data.get("min_items_to_show", 1)

    logger.info("ignoring {} item(s)".format(len(ignore_items)))
    logger.info("ignoring {} domain(s)".format(len(ignore_domains)))
    logger.info("Targetting group {}".format(target_group))

    entity_ids = []
    groups = []

    for s in hass.states.all():
        state = hass.states.get(s.entity_id)
        domain = state.entity_id.split(".")[0]

        if state.entity_id in ignore_items:
            continue # Ignore this entity, and go to the next one

        if (domain not in ignore_domains):
            if (domain != "group"):
                if (("hidden" not in state.attributes) or
                        (state.attributes["hidden"] == False)):
                    entity_ids.append(state.entity_id)
            else:
                if (("view" not in state.attributes) or
                        (state.attributes["view"] == False)):
                    entity_ids.append(state.entity_id)

        if (domain == "group") and (state.entity_id != target_group):
            groups.append(state.entity_id)

    logger.info("==== Entity count ====")
    logger.info("{0} entities".format(len(entity_ids)))
    logger.info("{0} groups".format(len(groups)))

    for groupname in groups:
        group = hass.states.get(groupname)
        for a in group.attributes["entity_id"]:
            if a in entity_ids:
                entity_ids.remove(a)

    if (len(entity_ids)) > min_items_to_show or show_if_empty:
        visible = True
    else:
        visible = False

#  if (use_custom_ui): #, show card
    left_overs=""
    for a in entity_ids:
        left_overs = "{}!- {}\n".format(left_overs,a)
        left_overs_badge = "\n".join(entity_ids)
    ignore_items_unlist = ', '.join(ignore_items)
    ignore_domains_unlist = ', '.join(ignore_domains)

    left_over_card = '*=========== Orphans ===========\n'\
                     '{}'\
                     '*========== Entity count =========\n'\
                     '!- {} entities to list\n'\
                     '+- {} groups processed\n'\
                     '*=========== Settings ===========\n'\
                     '/- targetting group: {}\n'\
                     '$- ignoring {} items:\n' \
                     '%|-> {}\n'\
                     '$- ignoring {} domains:\n' \
                     '%|-> {}'\
                     .format(left_overs,
                             len(entity_ids),
                             len(groups),
                             target_group,
                             len(ignore_items),
                             ignore_items_unlist,
                             len(ignore_domains),
                             ignore_domains_unlist)

    hass.states.set('sensor.orphans_sensor', len(entity_ids), {
        'custom_ui_state_card': 'state-card-value_only',
        'text': left_over_card,
      # 'unit_of_measurement': 'Orphans'
        })

#  else: ##show group with found orphans:
    service_data = {'object_id': 'orphans', 'name': 'Orphans',
                    'view': show_as_view, 'icon': 'mdi:magnify',
                    'control': 'hidden', 'entities': entity_ids,
                    'visible': True}

    hass.services.call('group', 'set', service_data, False)

# show badge
    hass.states.set('sensor.orphans_badge', len(entity_ids), {
        'text': left_overs_badge,
        'unit_of_measurement': 'Orphans',
        'friendly_name': len(entity_ids),
        'entity_picture': '/local/badges/orphans.png'
         })

scan_orphans(hass, logger, data)

frontend:

13
25
33

1 Like