Fix via_device race in synology_dsm (#177769)

This commit is contained in:
Erik Montnemery
2026-08-01 16:53:59 +02:00
committed by GitHub
parent 0adfb98e81
commit ebc44c0c8a
10 changed files with 104 additions and 16 deletions
@@ -134,6 +134,39 @@ async def async_setup_entry(hass: HomeAssistant, entry: SynologyDSMConfigEntry)
coordinator_cameras=coordinator_cameras,
coordinator_switches=coordinator_switches,
)
# Register parent devices before forwarding platform setups so that child
# devices (storage/USB devices, surveillance station, cameras) can resolve
# their via_device_id regardless of platform setup order.
if TYPE_CHECKING:
assert api.information is not None
assert api.network is not None
central_device = dev_reg.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, api.information.serial)},
connections={(dr.CONNECTION_NETWORK_MAC, mac) for mac in api.network.macs},
name=api.network.hostname,
manufacturer="Synology",
model=api.information.model,
sw_version=api.information.version_string,
configuration_url=api.config_url,
)
if api.surveillance_station is not None:
dev_reg.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={
(
DOMAIN,
f"{api.information.serial}_{SynoSurveillanceStation.INFO_API_KEY}",
)
},
name=f"{api.network.hostname} Surveillance Station",
manufacturer="Synology",
model=api.information.model,
sw_version=coordinator_switches.version if coordinator_switches else None,
via_device_id=central_device.id,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
if entry.options[CONF_BACKUP_SHARE]:
@@ -16,6 +16,7 @@ from homeassistant.components.camera import (
CameraEntityFeature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -100,9 +101,13 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C
identifiers={(DOMAIN, f"{information.serial}_{self.camera_data.id}")},
name=self.camera_data.name,
model=self.camera_data.model,
via_device=(
DOMAIN,
f"{information.serial}_{SynoSurveillanceStation.INFO_API_KEY}",
via_device_id=dr.async_get_device_id_by_identifier(
self.hass,
(
DOMAIN,
f"{information.serial}_{SynoSurveillanceStation.INFO_API_KEY}",
),
config_entry_id=self.coordinator.config_entry.entry_id,
),
)
@@ -3,6 +3,7 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, override
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -158,6 +159,10 @@ class SynologyDSMDeviceEntity(
manufacturer=self._device_manufacturer,
model=self._device_model,
sw_version=self._device_firmware,
via_device=(DOMAIN, information.serial),
via_device_id=dr.async_get_device_id_by_identifier(
self.coordinator.hass,
(DOMAIN, information.serial),
config_entry_id=self.coordinator.config_entry.entry_id,
),
configuration_url=self._api.config_url,
)
@@ -8,6 +8,7 @@ from synology_dsm.api.surveillance_station import SynoSurveillanceStation
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -128,5 +129,9 @@ class SynoDSMSurveillanceHomeModeToggle(
manufacturer="Synology",
model=self._api.information.model,
sw_version=self._version,
via_device=(DOMAIN, self._api.information.serial),
via_device_id=dr.async_get_device_id_by_identifier(
self.hass,
(DOMAIN, self._api.information.serial),
config_entry_id=self.coordinator.config_entry.entry_id,
),
)
+5 -2
View File
@@ -1,7 +1,7 @@
"""Configure Synology DSM tests."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
from unittest.mock import AsyncMock, Mock, patch
import pytest
@@ -9,6 +9,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from .common import mock_dsm_hardware, mock_dsm_information
from .consts import HOST, MACS
@pytest.fixture
@@ -34,7 +35,9 @@ def fixture_dsm():
dsm.update = AsyncMock(return_value=True)
dsm.information = mock_dsm_information()
dsm.network.update = AsyncMock(return_value=True)
dsm.network = Mock(
update=AsyncMock(return_value=True), macs=MACS, hostname=HOST
)
dsm.hardware = mock_dsm_hardware()
dsm.surveillance_station.update = AsyncMock(return_value=True)
dsm.upgrade.update = AsyncMock(return_value=True)
+6 -2
View File
@@ -92,7 +92,9 @@ def mock_dsm_with_filestation():
dsm.surveillance_station.update = AsyncMock(return_value=True)
dsm.upgrade.update = AsyncMock(return_value=True)
dsm.utilisation = Mock(cpu_user_load=1, update=AsyncMock(return_value=True))
dsm.network = Mock(update=AsyncMock(return_value=True), macs=MACS)
dsm.network = Mock(
update=AsyncMock(return_value=True), macs=MACS, hostname=HOST
)
dsm.hardware = mock_dsm_hardware()
dsm.storage = Mock(
disks_ids=["sda", "sdb", "sdc"],
@@ -146,7 +148,9 @@ def mock_dsm_without_filestation():
dsm.surveillance_station.update = AsyncMock(return_value=True)
dsm.upgrade.update = AsyncMock(return_value=True)
dsm.utilisation = Mock(cpu_user_load=1, update=AsyncMock(return_value=True))
dsm.network = Mock(update=AsyncMock(return_value=True), macs=MACS)
dsm.network = Mock(
update=AsyncMock(return_value=True), macs=MACS, hostname=HOST
)
dsm.hardware = mock_dsm_hardware()
dsm.information = mock_dsm_information()
dsm.storage = Mock(
@@ -67,7 +67,9 @@ def mock_controller_service():
dsm.surveillance_station.update = AsyncMock(return_value=True)
dsm.upgrade.update = AsyncMock(return_value=True)
dsm.utilisation = Mock(cpu_user_load=1, update=AsyncMock(return_value=True))
dsm.network = Mock(update=AsyncMock(return_value=True), macs=MACS)
dsm.network = Mock(
update=AsyncMock(return_value=True), macs=MACS, hostname=HOST
)
dsm.hardware = mock_dsm_hardware()
dsm.storage = Mock(
disks_ids=["sda", "sdb", "sdc"],
@@ -91,7 +93,9 @@ def mock_controller_service_2sa():
dsm.surveillance_station.update = AsyncMock(return_value=True)
dsm.upgrade.update = AsyncMock(return_value=True)
dsm.utilisation = Mock(cpu_user_load=1, update=AsyncMock(return_value=True))
dsm.network = Mock(update=AsyncMock(return_value=True), macs=MACS)
dsm.network = Mock(
update=AsyncMock(return_value=True), macs=MACS, hostname=HOST
)
dsm.hardware = mock_dsm_hardware()
dsm.storage = Mock(
disks_ids=["sda", "sdb", "sdc"],
@@ -113,7 +117,9 @@ def mock_controller_service_vdsm():
dsm.surveillance_station.update = AsyncMock(return_value=True)
dsm.upgrade.update = AsyncMock(return_value=True)
dsm.utilisation = Mock(cpu_user_load=1, update=AsyncMock(return_value=True))
dsm.network = Mock(update=AsyncMock(return_value=True), macs=MACS)
dsm.network = Mock(
update=AsyncMock(return_value=True), macs=MACS, hostname=HOST
)
dsm.hardware = mock_dsm_hardware()
dsm.storage = Mock(
disks_ids=[],
@@ -135,7 +141,9 @@ def mock_controller_service_with_filestation():
dsm.surveillance_station.update = AsyncMock(return_value=True)
dsm.upgrade.update = AsyncMock(return_value=True)
dsm.utilisation = Mock(cpu_user_load=1, update=AsyncMock(return_value=True))
dsm.network = Mock(update=AsyncMock(return_value=True), macs=MACS)
dsm.network = Mock(
update=AsyncMock(return_value=True), macs=MACS, hostname=HOST
)
dsm.hardware = mock_dsm_hardware()
dsm.storage = Mock(
disks_ids=["sda", "sdb", "sdc"],
@@ -2,7 +2,7 @@
from pathlib import Path
import tempfile
from unittest.mock import AsyncMock, MagicMock, patch
from unittest.mock import AsyncMock, MagicMock, Mock, patch
from aiohttp import web
import pytest
@@ -41,7 +41,7 @@ def dsm_with_photos() -> MagicMock:
dsm.login = AsyncMock(return_value=True)
dsm.update = AsyncMock(return_value=True)
dsm.information = mock_dsm_information()
dsm.network.update = AsyncMock(return_value=True)
dsm.network = Mock(update=AsyncMock(return_value=True), macs=MACS, hostname=HOST)
dsm.hardware = mock_dsm_hardware()
dsm.surveillance_station.update = AsyncMock(return_value=True)
dsm.upgrade.update = AsyncMock(return_value=True)
@@ -41,7 +41,9 @@ def mock_dsm_with_filestation():
dsm.surveillance_station.update = AsyncMock(return_value=True)
dsm.upgrade.update = AsyncMock(return_value=True)
dsm.utilisation = Mock(cpu_user_load=1, update=AsyncMock(return_value=True))
dsm.network = Mock(update=AsyncMock(return_value=True), macs=MACS)
dsm.network = Mock(
update=AsyncMock(return_value=True), macs=MACS, hostname=HOST
)
dsm.hardware = mock_dsm_hardware()
dsm.storage = Mock(
disks_ids=["sda", "sdb", "sdc"],
@@ -406,3 +406,26 @@ async def test_hub_device_info_mac_connections(
("mac", "00:11:32:xx:xx:59"),
("mac", "00:11:32:xx:xx:5a"),
}
async def test_storage_device_via_device(
hass: HomeAssistant,
setup_dsm_with_usb: MagicMock,
) -> None:
"""Test that storage/USB child devices link to the hub via via_device_id."""
dev_reg = dr.async_get(hass) # pylint: disable=home-assistant-tests-registry-fixtures
entry_id = setup_dsm_with_usb.mock_entry.entry_id
hub_device = dev_reg.async_get_device_by_identifier((DOMAIN, SERIAL), entry_id)
assert hub_device is not None
volume_device = dev_reg.async_get_device_by_identifier(
(DOMAIN, f"{SERIAL}_volume_1"), entry_id
)
assert volume_device is not None
assert volume_device.via_device_id == hub_device.id
usb_partition_device = dev_reg.async_get_device_by_identifier(
(DOMAIN, f"{SERIAL}_USB Disk 1 Partition 1"), entry_id
)
assert usb_partition_device is not None
assert usb_partition_device.via_device_id == hub_device.id