Accessing integration entities

I’ve decided to challenge myself to learn how to write an integration for my View Assist project as it is now held together with duct tape (actually it’s configured using template sensors and some helpers).

To be clear, I am not a programmer so this has been most challenging for me but I have made progress. Through help of some kind folks on the VA discord, web searches and some AI help I am at the point of creating services.

At the moment, the challenge is to convert this template into a service:

  - variables:
      target_satellite_device: >-
        {% for sat_id in integration_entities('view_assist')|select('match',
        'sensor\.')|list %}
          {% set sat = states[sat_id] %}
          {% if sat and ((device_id(sat.attributes.mic_device) == trigger.device_id) or (device_id(sat.attributes.display_device) == trigger.device_id)) %}
            {{ sat.entity_id }}
          {% endif %}
        {% endfor %}

So the example above pulls all sensor devices from the view_assist domain then loops through and compares the trigger.device_id with the current iteration’s mic and display device returning the match. Purpose? Home Assistant will return the device_id of the device that made the request in a custom sentence automation. The end goal is to find what VA device has the attribute that has that device id. Once that is found then that VA satellite can be used for finding the appropriate attributes associated with it. Those are used to set media/music player, display device, attribute variables, etc that are used in the custom sentence automation action section.

For better understanding, a VA device could look like this:

template:
  - sensor:
    - name: ViewAssist_livingroom
      state: ""
      attributes:
        type: view_audio
        mic_device: "sensor.streamassist_livingroom_stt" 
        mediaplayer_device: "media_player.browsermod_livingroom"
        musicplayer_device:  "media_player.browsermod_livingroom"  
        display_device: "sensor.browsermod_livingroom_browser_path" 
        browser_id: "ViewAssist-livingroom"

Here’s the working version of my init.py:

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import (
    HomeAssistant,
    ServiceCall,
    ServiceResponse,
    SupportsResponse,
)
from .const import DOMAIN
import homeassistant.helpers.entity_registry as er
import logging

_LOGGER = logging.getLogger(__name__)

PLATFORMS: list[Platform] = [Platform.SENSOR]

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
    """Set up View Assist from a config entry."""
    hass.data.setdefault(DOMAIN, {})
    # Request platform setup
    await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)


    async def handle_get_id(call: ServiceCall) -> ServiceResponse:
        """Handle a device lookup call."""
        hass: HomeAssistantType = call.hass
        entity_registry = er.async_get(hass)
        entity_id = call.data.get('entity_id')
        
        entity_entry = entity_registry.async_get(entity_id)
        if entity_entry and entity_entry.device_id:
            device_id = entity_entry.device_id
            return {"device_id": device_id}
        
        return {"error":  "Device ID not found"}

        # Examples for later on how to get and return variables
        # entity_id = call.data.get('entity_id')
        # state = hass.states.get(entity_id)
        # mic_device = state.attributes.get('mic_device')
        # display_device = state.attributes.get('display_device')
        # return {"mic_device": mic_device, "display_device": display_device}

    hass.services.async_register(
        DOMAIN,
        "get_id",
        handle_get_id,
        supports_response=SupportsResponse.ONLY,
    )

    async def handle_satellite_lookup(call: ServiceCall) -> ServiceResponse:
        """Handle a satellite lookup call."""
        device_id = call.data.get("device_id")
        return {"device_id_received": device_id}

    hass.services.async_register(
        DOMAIN,
        "get_satellite",
        handle_satellite_lookup,
        supports_response=SupportsResponse.ONLY,
    )

    return True
    
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
    """Unload a config entry."""
    if unloaded := await hass.config_entries.async_forward_entry_unload(entry, PLATFORMS):
        hass.data[DOMAIN].pop(entry.entry_id)
    return unloaded

What I’ve managed to do here is create a service that given an entity_id will look for and return the corresponding device_id. Eventually this will be used to match against the VA device’s attribute mic_device and display_device as seen in the template above.

My question is how do I list all entities associated with my view_assist integration? I think once I can create a loop for each of the entities I can then check the attributes’ device_id for a match for each element in the loop and get the same result the template example provides.

I am finding my way through the integration creation but having a hard time finding easy to follow examples of what I want to do. Again, I am a novice so while this may be easy for most, I am struggling. I am confident that with a few examples my understanding will grow. Any help is most appreciated.

So you can get a list of the entities by querying the entity registry. Below are 2 examples. 1 to get the entities by device if you have the device id and 1 to get all entities for your integration instance.

from homeassistant.helpers import entity_registry as er

entity_registry = er.async_get(self.hass)

device_entities=er.async_entries_for_device(
                    entity_registry, device_id, include_disabled_entities=True
 )

integration_entities=er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)

Hope that helps.

Thank you for the speedy help. Unfortunately I’m having problems getting the integration to load after making those additions. Can you check what I’ve done and let me know what I am doing wrong?

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import (
    HomeAssistant,
    ServiceCall,
    ServiceResponse,
    SupportsResponse,
)
from .const import DOMAIN
#import homeassistant.helpers.entity_registry as er
from homeassistant.helpers import entity_registry as er

import logging

_LOGGER = logging.getLogger(__name__)

PLATFORMS: list[Platform] = [Platform.SENSOR]

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
    """Set up View Assist from a config entry."""
    hass.data.setdefault(DOMAIN, {})
    # Request platform setup
    await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)


    async def handle_get_id(call: ServiceCall) -> ServiceResponse:
        """Handle a device lookup call."""
        hass: HomeAssistantType = call.hass
        entity_registry = er.async_get(hass)
        entity_id = call.data.get('entity_id')
        
        entity_entry = entity_registry.async_get(entity_id)
        if entity_entry and entity_entry.device_id:
            device_id = entity_entry.device_id
            return {"device_id": device_id}
        
        return {"error":  "Device ID not found"}

        # entity_id = call.data.get('entity_id')
        # state = hass.states.get(entity_id)
        # mic_device = state.attributes.get('mic_device')
        # display_device = state.attributes.get('display_device')
        # return {"mic_device": mic_device, "display_device": display_device}

    hass.services.async_register(
        DOMAIN,
        "get_id",
        handle_get_id,
        supports_response=SupportsResponse.ONLY,
    )

    async def handle_satellite_lookup(call: ServiceCall) -> ServiceResponse:
        """Handle a satellite lookup call."""
        device_id = call.data.get("device_id")
        return {"device_id_received": device_id}

    hass.services.async_register(
        DOMAIN,
        "get_satellite",
        handle_satellite_lookup,
        supports_response=SupportsResponse.ONLY,
    )
#####

    async def handle_get_members(call: ServiceCall) -> ServiceResponse:
        """Handle a get members lookup call."""
        entity_registry = er.async_get(self.hass)
		integration_entities=er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)

        return {"integration_entities": integration_entities}

    hass.services.async_register(
        DOMAIN,
        "get_members",
        handle_get_members,
        supports_response=SupportsResponse.ONLY,
    )
#####

    return True
    
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
    """Unload a config entry."""
    if unloaded := await hass.config_entries.async_forward_entry_unload(entry, PLATFORMS):
        hass.data[DOMAIN].pop(entry.entry_id)
    return unloaded

Note my logs are showing that the error received points to:

integration_entities=er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)

I’ve also changed my import to match yours though I think what I had previously was just a different way of doing the same?

I presume the error complains about config_entry not being defined? The line should be

integration_entities=er.async_entries_for_config_entry(entity_registry, entry.entry_id)

as that is what you defined the parameter as in async_setup_entry.

Thanks again. I found the original problem to be a spacing issue. I got excited last night to try what you provided and the editor I was using did not format the same as my office machine. Sorry for that.

Unfortunately I did try as it was before and with the change above and I am getting an error. This time it is on line 67 which is:

entity_registry = er.async_get(self.hass)

the error is very generic:

File "/config/custom_components/view_assist/__init__.py", line 67, in handle_get_members

I’ve posted the code here if you can look. Figured it might make it easier to read:

https://dpaste.org/kkq2h

Thanks for the help and your patience. Believe it or not, I am starting to piece some of this together but still a long ways to go.

It should be just hass not self.hass. sorry was doing on my phone and copied and pasted from one of my integrations but it doesn’t quite suit how you are doing it.

Ie

entity_registry = er.async_get(hass)

Wow! Some day I hope to be able to provide help to users from my phone!! Absolutely no apology necessary. Thank you!

I did notice that bit was different than what I had done before but admittedly I thought it may be required.

I think it is working now but I am getting an error on the return. If you can, here’s what I have:

    async def handle_get_members(call: ServiceCall) -> ServiceResponse:
        """Handle a get members lookup call."""
        entity_registry = er.async_get(hass)
        integration_entities=er.async_entries_for_config_entry(entity_registry, entry.entry_id)

        return {"device_id_received": integration_entities}

and the error is:
Failed to perform the action view_assist.get_members. Invalid JSON in response

I am calling this service without any options. I am assuming that it will provide the members of my view_assist domain as a list. Is that incorrect to assume? I’m trying to add to this incrementally and was hoping to see what the variable had in it to move to the next part.

Yes it will return a list of all entities for your integration.

Your issue is that integration_entities is a list of RegistryEntry class objects, which will not convert to json. If you want the list of entity ids, do this.

async def handle_get_members(call: ServiceCall) -> ServiceResponse:
        """Handle a get members lookup call."""
        entity_registry = er.async_get(hass)
        integration_entities=er.async_entries_for_config_entry(entity_registry, entry.entry_id)
        entity_ids = [entity.entity_id for entity in integration_entities]

        return {"device_id_received": entity_ids}

Here is a link to the RegistryEntity class so you can see what parameters are available.


I am able to use your code and it does return a value but it is only one of the members and not all of them. I ran it first with two members and it returned ViewAssist-livingroom_test only. I added the ViewAssist-masterbathroom_vpe and when I called the service again it returned that entity and not the livingroom_test one like it did before.

Is it possible that this I just listing the last value rather than all of them?

Ok, so looking at your screenshot, you are running 3 instances of your integration.

Apologies, if i am teaching you to suck eggs but maybe some definitions may help understand the issue.

Entity - sensor or switch etc
Device - a representation of a physical device that has 1 or more entities grouped to it
Instance - a running copy of your integration
Config entry - the configuration specific to an instance of your integration with a unique id defining that instance

So, as you can see from the above, you have 3 copies of your code running, no devices and 1 entity per instance of code.

Now, running multiple instances can get tricky when you get into things like services.

As HA starts, it is loading your code multiple times and therefore executing your service registration for each instance. As each registration effectively overwrites the previous, you are left with just 1 registration (likely your last configured instance).

So, when you call that service, it runs only on the last loaded instance. The service gets the list of entity ids for that config entry id (ie from the above, just that 1 instance of your code). This is what you are seeing.

There are 2 ways to handle this.

  1. In your service call, you need to get all the config entry ids for your domain and then iterate over that list, getting the entities for each and then return that.

  2. Make your integration a single instance that can handle multiple devices.

The determination to which option is whether you have some single config (device ip, ussrname/password to cloud service etc) that allows HA to find each one.

If yes, i highly recommend going with option 2 as supporting multi instance is complicated if you are new to this.

If not, i’ll post you a copy of your service function that will return all entities for all instances later today, but be aware that as you progress, you will likely bump into multi instance issues some more and be banging your head!

So, heres how to get all entities for all instances of your integration. Excuse any formatting issues, pita doing code on phone!

async def handle_get_members(call: ServiceCall) -> ServiceResponse:
        """Handle a get members lookup call."""
        entity_registry = er.async_get(hass)

        entities = []

        entry_ids = [entry for entry in hass.config_entries.async_entries(DOMAIN)]

        for entry_id in entry_ids:
            integration_entities=er.async_entries_for_config_entry(entity_registry, entry_id)
            entity_ids = [entity.entity_id for entity in integration_entities]
            entities.append(entity_ids)

        return {"device_id_received": entity_ids}

I want to reiterate how much I appreciate the help you are giving. The explanation you gave is thorough and I do understand things better now and see why things were not working as I had hoped.

I would like to keep things the way they are structurally. This SHOULD be the only instance where I need to query like this. All other actions will use the result of this service request as inputs. Hopefully that makes sense.

I did try the code you provided and can see how you are iterating over each instance and then trying to pull the information for each within that instance. Unfortunately I am returned with an empty list. Can you take a look and see if you can spot what might be going wrong or how I might be able to troubleshoot?

Sorry last line should be

return {"device_id_received": entities}

EDIT: And you’re very welcome for the help. It not easy to start developing in HA but once you get the general gist, it all starts to make sense. Not always easy to read the docs and get it.

EDIT (again!): if that doesnt work, update your code in your github repo and i’ll try running it in my dev env and see whats wrong.

Ahh. I do see the error now. Not sending the list that was set. Unfortunately I ended up with this:

I’ve updated the repo to reflect. Do note that I have a bunch of previous iterations commented out so excuse the mess.

Thanks

Apologies, i see another typo. Must get better at using my phone!

The line

entry_ids = [entry for entry in hass.config_entries.async_entries(DOMAIN)]

should be

entry_ids = [entry.entry_id for entry in hass.config_entries.async_entries(DOMAIN)]
1 Like

And also line

entities.append(entity_ids)

should be

entities.extend(entity_ids)

This is why you were getting 3 lists and not one.

1 Like

Wahoo!!!

device_id_received:
  - sensor.viewassist_office_test
  - sensor.viewassist_livingroom_test
  - sensor.viewassist_masterbathroom_vpe

Absolutely fantastic! I will continue with the translation of the template to this service using what you’ve provided. Most importantly, this has shorten the time needed to get here while teaching me a lot about the integrations.

I do understand that writing an integration is advanced and that finding examples can be hard. Your help in working with me on this specific example is invaluable to my success and reduces frustration. Time spent on side projects is always limited so this effort you’ve provided is so appreciated!

You are a champion!

1 Like

No problem. Do reach out again if you need more help.

1 Like

Well that didn’t take long. I’m hacking away at this. I’m trying to grab the device_id of the mic_id variable as I will need that for the eventual comparison. I am obviously not doing this correctly despite a bunch of searching:

    async def handle_get_members(call: ServiceCall) -> ServiceResponse:
            """Handle a get members lookup call."""
            entity_registry = er.async_get(hass)

            entities = []

            entry_ids = [entry.entry_id for entry in hass.config_entries.async_entries(DOMAIN)]

            for entry_id in entry_ids:
                integration_entities=er.async_entries_for_config_entry(entity_registry, entry_id)
                entity_ids = [entity.entity_id for entity in integration_entities]
                entities.extend(entity_ids)

    
            # Fetch the 'mic_device' attribute for each entity
            mic_devices = []
            for entity_id in entities:
                state = hass.states.get(entity_id)
                if state:
                    mic_id = state.attributes.get("mic_device")
                    if mic_id:
                        mic_device_id = mic_id.attributes.get(ATTR_ENTITY_ID)
                        mic_devices.append(mic_device_id)

            # Return the list of mic_device attributes
            return {"mic_devices": mic_devices}

This is the line I’m guessing at mic_device_id = mic_id.attributes.get(ATTR_ENTITY_ID) . Where can I get more information on how to use the get functions? Struggling but still feeling confident I can put it together with a few more pieces.

So, just trying to get my head around how your data is structured, but I think from looking at the code in your repo, the attribute mic_device is an entity id of another entity in HA?

So, to get the value of that attribute for each of your entity ids, you would just need to do this

        # Fetch the 'mic_device' attribute for each entity
        mic_devices = []
        for entity_id in entities:
            if state := hass.states.get(entity_id):
                if mic_id := state.attributes.get("mic_device"):
                    mic_devices.append(mic_id)

        # Return the list of mic_device attributes
        return {"mic_devices": mic_devices}