mirror of
https://github.com/home-assistant/core.git
synced 2026-08-03 20:24:55 +02:00
Fix via_device race in elkm1 (#177714)
This commit is contained in:
@@ -26,7 +26,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.network import is_ip_address
|
||||
@@ -58,6 +58,7 @@ from .discovery import (
|
||||
async_trigger_discovery,
|
||||
async_update_entry_from_discovery,
|
||||
)
|
||||
from .entity import create_elk_system_device_info
|
||||
from .models import ELKM1Data
|
||||
from .services import async_setup_services
|
||||
|
||||
@@ -308,6 +309,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ElkM1ConfigEntry) -> boo
|
||||
keypads={},
|
||||
)
|
||||
|
||||
# Register the ElkM1 system device before forwarding platforms so entities
|
||||
# on any platform can deterministically resolve it as their via_device.
|
||||
dr.async_get(hass).async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
**create_elk_system_device_info(elk, prefix, entry.unique_id),
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
@@ -10,6 +10,7 @@ from elkm1_lib.elk import Elk
|
||||
|
||||
from homeassistant.const import ATTR_CONNECTIONS
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
@@ -47,6 +48,23 @@ def create_elk_entities(
|
||||
return entities
|
||||
|
||||
|
||||
def create_elk_system_device_info(elk: Elk, prefix: str, mac: str | None) -> DeviceInfo:
|
||||
"""Return the device info for the ElkM1 system device."""
|
||||
device_name = "ElkM1"
|
||||
if prefix:
|
||||
device_name += f" {prefix}"
|
||||
device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{prefix}_system")},
|
||||
manufacturer="ELK Products, Inc.",
|
||||
model="M1",
|
||||
name=device_name,
|
||||
sw_version=elk.panel.elkm1_version,
|
||||
)
|
||||
if mac:
|
||||
device_info[ATTR_CONNECTIONS] = {(CONNECTION_NETWORK_MAC, mac)}
|
||||
return device_info
|
||||
|
||||
|
||||
def generate_unique_id(prefix: str, element: Element) -> str:
|
||||
"""Generate a unique id."""
|
||||
# unique_id starts with elkm1_ iff there is no prefix
|
||||
@@ -128,10 +146,16 @@ class ElkEntity(Entity):
|
||||
@override
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Device info connecting via the ElkM1 system."""
|
||||
config_entry = self.platform.config_entry
|
||||
assert config_entry
|
||||
return DeviceInfo(
|
||||
name=self._element.name,
|
||||
identifiers={(DOMAIN, self._unique_id)},
|
||||
via_device=(DOMAIN, f"{self._prefix}_system"),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
self.hass,
|
||||
(DOMAIN, f"{self._prefix}_system"),
|
||||
config_entry_id=config_entry.entry_id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -142,16 +166,4 @@ class ElkAttachedEntity(ElkEntity):
|
||||
@override
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Device info for the underlying ElkM1 system."""
|
||||
device_name = "ElkM1"
|
||||
if self._prefix:
|
||||
device_name += f" {self._prefix}"
|
||||
device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{self._prefix}_system")},
|
||||
manufacturer="ELK Products, Inc.",
|
||||
model="M1",
|
||||
name=device_name,
|
||||
sw_version=self._elk.panel.elkm1_version,
|
||||
)
|
||||
if self._mac:
|
||||
device_info[ATTR_CONNECTIONS] = {(CONNECTION_NETWORK_MAC, self._mac)}
|
||||
return device_info
|
||||
return create_elk_system_device_info(self._elk, self._prefix, self._mac)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Tests for the Elk-M1 Control init."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from homeassistant.components.elkm1.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PREFIX, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from . import MOCK_MAC, _patch_discovery, mock_elk
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
def _mocked_elk_with_light() -> MagicMock:
|
||||
"""Return a mocked Elk that exposes a single PLC light and nothing else."""
|
||||
light = MagicMock()
|
||||
light.index = 0
|
||||
light.name = "Test Light"
|
||||
light.default_name.return_value = "test_light"
|
||||
light.configured = True
|
||||
light.status = 0
|
||||
light.as_dict.return_value = {}
|
||||
|
||||
elk = mock_elk(sync_complete=True)
|
||||
elk.is_connected.return_value = True
|
||||
for collection in (
|
||||
"areas",
|
||||
"tasks",
|
||||
"counters",
|
||||
"keypads",
|
||||
"zones",
|
||||
"outputs",
|
||||
"settings",
|
||||
"thermostats",
|
||||
):
|
||||
setattr(elk, collection, [])
|
||||
elk.lights = [light]
|
||||
# The panel sensor is an attached entity; skip it so the only registered
|
||||
# device besides the system device is the light's own (via_device) device.
|
||||
elk.panel.configured = False
|
||||
elk.panel.elkm1_version = "1.0.0"
|
||||
elk.panel.temperature_units = "F"
|
||||
return elk
|
||||
|
||||
|
||||
async def test_light_via_device_links_to_system_device(
|
||||
hass: HomeAssistant, device_registry: dr.DeviceRegistry
|
||||
) -> None:
|
||||
"""A child (light) device links to the system device registered at setup.
|
||||
|
||||
With auto configure and only a light present, no sibling attached entity
|
||||
creates the system device, so the link resolves only because setup
|
||||
registers the system device before platforms are forwarded.
|
||||
"""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_HOST: "elks://1.2.3.4",
|
||||
CONF_USERNAME: "user",
|
||||
CONF_PASSWORD: "pass",
|
||||
CONF_PREFIX: "",
|
||||
"auto_configure": True,
|
||||
},
|
||||
unique_id=MOCK_MAC,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with (
|
||||
_patch_discovery(),
|
||||
patch(
|
||||
"homeassistant.components.elkm1.Elk",
|
||||
return_value=_mocked_elk_with_light(),
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
system_device = device_registry.async_get_device_by_identifier(
|
||||
(DOMAIN, "_system"), config_entry.entry_id
|
||||
)
|
||||
assert system_device is not None
|
||||
assert system_device.name == "ElkM1"
|
||||
|
||||
light_device = device_registry.async_get_device_by_identifier(
|
||||
(DOMAIN, "elkm1_test_light"), config_entry.entry_id
|
||||
)
|
||||
assert light_device is not None
|
||||
assert light_device.via_device_id == system_device.id
|
||||
Reference in New Issue
Block a user