Home Assistant Device Registry#
The HA device registry tracks every device known to Home Assistant. Integrations register devices either automatically (via an entity's device_info property) or manually (by calling DeviceRegistry.async_get_or_create() directly). Each device entry carries identifying metadata β identifiers, connections, manufacturer, model, sw_version, configuration_url, etc. β and can express a parent-child relationship via via_device_id. (Device registry docs)
Key API entrypoints:
dr.async_get(hass)β retrieve the device registry instancedevice_registry.async_get_or_create(config_entry_id=..., identifiers=..., ...)β register or upsert a devicedevice_registry.async_remove_device(device_id)β remove a device by iddevice_registry.async_get_device_by_identifier((DOMAIN, id), config_entry_id)β look up a device scoped to a config entry (replaces deprecatedasync_get_device)
Identifier format: Identifiers are two-tuples (DOMAIN, unique_string). Each identifier is unique per config entry (as of HA 2026.8). The spoolman integration uses a three-tuple pattern (DOMAIN, url, unique_id) as identifiers β e.g. (DOMAIN, self.config[CONF_URL], "spool_3") β to namespace devices per server URL.
Device Hierarchy: via_device / via_device_id#
Devices can model a parent-child topology (e.g., a hub and its sub-devices) using the via_device / via_device_id field. The parent must be registered before the child.
In the spoolman integration, spool sensors are organized under a two-level hierarchy:
- Location Hub device β a virtual device per spool location (e.g.
location_printer_shelf), registered first. - Spool device β registered as a child of its location hub using
via_device.
Both are created via explicit device_registry.async_get_or_create() calls before entities are added, ensuring the parent exists when HA resolves the via_device reference.
HA 2026.8+ change: The
DeviceInfo["via_device"]identifier-tuple form is deprecated (see below). Usevia_device_id(a device id string) instead.
Child devices (HA 2026.9+): A lighter alternative to via_device_id is the new parent_device_id concept ("child devices"). Child devices carry no hardware/firmware metadata; they reference their parent through parent_device_id. HA recommends migrating from via_device_id hierarchies to child devices where applicable.
Deprecations (HA 2026.8 β 2027.8)#
HA 2026.8 introduced a sweeping change: devices now belong to a single config entry (not shared across integrations). This made identifier-based lookups ambiguous and triggered several API deprecations. All deprecated APIs log a warning at runtime and stop working in HA Core 2027.8.
| Deprecated | Replacement | Notes |
|---|---|---|
DeviceInfo["via_device"] / async_get_or_create(via_device=...) | via_device_id (device id string) | Identifier tuples are no longer globally unique, so the old form is ambiguous |
DeviceEntry.config_entries | DeviceEntry.config_entry_id | Device now owned by a single config entry |
DeviceEntry.primary_config_entry | DeviceEntry.config_entry_id | Same reason |
DeviceRegistry.async_get_device(identifiers=...) | async_get_device_by_identifier((DOMAIN, id), config_entry_id) | Scoped lookup removes ambiguity |
async_update_device(add_config_entry_id=...) | async_update_device(new_config_entry_id=...) | Moving a device is now a single call |
Passing both
via_deviceandvia_device_idraisesHomeAssistantErrorimmediately β they are mutually exclusive.
Spoolman Integration: Active Warning (Issue #887)#
As of 2026-09-02, the spoolman integration triggers two deprecation warnings on HA 2027.8.0 because it still passes via_device (identifier-tuple form):
Warning 1 β raised in sensors/spool.py line 127 when async_get_or_create is called for the spool device:
WARNING [...] custom integration 'spoolman' calls `device_registry.async_get_or_create`
with a deprecated `via_device` parameter; use `via_device_id` instead at
custom_components/spoolman/sensors/spool.py, line 127
Warning 2 β surfaced at sensor.py line 415 (async_add_entities) because entity device info is resolved during entity registration.
Root cause: Line 111 of spool.py passes via_device=(DOMAIN, self.config[CONF_URL], f"location_{location_name}"). This is an identifier tuple; HA 2026.8+ requires a device id string.
Fix: After registering the location hub device, use its returned device.id as via_device_id in the spool device info:
# Step 1 β register location hub first (unchanged)
location_device = device_registry.async_get_or_create(
config_entry_id=..., **location_device_info
)
# Step 2 β pass the hub's id as via_device_id (not the identifier tuple)
spool_device = device_registry.async_get_or_create(
config_entry_id=...,
identifiers=...,
via_device_id=location_device.id, # β was: via_device=(DOMAIN, url, "location_X")
...
)
See the HA 2026.8 migration blog post for canonical examples; the telegram_bot refactor (core PR #176606) is the closest analogue.
Device Lifecycle Management in Spoolman#
The integration proactively cleans up stale devices on every startup via three functions in __init__.py:
| Function | What it removes |
|---|---|
_async_remove_old_location_devices() | Legacy devices from pre-v1.2.0 whose identifier third element starts with "location_" |
_async_cleanup_orphaned_spool_devices() | Spool devices whose spool ID no longer exists in the Spoolman API |
_async_cleanup_extra_field_entities() | Extra-field sensor entities for fields deleted from Spoolman |
All three are called from async_setup_entry() after the coordinator's first data refresh.
Device identifier evolution:
| Version | Spool identifier | Effect |
|---|---|---|
| pre-v1.2.0 | (DOMAIN, "spool_<id>") β 2-tuple | Each sensor registered as a separate device |
| v1.2.0+ | (DOMAIN, url, "spool_<id>") β 3-tuple | All sensors for a spool group under one device |
Filament sensors use a flat device with identifier (DOMAIN, url, "Filaments") and no via_device relationship.