This guide explains how to create devices in Home Assistant's device registry for Bluetooth scanners (Ruuvi Gateways). Based on research of Home Assistant core integrations (ESPHome, MQTT, Bluetooth).
- A device represents a physical device with its own control unit or a service
- Devices appear in Home Assistant's device registry and can be browsed in the UI
- Multiple entities can belong to a single device
- Devices can have parent-child relationships using
via_device
Key properties for device creation:
| Property | Description | Required |
|---|---|---|
identifiers |
Set of (domain, id) tuples that uniquely identify the device | Yes (or connections) |
connections |
Set of (connection_type, identifier) tuples for network connections | Yes (or identifiers) |
name |
Device name | Recommended |
manufacturer |
Device manufacturer | Recommended |
model |
Device model name | Recommended |
sw_version |
Firmware/software version | Optional |
hw_version |
Hardware version | Optional |
configuration_url |
URL to configure the device | Optional |
via_device |
Parent device identifier (for sub-devices) | Optional |
suggested_area |
Suggested area placement | Optional |
When to use: When you have entities (sensors, switches, etc.) that belong to the device
The simplest method - devices are automatically created when entities are registered with device_info:
from homeassistant.helpers.entity import DeviceInfo
class MyEntity(SensorEntity):
"""Example entity."""
def __init__(self, gateway_mac: str):
"""Initialize the entity."""
self._gateway_mac = gateway_mac
self._attr_unique_id = f"{gateway_mac}_signal"
@property
def device_info(self) -> DeviceInfo:
"""Return device information."""
return DeviceInfo(
identifiers={(DOMAIN, self._gateway_mac)},
name=f"Ruuvi Gateway {self._gateway_mac}",
manufacturer="Ruuvi",
model="Ruuvi Gateway",
sw_version="1.0.0",
configuration_url=f"http://{self._gateway_mac}.local",
)Key points:
- Entity MUST have a
unique_idproperty - Entity MUST be loaded via a config entry
- Device is automatically created/updated when entity is added
- Multiple entities with the same
identifierswill be grouped under one device
When to use: When you need to create devices without entities (e.g., hub/gateway devices)
Direct device creation without entities:
from homeassistant.helpers import device_registry as dr
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the integration."""
# Get device registry
device_registry = dr.async_get(hass)
# Create device for each gateway
gateway_mac = "AA:BB:CC:DD:EE:FF"
device_entry = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, gateway_mac)},
connections={(dr.CONNECTION_NETWORK_MAC, gateway_mac)},
name=f"Ruuvi Gateway {gateway_mac}",
manufacturer="Ruuvi",
model="Ruuvi Gateway",
sw_version="1.0.0",
configuration_url=f"http://{gateway_mac}.local",
suggested_area="Living Room",
)
# Store device_id for later use
device_id = device_entry.idFrom homeassistant/components/esphome/manager.py:
@callback
def _async_setup_device_registry(
hass: HomeAssistant,
entry: ESPHomeConfigEntry,
entry_data: RuntimeEntryData
) -> str:
"""Set up device registry feature for a particular config entry."""
device_info = entry_data.device_info
device_registry = dr.async_get(hass)
# Create main device
device_entry = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
configuration_url=configuration_url,
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)},
name=entry_data.friendly_name or entry_data.name,
manufacturer=manufacturer,
model=model,
sw_version=sw_version,
suggested_area=suggested_area,
)
# Handle sub devices with via_device
for sub_device in device_info.devices:
sub_device_entry = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, f"{device_info.mac_address}_{sub_device.device_id}")},
name=sub_device.name or device_entry.name,
manufacturer=manufacturer,
model=model,
sw_version=sw_version,
suggested_area=sub_device_suggested_area,
)
# Update the sub device to set via_device_id
device_registry.async_update_device(
sub_device_entry.id,
via_device_id=device_entry.id,
)
return device_entry.idUse via_device to create hierarchical device structures:
# Create parent device (hub/gateway)
parent_device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, "hub_001")},
name="Main Hub",
manufacturer="Ruuvi",
model="Gateway",
)
# Create child device (connected sensor)
child_device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, "sensor_001")},
name="Temperature Sensor",
manufacturer="Ruuvi",
model="RuuviTag",
via_device=(DOMAIN, "hub_001"), # Links to parent
)Common connection types from device_registry:
CONNECTION_BLUETOOTH = "bluetooth"
CONNECTION_NETWORK_MAC = "mac"
CONNECTION_UPNP = "upnp"
CONNECTION_ZIGBEE = "zigbee"
CONNECTION_ZWAVE = "zwave"Identifiers:
- Domain-specific IDs:
(DOMAIN, serial_number) - Must be unique within your domain
- Use for logical device identification
Connections:
- Physical connection IDs:
(CONNECTION_NETWORK_MAC, mac_address) - Must be globally unique across all integrations
- Use for physical device identification
Best practice: Provide both when possible:
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, gateway_mac)}, # Domain-specific
connections={(dr.CONNECTION_NETWORK_MAC, gateway_mac)}, # Physical
name="My Gateway",
)Use async_update_device() to modify existing devices:
device_registry = dr.async_get(hass)
# Update device properties
device_registry.async_update_device(
device_id,
sw_version="2.0.0",
name="New Name",
suggested_area="Bedroom",
)Implement async_remove_config_entry_device in __init__.py:
async def async_remove_config_entry_device(
hass: HomeAssistant,
config_entry: ConfigEntry,
device_entry: DeviceEntry
) -> bool:
"""Remove a config entry from a device."""
# Perform cleanup
# Return True if successful, False to prevent removal
return TrueDevice info is categorized as Link, Primary, or Secondary:
Primary (full device info):
- Must include: identifiers/connections, manufacturer, model, name
- Optional: configuration_url, sw_version, hw_version, suggested_area, via_device
Link (minimal info):
- Only connections and/or identifiers
- Used for device linking without full registration
Secondary (default fallbacks):
- Uses default_manufacturer, default_model, default_name
- Values overridden if primary values set later
Our integration uses Method 2 (manual device creation) combined with automatic Bluetooth scanner registration.
Ruuvi Gateway Bluetooth Proxy Integration
└── Gateway Device (ruuvi_gateway_bt_proxy domain)
├── Created manually via device_registry.async_get_or_create()
├── Entities:
│ ├── binary_sensor.gateway_status (online/offline)
│ └── number.gateway_rssi_filter (per-gateway RSSI threshold)
└── Linked Device:
└── Bluetooth Scanner (bluetooth domain)
├── Created automatically by Bluetooth integration
├── Linked via source_device_id
└── Source: <gateway_mac>
def _ensure_scanner_registered(self, gateway_mac: str) -> None:
"""Ensure a scanner is registered for the given gateway."""
source = gateway_mac
if source not in self._registered_scanners:
# Step 1: Create gateway device first
self._create_gateway_device(gateway_mac)
# Step 2: Create and register scanner
from habluetooth import BaseHaRemoteScanner
from homeassistant.components.bluetooth import async_register_scanner
scanner = BaseHaRemoteScanner(
source, # MAC address as source
source, # adapter - use same MAC
None, # connector - no connection support
False, # connectable - passive scanner only
)
# Get device ID for linking
gateway_device_id = self._gateway_devices.get(gateway_mac)
# Step 3: Register scanner with Bluetooth integration
# This automatically creates a scanner device in bluetooth domain
unregister_callback = async_register_scanner(
self.hass,
scanner,
connection_slots=0,
source_domain=DOMAIN, # Our integration domain
source_model="Ruuvi Gateway",
source_config_entry_id=self.entry.entry_id,
source_device_id=gateway_device_id, # Links to our device
)
# Step 4: Set up the scanner
scanner.async_setup()
# Store for cleanup
self._scanner_unregister_callbacks[source] = unregister_callback
self._registered_scanners.add(source)
def _create_gateway_device(self, gateway_mac: str) -> None:
"""Create a device entry for a Ruuvi Gateway."""
if gateway_mac in self._gateway_devices:
return
device_registry = dr.async_get(self.hass)
# Create gateway device
gateway_device = device_registry.async_get_or_create(
config_entry_id=self.entry.entry_id,
identifiers={(DOMAIN, gateway_mac)},
connections={(dr.CONNECTION_NETWORK_MAC, gateway_mac)},
name=f"Ruuvi Gateway {gateway_mac}",
manufacturer="Ruuvi",
model="Ruuvi Gateway",
)
# Note: Bluetooth scanner device is automatically created by
# Bluetooth integration when we call async_register_scanner
# with source_device_id parameter
self._gateway_devices[gateway_mac] = gateway_device.id-
Gateway Device - Created manually in our integration domain
- Has entities for status and RSSI filter
- Visible in our integration's device list
-
Scanner Device - Created automatically by Bluetooth integration
- Created when
async_register_scanneris called withsource_device_id - Appears in Bluetooth integration's device list
- Linked to gateway device via
source_device_id - Source is the gateway MAC address
- Created when
-
Why This Works
- Matches ESPHome pattern for Bluetooth proxy devices
- Bermuda and other integrations recognize the scanner
- Clean separation of concerns (gateway vs scanner)
- Proper parent-child relationship via linking
-
Common Mistakes to Avoid
- ❌ Don't manually create a device for the scanner in your domain
- ❌ Don't use custom source strings like
"ruuvi_gw_<mac>" - ❌ Don't forget to pass
source_device_idto link devices - ✅ Do let Bluetooth integration create the scanner device
- ✅ Do use gateway MAC directly as source
- ✅ Do create gateway device before registering scanner
-
Always provide unique identifiers
- Use MAC addresses or serial numbers
- Format consistently (uppercase MAC addresses)
-
Use both identifiers and connections
- Helps with device matching and discovery
- Makes devices more robust
-
Provide meaningful names
- Include identifiable information
- Use friendly formatting
-
Store config_entry_id
- Ensures devices are linked to your integration
- Enables proper cleanup on removal
-
Handle dynamic discovery
- Create devices when first seen
- Update last-seen timestamps
- Clean up stale devices
-
Use via_device for hierarchies
- Shows device relationships in UI
- Enables logical grouping
-
Provide configuration_url when possible
- Improves user experience
- Enables quick access to device settings