From cbe14fc1dd7faa66d739aea33611a9cd8c340dd2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 1 Aug 2026 18:49:08 +0200 Subject: [PATCH] Fix via_device race in netgear (#177734) --- homeassistant/components/netgear/__init__.py | 6 ++ homeassistant/components/netgear/entity.py | 23 ++--- homeassistant/components/netgear/router.py | 19 +++++ tests/components/netgear/test_init.py | 89 ++++++++++++++++++++ 4 files changed, 120 insertions(+), 17 deletions(-) create mode 100644 tests/components/netgear/test_init.py diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index 2212644bce60..4f59f85b2372 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -74,6 +74,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: NetgearConfigEntry) -> b coordinator_link=coordinator_link, ) + # Register the router device before platforms so tracked devices can always + # resolve it as their via_device parent, regardless of platform setup order. + dr.async_get(hass).async_get_or_create( + config_entry_id=entry.entry_id, **router.device_info + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/netgear/entity.py b/homeassistant/components/netgear/entity.py index 40bbb9801681..c1d172521ce5 100644 --- a/homeassistant/components/netgear/entity.py +++ b/homeassistant/components/netgear/entity.py @@ -3,7 +3,6 @@ from abc import abstractmethod from typing import Any, override -from homeassistant.const import CONF_HOST from homeassistant.core import callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo @@ -37,7 +36,11 @@ class NetgearDeviceEntity(CoordinatorEntity[NetgearTrackerCoordinator]): connections={(dr.CONNECTION_NETWORK_MAC, self._mac)}, default_name=self._device_name, default_model=device["device_model"], - via_device=(DOMAIN, coordinator.router.unique_id), + via_device_id=dr.async_get_device_id_by_identifier( + coordinator.hass, + (DOMAIN, coordinator.router.unique_id), + config_entry_id=coordinator.config_entry.entry_id, + ), ) def get_device_name(self): @@ -69,22 +72,8 @@ class NetgearRouterEntity(Entity): def __init__(self, router: NetgearRouter) -> None: """Initialize a Netgear device.""" self._router = router - - configuration_url = None - if host := router.entry.data[CONF_HOST]: - configuration_url = f"http://{host}/" - self._attr_unique_id = router.serial_number - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, router.unique_id)}, - manufacturer="Netgear", - name=router.device_name, - model=router.model, - serial_number=router.serial_number, - sw_version=router.firmware_version, - hw_version=router.hardware_version, - configuration_url=configuration_url, - ) + self._attr_device_info = router.device_info class NetgearRouterCoordinatorEntity[T: NetgearDataCoordinator[Any]]( diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 1dc86b150e47..c46e0eb86ede 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -17,6 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.util import dt as dt_util from .const import ( @@ -270,6 +271,24 @@ class NetgearRouter: async with self.api_lock: await self.hass.async_add_executor_job(self.api.update_new_firmware) + @property + def device_info(self) -> DeviceInfo: + """Return the device information for the router.""" + configuration_url = None + if host := self.entry.data[CONF_HOST]: + configuration_url = f"http://{host}/" + + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + manufacturer="Netgear", + name=self.device_name, + model=self.model, + serial_number=self.serial_number, + sw_version=self.firmware_version, + hw_version=self.hardware_version, + configuration_url=configuration_url, + ) + @property def port(self) -> int: """Port used by the API.""" diff --git a/tests/components/netgear/test_init.py b/tests/components/netgear/test_init.py new file mode 100644 index 000000000000..5c11b0204666 --- /dev/null +++ b/tests/components/netgear/test_init.py @@ -0,0 +1,89 @@ +"""Tests for the Netgear integration setup.""" + +from unittest.mock import Mock, patch + +from pynetgear import Device + +from homeassistant.components.netgear.const import DOMAIN +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from tests.common import MockConfigEntry + +SERIAL = "5ER1AL0000001" +HOST = "10.0.0.1" + +ROUTER_INFOS = { + "DeviceMode": "0", + "ModelName": "RBR20", + "SerialNumber": SERIAL, + "Firmwareversion": "V2.3.5.26", + "Hardwareversion": "N/A", + "DeviceName": "Desk", +} + +TRACKED_DEVICE = Device( + name="Tracked-Device", + ip="10.0.0.10", + mac="AA:BB:CC:DD:EE:FF", + type="wireless", + signal=100, + link_rate=800, + allow_or_block="Allow", + device_type=32, + device_model="iPhone", + ssid="MyWifi", + conn_ap_mac="", +) + + +async def test_tracked_device_links_to_router( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: + """Test a tracked device is linked to the router via its via_device.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: HOST, + CONF_PORT: 80, + CONF_SSL: False, + CONF_USERNAME: "admin", + CONF_PASSWORD: "password", + }, + unique_id=SERIAL, + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.netgear.router.Netgear") as netgear_mock: + api = netgear_mock.return_value + api.login_try_port = Mock(return_value=True) + api.get_info = Mock(return_value=ROUTER_INFOS) + api.port = 80 + api.ssl = False + api.get_attached_devices_2 = Mock(return_value=[TRACKED_DEVICE]) + api.get_traffic_meter = Mock(return_value=None) + api.get_new_speed_test_result = Mock(return_value=None) + api.check_new_firmware = Mock(return_value=None) + api.get_system_info = Mock(return_value=None) + api.check_ethernet_link = Mock(return_value=None) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + router_device = device_registry.async_get_device_by_identifier( + (DOMAIN, SERIAL), entry.entry_id + ) + assert router_device is not None + + tracked_device = device_registry.async_get_device_by_connection( + (dr.CONNECTION_NETWORK_MAC, dr.format_mac(TRACKED_DEVICE.mac)), entry.entry_id + ) + assert tracked_device is not None + assert tracked_device.via_device_id == router_device.id