- Trustworthy analysis - users should be able to rely on EntityMap's dependency and impact reports
- Minimal footprint - avoid entity bloat, excessive logging, or recorder churn
- Maintainability - clean module boundaries, typed models, testable adapters
- Extensibility - adding new sources or finding types should be straightforward
- Native feel - integrate naturally with Home Assistant's UI patterns
EntityMap does not use DataUpdateCoordinator. Here's why:
| Approach | Pros | Cons | Verdict |
|---|---|---|---|
| DataUpdateCoordinator | Familiar HA pattern, automatic retry | Designed for polling external APIs; unnecessary overhead for local computation | ❌ Not appropriate |
| Pure event-driven | Responsive, no wasted computation | May miss changes not covered by events; complex to ensure completeness | |
| Pure on-demand | User controls when scans run | Graph can be stale; poor UX for dashboard | |
| Hybrid | Responsive + reliable + user-controllable | Slightly more complex setup | ✅ Chosen |
- Startup scan: Runs immediately when Home Assistant is already running (e.g. the integration is added at runtime), otherwise on
EVENT_HOMEASSISTANT_STARTED- viahomeassistant.helpers.start.async_at_started - Event-driven refresh: Listens to
entity_registry_updatedanddevice_registry_updated; rebuilds are debounced so a burst of changes collapses into a single scan - Periodic reconciliation: Timer-based full rescan at configurable intervals (default: 6 hours)
- Manual rescan: Button entity or
entitymap.scanservice
Each scan computes fragility findings once and caches them on the builder (reused by the sensors, panel, and repairs), and records the scan's status, duration, and adapter-error count (exposed as attributes on the Last scan sensor).
DependencyGraph
├── nodes: dict[str, GraphNode] # Keyed by node_id
├── edges: list[GraphEdge] # All directed edges
├── _inbound: dict[str, list[Edge]] # Index: target → edges
└── _outbound: dict[str, list[Edge]]# Index: source → edges
- Devices:
device.{device_id} - Entities:
{domain}.{object_id}(the entity_id) - Areas:
area.{area_id}
Edges point from dependent → dependency:
automation.motion_light → light.living_roommeans the automation depends on the lightlight.living_room → device.abc123means the entity belongs to the device
Each adapter implements SourceAdapter.async_populate(graph):
RegistryAdapter - devices, entities, areas (always runs first)
AutomationAdapter - automation triggers, conditions, actions
ScriptAdapter - script sequences and service calls
SceneAdapter - scene entity membership
GroupAdapter - group member entities
TemplateAdapter - template entity Jinja2 references
Adapters run sequentially. RegistryAdapter must run first to establish base nodes; others add edges and placeholder nodes for missing references. The automation and script adapters share parsing helpers (adapters/_config_parse.py: list coercion, entity-id extraction, template-reference scanning, nested-action iteration). The WebSocket API lives in websocket.py and the area → device → entity tree builder in hierarchy.py.
The fragility engine runs synchronously on the in-memory graph. It detects patterns by iterating edges and comparing against node metadata:
- Missing references: Edge target doesn't exist in nodes
- Device ID references: Edge kind is
DEVICE_TRIGGER/CONDITION/ACTION - Disabled/unavailable: Target node is disabled or unavailable
- Tight coupling: Same automation has 3+ device_id refs to one device
- Hidden dependencies: Automation calls a script with 3+ sub-dependencies
Each finding gets a deterministic ID (MD5 hash of key fields) for stable tracking.
Impact analysis combines:
- Direct dependents - one-hop inbound edges
- Transitive dependents - full BFS traversal of inbound graph
- Risk scoring - weighted formula based on dependent count, type, and edge fragility
- Migration suggestions - pattern-matched recommendations based on edge types
The panel is a native Web Component (HTMLElement + Shadow DOM), served as a static ES-module directory - panel.py registers /entitymap_frontend/ via StaticPathConfig, and the sidebar panel's module_url points at entitymap-panel.js. The code is split into focused modules composed as mixins:
entitymap-panel.js- core element: lifecycle, data loading, render/event wiringlayout.js- shell markup, filter chips, legendgraph-view.js/graph-render.js/graph-interactions.js- graph orchestration, D3 drawing primitives, and interactions (focus, path, scope filter, export, banner)hierarchy-view.js/hierarchy-tree.js- hierarchy list and D3 treefindings-view.js,detail-view.js- issues grid and detail drawerconstants.js(config + palettes),graph-data.js(pure helpers: degree, domain, shortest path),styles.js(CSS)
Capabilities: D3 force layout, SVG rendering, node size by degree, curved parallel edges, type/area/domain filters, search highlighting, focus (neighborhood isolation) and shortest-path highlighting, minimap, zoom controls, PNG export, dark mode, a colorblind-safe palette, a compact density mode, keyboard navigation, and a screen-reader live region. Tunable layout values live in constants.js (GRAPH). D3.js is loaded from CDN on first panel load to keep the integration package small.
- Memory: Graph is stored in-memory; typical HA installation uses <10MB
- CPU: Full scan is async and completes in <2s for most installations
- Storage: No persistent storage beyond the config entry
- Recorder: Summary sensors update only on graph changes (not continuously)
- Network: Zero external network calls