pyscript icon indicating copy to clipboard operation
pyscript copied to clipboard

How to work with areas (and devices)

Open luar123 opened this issue 2 years ago • 6 comments

I would like to get the area of an entity and all entities belonging to one area, but if I have not missed something there is no possibility to do this through pyscript directly.

This functionality would allow to create very universal automations, e.g. a window sensor could turn of all heaters in the same room when the window is opened and turn them back on when the window is closed and all other windows in the same room are closed as well. For this the area_id of entities needs to be accessible and entities need to be selected based on area_id and device type (domain, class).

I found some information in the dev docs of homeassistant and with the hass object I was able to retrieve this info:

import homeassistant
ent_reg = homeassistant.helpers.entity_registry.async_get(hass)
ent=ent_reg.async_get('<entity>')
area = ent_reg.async_get(ent).area_id

Unfortunately this is typically None, if entities inherit the area from their device. Therefore, first the device is needed:

import homeassistant
ent_reg = homeassistant.helpers.entity_registry.async_get(hass)
dev_reg = homeassistant.helpers.device_registry.async_get(hass)
ent=ent_reg.async_get('<entity>')
dev=ent.device_id
area = dev_reg.async_get(dev).area_id

Getting all devices of an area: homeassistant.helpers.device_registry.async_entries_for_area(dev_reg, area) and from this all entities could be fetched and somehow filtered for domain/class.

That said, I have a couple of questions:

  • Is this the correct way of doing it or is there a better way? (I am new to pyscipt and have never worked with hass internals before)
  • could this be integrated into pyscript?
  • If not I would probably write some helper functions which return the area_id for an entity or return entities based on area/domain/class
  • Other thoughts?

(#138 might be related)

luar123 avatar Oct 08 '21 16:10 luar123

Also in #129 is some discussion about entities and devices.

luar123 avatar Oct 09 '21 11:10 luar123

I don't really use the areas features of Home Assistant. However, the few times I've tried to mess with them, my pyscript code looked very similar to yours, so I think you're doing it correctly.

I think if you can make this work with helper functions, then that's the way to go (and share your module with the community). No reason to bloat the pyscript core if it can be done another way. However, if the functionality is cumbersome or impossible without integration into the pyscript core, it certainly COULD be included. In that case, you should submit a PR.

dlashua avatar Oct 30 '21 12:10 dlashua

Thanks for the feedback. I am now using these three functions to get the area_id of an entity, the area name, and all entities filtered by class or domain for an area_id:

import homeassistant

devreg = homeassistant.helpers.device_registry.async_get(hass)
entreg = homeassistant.helpers.entity_registry.async_get(hass)
areareg = homeassistant.helpers.area_registry.async_get(hass)

def get_area_id(ent):
    ent = entreg.async_get(ent)
    if ent is None:
        return
    if ent.area_id is None:
        if ent.device_id is None:
            return
        dev = devreg.async_get(ent.device_id)
        if dev is None:
            return
        return dev.area_id
    else:
        return ent.area_id
    
def get_area_name(area_id):
    area = areareg.async_get_area(area_id)
    if area is not None:
        return area.name
    

def get_entities_for_area(area_id, domain=None, device_class=None):
    entities=homeassistant.helpers.entity_registry.async_entries_for_area(entreg, area_id)
    entities.extend([e for x in homeassistant.helpers.device_registry.async_entries_for_area(devreg, area_id) for e in homeassistant.helpers.entity_registry.async_entries_for_device(entreg, x.id)])
    if domain is not None:
        entities=[e for e in entities if e.domain==domain]
    if device_class is not None:
        entities=[e for e in entities if e.device_class==device_class]
    if entities==[]:
        return []
    else:
        return [e.entity_id for e in (entities or [])]

(Some lines of code are only needed because of #236) For me this is working fine. Only drawback is that the hass object is needed.

luar123 avatar Oct 30 '21 20:10 luar123

This is short, clean code!

Since these functions are not using any of the pyscript language features, you could decorate them with @pyscript_compile which should make the list comprehensions work the way you expect them to again.

If these were included in the core, they would still have to use the hass object. It would just mean the user wouldn't have to enable the hass object in their config since this is always available in the core. But, as a test, I just put your code into modules/area_helpers.py, included the @pyscript_compile decorators and then wrote a test script:

import area_helpers

area_id = area_helpers.get_area_id('switch.bathguest_exhaust')
log.info(f'AREA ID: {area_id}')

area_name = area_helpers.get_area_name(area_id)
log.info(f'AREA NAME: {area_name}')

area_entities = area_helpers.get_entities_for_area(area_id)
log.info(f'AREA ENTITIES: {area_entities}')

It works perfectly! So this is very usable even without being in the core and would be a great contribution to the community for anyone wanting to work with areas.

I think, to be feature complete and useful in either direction a get_area_id_by_name method would be useful. Then I could have a pyscript automation that, for instance, turned on all the lights in the "Living Room" area, without needing to know one of the entity_ids in that area.

dlashua avatar Oct 31 '21 10:10 dlashua

Thanks, that is exactly how I am using it. @pyscript_compile is a very good idea, makes get_entities_for_area cleaner:

@pyscript_compile
def get_entities_for_area(area_id, domain=None, device_class=None):
    entities=homeassistant.helpers.entity_registry.async_entries_for_area(entreg, area_id)
    entities.extend([e for x in homeassistant.helpers.device_registry.async_entries_for_area(devreg, area_id) for e in homeassistant.helpers.entity_registry.async_entries_for_device(entreg, x.id)])
    if domain is not None:
        entities=[e for e in entities if e.domain==domain]
    if device_class is not None:
        entities=[e for e in entities if e.device_class==device_class]
    return [e.entity_id for e in entities]

get_area_id_by_name is very simple:

@pyscript_compile
def get_area_id_by_name(area_name):
    area = areareg.async_get_area_by_name(area_name)
    if area is not None:
        return area.id

So, should I add this somewhere else, docu/wiki/etc, to increase the visibility for the community?

luar123 avatar Nov 01 '21 12:11 luar123

I would upload the code to a github repo and then mention it in the wiki. That way people can find it if they need it.

dlashua avatar Nov 02 '21 14:11 dlashua

I found a much easier way to do this that I had to share.

All of the information requested is available as template filters. If we import the template module, we have access to the same data, features, and filters available in Jinja templates.

Just add this import:

import homeassistant.helpers.template as template

and you can make calls like this - note that you just need to pass in the hass object when using in Pyscript

area_entities = template.area_entities(hass, "kitchen")

or

device_id = template.device_id(hass, "light.kitchen_sink")
device_entities = template.device_entities(hass, device_id)

you can even play with Template State objects if you wanted...

state_attributes = template.TemplateStateFromEntityId(hass, "light.kitchen_sink").attributes

MizterB avatar Jan 26 '23 02:01 MizterB

@MizterB you have just opened a pandora's box for me. Thank you!

ALERTua avatar Jan 26 '23 08:01 ALERTua

@luar123 could you please update or close the issue? Thank you

ALERTua avatar Sep 21 '23 09:09 ALERTua

I think there were some good solutions proposed, so I will close here. Thanks everyone

luar123 avatar Sep 22 '23 08:09 luar123