Fix via_device race in yale_smart_alarm (#177858)

This commit is contained in:
Erik Montnemery
2026-08-03 13:06:50 +02:00
committed by GitHub
parent c9023c6c98
commit 00eda4caa1
4 changed files with 81 additions and 15 deletions
@@ -4,7 +4,7 @@ from homeassistant.components.lock import CONF_DEFAULT_CODE, DOMAIN as LOCK_DOMA
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_CODE, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .const import LOGGER, PLATFORMS
from .coordinator import YaleDataUpdateCoordinator
@@ -19,6 +19,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: YaleConfigEntry) -> bool
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
# Register the alarm panel device so child devices can link to it via
# via_device_id, since the panel is otherwise only registered by a sibling
# entity on a concurrently loaded platform.
dr.async_get(hass).async_get_or_create(
config_entry_id=entry.entry_id,
**coordinator.device_info,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -10,12 +10,20 @@ from yalesmartalarmclient.exceptions import AuthenticationError
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
if TYPE_CHECKING:
from . import YaleConfigEntry
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, YALE_BASE_ERRORS
from .const import (
DEFAULT_SCAN_INTERVAL,
DOMAIN,
LOGGER,
MANUFACTURER,
MODEL,
YALE_BASE_ERRORS,
)
class YaleDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
@@ -36,6 +44,19 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
)
self.locks: list[YaleLock] = []
@property
def device_info(self) -> DeviceInfo:
"""Return device info for the alarm panel."""
panel_info = self.data["panel_info"]
return DeviceInfo(
identifiers={(DOMAIN, self.config_entry.data[CONF_USERNAME])},
connections={(CONNECTION_NETWORK_MAC, panel_info["mac"])},
manufacturer=MANUFACTURER,
model=MODEL,
name=self.config_entry.title,
sw_version=panel_info["version"],
)
def _yale_setup(self) -> tuple[YaleSmartAlarmClient, list[YaleLock]]:
"""Set up connection to Yale."""
yale = YaleSmartAlarmClient(
@@ -3,7 +3,8 @@
from yalesmartalarmclient import YaleLock
from homeassistant.const import CONF_USERNAME
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -25,7 +26,11 @@ class YaleEntity(CoordinatorEntity[YaleDataUpdateCoordinator]):
manufacturer=MANUFACTURER,
model=MODEL,
identifiers={(DOMAIN, data["address"])},
via_device=(DOMAIN, coordinator.config_entry.data[CONF_USERNAME]),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, coordinator.config_entry.data[CONF_USERNAME]),
config_entry_id=coordinator.config_entry.entry_id,
),
)
@@ -43,7 +48,11 @@ class YaleLockEntity(CoordinatorEntity[YaleDataUpdateCoordinator]):
manufacturer=MANUFACTURER,
model=MODEL,
identifiers={(DOMAIN, lock.sid())},
via_device=(DOMAIN, coordinator.config_entry.data[CONF_USERNAME]),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, coordinator.config_entry.data[CONF_USERNAME]),
config_entry_id=coordinator.config_entry.entry_id,
),
)
self.lock_data = lock
@@ -56,12 +65,4 @@ class YaleAlarmEntity(CoordinatorEntity[YaleDataUpdateCoordinator], Entity):
def __init__(self, coordinator: YaleDataUpdateCoordinator) -> None:
"""Initialize an Yale device."""
super().__init__(coordinator)
panel_info = coordinator.data["panel_info"]
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.config_entry.data[CONF_USERNAME])},
manufacturer=MANUFACTURER,
model=MODEL,
name=coordinator.config_entry.title,
connections={(CONNECTION_NETWORK_MAC, panel_info["mac"])},
sw_version=panel_info["version"],
)
self._attr_device_info = coordinator.device_info
+37 -1
View File
@@ -2,11 +2,14 @@
from unittest.mock import Mock, patch
import pytest
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.components.yale_smart_alarm.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .conftest import ENTRY_CONFIG, OPTIONS_CONFIG
@@ -44,6 +47,39 @@ async def test_setup_entry(
assert entry.state is ConfigEntryState.NOT_LOADED
@pytest.mark.parametrize(
("load_platforms", "child_identifier"),
[
pytest.param([Platform.LOCK], "1111", id="lock"),
pytest.param([Platform.BINARY_SENSOR], "RF4", id="binary_sensor"),
],
)
async def test_child_devices_linked_to_panel(
hass: HomeAssistant,
load_config_entry: tuple[MockConfigEntry, Mock],
device_registry: dr.DeviceRegistry,
child_identifier: str,
) -> None:
"""Test child devices link to the alarm panel through via_device_id.
Only a single platform is loaded per case, whose entities never create the
panel device themselves, so the link is established solely by the panel
being registered at setup. Covers both YaleEntity and YaleLockEntity.
"""
config_entry, _ = load_config_entry
panel_device = device_registry.async_get_device_by_identifier(
(DOMAIN, ENTRY_CONFIG["username"]), config_entry.entry_id
)
assert panel_device is not None
child_device = device_registry.async_get_device_by_identifier(
(DOMAIN, child_identifier), config_entry.entry_id
)
assert child_device is not None
assert child_device.via_device_id == panel_device.id
async def test_migrate_entry(
hass: HomeAssistant,
get_client: Mock,