Entity Device Mapper (ent2dev) is a custom Home Assistant (HASS) integration that builds a high-performance mapping of entities to their corresponding devices.
It provides a WebSocket API (ent2dev/map) for frontend cards and scripts to access the device map efficiently without bloating the Home Assistant state machine or database.
-
Efficient Architecture:
- Zero Database Impact: Data is stored in memory and served on-demand via WebSocket. No state attributes are written to the recorder.
- Non-Blocking: Map building is non-blocking to prevent system freezes during startup or large registry updates.
- Debounced Updates: Registry changes are aggregated (default 5s delay) to prevent "thundering herd" updates.
-
Entity-to-Device Mapping:
Maps eachentity_idto its associateddevice_idalong with key device attributes. -
On-Demand Services:
Exposesent2dev.get_devicesandent2dev.get_entitiesfor precise querying in templates and automations. -
Advanced Filtering:
Filter by IDs, names (substring), states (e.g.unavailable), labels, or integrations on demand. -
Universal Negation:
Supports!valuesyntax to exclude specific items (e.g.,labels=['!ephemeral'],integrations=['!tuya']).
ent2dev/
βββ custom_components/
β βββ ent2dev/
β βββ __init__.py # Main logic (API, Debouncer)
β βββ const.py # Configuration
β βββ config_flow.py # Config Entry setup
β βββ manifest.json
βββ README.md
βββ LICENSE
βββ .gitignore
Copy the ent2dev folder into your custom_components directory.
No YAML configuration is required. Add the integration via the UI: Settings > Devices & Services > Add Integration > Search for "Entity Device Mapper"
This integration exposes services to query data on demand. This is the preferred method for automations and templates.
Returns a list of unique devices that match the criteria. Useful for "Missing Devices" dashboards.
Parameters:
device_ids(list, optional): Filter by specific Device IDs.device_names(list, optional): Filter by device name (substring match, case-insensitive).entity_ids(list, optional): Find devices that contain any of these entities.entity_names(list, optional): Find devices containing entities matching these name substrings.entity_states(list, optional): Only match devices if they contain entities in these states (e.g.unavailable,unknown).match_every_entity_state(boolean, optional): Iftrue, include device ONLY if ALL valid entities matchfilter_states. Useful for avoiding devices that are only partially unavailable (e.g. appliances in standby). Prioritisation: if the entities are in multiple states, thematched_statein the output will reflect the state that appears first in yourfilter_stateslist. Example:filter_states: ["unknown", "unavailable"]will reportunknownfor mixed devices.filter_states: ["unavailable", "unknown"]will reportunavailable. Iffalse, the first entity that matches any offilter_stateswill be used to determine thematched_state.include_entities(boolean, optional): Iftrue(default), the output includes a dictionary of all entities belonging to the device. Set tofalseto exclude this and reduce response size.include_attributes(list, optional): Additional device attributes to include (e.g.sw_version,serial_number). If an attribute is missing on a device, it returnsnull.include_disabled(boolean, optional): Include disabled devices and entities (defaultfalse).disabled_by(list, optional): Filter by who disabled the device (e.g.user,!integration).labels(list, optional): Filter by device labels (e.g.kitchen,!ephemeral).integrations(list, optional): Filter by integration domain (e.g.tuya,!hacs).entry_types(list, optional): Filter by entry type (e.g.service,!service).device_models(list, optional): Filter by device model (substring match, e.g.!Chromecast,Hue).
Output Schema:
devices:
- device_id: "..."
name: "Kitchen Light"
model: "LWB010"
manufacturer: "Philips"
area_name: "Kitchen"
floor_name: "Ground Floor"
matched_state: "unavailable" # The state of the first entity that triggered the match
matched_entity_states: # All entities belonging to this device
light.kitchen_1:
state: "unavailable"
name: "Kitchen Light 1"
state: "unavailable"
name: "Kitchen Light 1"
disabled_by: null # Standard field: null if enabled
labels: ["my_label"] # List of device labels
entry_type: "service" # "service" or null (physical)
sw_version: "1.2.3" # Optional attributeReturns a list of entities matching the criteria, enriched with device context.
Parameters:
entity_ids(list, optional): Filter by specific Entity IDs.entity_names(list, optional): Filter by entity name (substring match).device_ids(list, optional): Find entities belonging to these devices.device_names(list, optional): Find entities belonging to devices matching these name substrings.entity_states(list, optional): Filter by entity state.include_attributes(list, optional): Additional entity state attributes to include (e.g.unit_of_measurement).include_disabled(boolean, optional): Include disabled entities (defaultfalse).disabled_by(list, optional): Filter by who disabled the entity (e.g.user,!integration).labels(list, optional): Filter by device labels.integrations(list, optional): Filter by device integration domain.
Output Schema:
entities:
- entity_id: "sensor.kitchen_temp"
name: "Kitchen Temperature"
state: "21.5"
unit_of_measurement: "Β°C" # Optional attribute (flattened)
disabled_by: null # Standard field
device: # Nested to disambiguate from entity attributes
device_id: "..."
name: "Kitchen Sensor"
area_name: "Kitchen"
floor_name: "Ground Floor"You can call the services directly from the frontend using the standard call_service command. This is preferred over the legacy map command as it supports filtering.
Example: getting offline devices in a custom card:
hass.callWS({
type: "call_service",
domain: "ent2dev",
service: "get_devices",
service_data: {
entity_states: ["unavailable", "unknown"]
},
return_response: true
}).then((response) => {
// response = { devices: [ ... ] }
console.log("Offline Devices:", response.devices);
});- Remove the integration from Settings > Devices & Services.
- Delete the
ent2devfolder fromcustom_components. - Restart Home Assistant.
The ent2dev integration effectively functions as a specialized "Database View" or "Stored Procedure" for Home Assistant's internal registries. While native tools can technically achieve similar results, ent2dev offers significant performance and usability advantages for specific "cross-domain" queries.
There is high functional overlap, but low architectural overlap. You can technically replicate almost everything ent2dev does using Jinja2 templates, but the implementation differs drastically in efficiency.
| Use Case | HA Native (Jinja2) | ent2dev Service |
|---|---|---|
| Get Device Entities | device_entities(device_id) |
get_entities(device_ids=[...]) |
| Get Device Area | area_name(device_attr(dev_id, 'area_id')) |
Included automatically (area_name) |
| Filter by State | `states | selectattr('state', 'eq', 'X')` |
| Get Offline Devices | Complex Loop (Iterate States -> Get Entity -> Get Device -> Deduplicate) | Single call: get_devices(entity_states=['unavailable']) |
This is where ent2dev shines. Home Assistant's data model is hierarchical but disjointed:
- Entities belong to Devices.
- Devices belong to Areas.
- Areas belong to Floors.
To get a flat list of "Offline Devices with their Floor Name" natively, you must traverse this hierarchy for every single item in a loop.
- Native Jinja: O(N) Iteration + 3 lookups per item (Device->Area->Floor). In Jinja, this is computationally expensive and slow (
statesobject can have thousands of items). ent2dev:- Iterates the Entity Registry in compiled Python (much faster than Jinja interpretation).
- Performs O(1) dictionary lookups for Device, Area, and Floor registries.
- Returns a pre-joined, flattened object.
ent2dev.get_entities: If you providedevice_ids, it useser.async_entries_for_device(an internal O(1) index). It skips scanning the entire entity registry.- Native
device_entities: Also uses internal indices, so performance here is comparable. - Native
selectattr: Iterates the entire list.ent2devalso iterates candidates, but Python execution is significantly faster than Jinja template rendering for large sets.
-
"Inverse" Querying (Entity State -> Device)
- Native: There is no direct way to ask "Give me all devices that have an unavailable entity". You have to find the entities first, then map them to devices, then deduplicate the device list.
ent2dev: Handles the deduction and deduplication logic internally, returning a clean list of unique devices.
-
Arbitrary List Querying & Batch Processing
- Advantage: You can pass a list of 50 disparate
device_ids(e.g., from a group or user selection) and get a rich return object for all of them in one go. - Native: You would need to write a complex Jinja loop to iterate your list, perform lookups for each, and manually construct a JSON-like string structure if you wanted to pass it to a frontend card.
- Advantage: You can pass a list of 50 disparate
-
Structured Data Objects
- Advantage:
ent2devreturns true Python/JSON objects (Dictionaries/Lists). - Native: Jinja templates primarily output Strings. While modern HA supports native types in variables, passing complex nested structures (Device -> Area -> Floor) out of a template sensor usually requires serializing to JSON strings and re-parsing, which is brittle and slow.
- Advantage:
-
Implicit Context Resolution (Flattening)
- It treats the
Device -> Area -> Floorhierarchy as a single flat record. This allows you to say "Group byfloor_name" immediately in your frontend card, whereas natively you'd have to perform those lookups inside the card's JavaScript or Jinja loop.
- It treats the
- Use Native Templates if you are looking up simple info for a single known entity (e.g.
is_state(...)) or iterating a small list. - Use
ent2devif you need to:- Query "up the tree" (find devices based on entity states).
- Process large lists (100+ items).
- Get joined/enriched data (Device+Area+Floor) for a collection of items.
- Return complex data structures to the frontend without string parsing.