mirror of
https://github.com/home-assistant/core.git
synced 2026-08-08 14:25:42 +01:00
Fix via_device race in motion_blinds (#177735)
This commit is contained in:
@@ -4,11 +4,12 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from motionblinds import AsyncMotionMulticast
|
||||
from motionblinds import DEVICE_TYPES_GATEWAY, DEVICE_TYPES_WIFI, AsyncMotionMulticast
|
||||
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import (
|
||||
CONF_BLIND_TYPE_LIST,
|
||||
@@ -21,6 +22,7 @@ from .const import (
|
||||
PLATFORMS,
|
||||
)
|
||||
from .coordinator import DataUpdateCoordinatorMotionBlinds, MotionBlindsConfigEntry
|
||||
from .entity import gateway_device_info
|
||||
from .gateway import ConnectMotionGateway
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -101,6 +103,20 @@ async def async_setup_entry(
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
# Register the gateway device up front so child blinds can resolve it as their
|
||||
# via_device parent regardless of the order platforms are set up in. The any()
|
||||
# is the exact complement of the children's linking condition, so the gateway is
|
||||
# still registered if it self-reports an unexpected device_type while RF (non
|
||||
# Wi-Fi) blinds depend on it.
|
||||
if motion_gateway.device_type in DEVICE_TYPES_GATEWAY or any(
|
||||
blind.device_type not in DEVICE_TYPES_WIFI
|
||||
for blind in motion_gateway.device_list.values()
|
||||
):
|
||||
dr.async_get(hass).async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
**gateway_device_info(motion_gateway),
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
@@ -24,6 +24,23 @@ from .coordinator import DataUpdateCoordinatorMotionBlinds
|
||||
from .gateway import device_name
|
||||
|
||||
|
||||
def gateway_device_info(gateway: MotionGateway) -> DeviceInfo:
|
||||
"""Return the device info of a Motionblinds gateway."""
|
||||
if gateway.firmware is not None:
|
||||
sw_version = f"{gateway.firmware}, protocol: {gateway.protocol}"
|
||||
else:
|
||||
sw_version = f"Protocol: {gateway.protocol}"
|
||||
|
||||
return DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, gateway.mac)},
|
||||
identifiers={(DOMAIN, gateway.mac)},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=DEFAULT_GATEWAY_NAME,
|
||||
model="Wi-Fi bridge",
|
||||
sw_version=sw_version,
|
||||
)
|
||||
|
||||
|
||||
class MotionCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinatorMotionBlinds]):
|
||||
"""Representation of a Motionblind entity."""
|
||||
|
||||
@@ -50,42 +67,37 @@ class MotionCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinatorMotionBlind
|
||||
self._update_interval_moving = UPDATE_INTERVAL_MOVING
|
||||
|
||||
if blind.device_type in DEVICE_TYPES_GATEWAY:
|
||||
gateway = blind
|
||||
self._attr_device_info = gateway_device_info(blind)
|
||||
else:
|
||||
gateway = blind._gateway # noqa: SLF001
|
||||
if gateway.firmware is not None:
|
||||
sw_version = f"{gateway.firmware}, protocol: {gateway.protocol}"
|
||||
else:
|
||||
sw_version = f"Protocol: {gateway.protocol}"
|
||||
if gateway.firmware is not None:
|
||||
sw_version = f"{gateway.firmware}, protocol: {gateway.protocol}"
|
||||
else:
|
||||
sw_version = f"Protocol: {gateway.protocol}"
|
||||
|
||||
if blind.device_type in DEVICE_TYPES_GATEWAY:
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, blind.mac)},
|
||||
identifiers={(DOMAIN, blind.mac)},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=DEFAULT_GATEWAY_NAME,
|
||||
model="Wi-Fi bridge",
|
||||
sw_version=sw_version,
|
||||
)
|
||||
elif blind.device_type in DEVICE_TYPES_WIFI:
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, blind.mac)},
|
||||
identifiers={(DOMAIN, blind.mac)},
|
||||
manufacturer=MANUFACTURER,
|
||||
model=blind.blind_type,
|
||||
name=device_name(blind),
|
||||
sw_version=sw_version,
|
||||
hw_version=blind.wireless_name,
|
||||
)
|
||||
else:
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, blind.mac)},
|
||||
manufacturer=MANUFACTURER,
|
||||
model=blind.blind_type,
|
||||
name=device_name(blind),
|
||||
via_device=(DOMAIN, blind._gateway.mac), # noqa: SLF001
|
||||
hw_version=blind.wireless_name,
|
||||
)
|
||||
if blind.device_type in DEVICE_TYPES_WIFI:
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, blind.mac)},
|
||||
identifiers={(DOMAIN, blind.mac)},
|
||||
manufacturer=MANUFACTURER,
|
||||
model=blind.blind_type,
|
||||
name=device_name(blind),
|
||||
sw_version=sw_version,
|
||||
hw_version=blind.wireless_name,
|
||||
)
|
||||
else:
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, blind.mac)},
|
||||
manufacturer=MANUFACTURER,
|
||||
model=blind.blind_type,
|
||||
name=device_name(blind),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
coordinator.hass,
|
||||
(DOMAIN, gateway.mac),
|
||||
config_entry_id=coordinator.config_entry.entry_id,
|
||||
),
|
||||
hw_version=blind.wireless_name,
|
||||
)
|
||||
|
||||
@property
|
||||
@override
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Test the Motionblinds setup."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from motionblinds import DEVICE_TYPES_GATEWAY, DEVICE_TYPES_WIFI, BlindType
|
||||
from motionblinds.motion_blinds import DEVICE_TYPE_BLIND
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.motion_blinds.const import DEFAULT_INTERFACE, DOMAIN
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
TEST_HOST = "1.2.3.4"
|
||||
TEST_API_KEY = "12ab345c-d67e-8f"
|
||||
TEST_GATEWAY_MAC = "abcdefghijkl"
|
||||
TEST_BLIND_MAC = "abcdefghijkl0001"
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_gateway")
|
||||
def mock_gateway_fixture() -> Mock:
|
||||
"""Return a mocked gateway with a single sub-blind."""
|
||||
blind = Mock()
|
||||
blind.mac = TEST_BLIND_MAC
|
||||
blind.device_type = DEVICE_TYPE_BLIND
|
||||
blind.type = BlindType.RollerBlind
|
||||
blind.blind_type = BlindType.RollerBlind.name
|
||||
blind.wireless_name = "RF"
|
||||
blind.battery_voltage = 0
|
||||
blind.limit_status = "Limit2Detected"
|
||||
blind.position = 0
|
||||
blind.angle = 0
|
||||
blind.RSSI = -50
|
||||
|
||||
gateway = Mock()
|
||||
gateway.mac = TEST_GATEWAY_MAC
|
||||
gateway.device_type = DEVICE_TYPES_GATEWAY[0]
|
||||
gateway.firmware = "1.0.0"
|
||||
gateway.protocol = "1.0"
|
||||
gateway.device_list = {TEST_BLIND_MAC: blind}
|
||||
gateway.blind_type_list = {TEST_BLIND_MAC: BlindType.RollerBlind.value}
|
||||
|
||||
blind._gateway = gateway
|
||||
return gateway
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_connect", autouse=True)
|
||||
def mock_connect_fixture(mock_gateway: Mock) -> Generator[None]:
|
||||
"""Mock the connection to the Motion gateway."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.motion_blinds.AsyncMotionMulticast"
|
||||
) as multicast_class,
|
||||
patch(
|
||||
"homeassistant.components.motion_blinds.ConnectMotionGateway"
|
||||
) as connect_class,
|
||||
):
|
||||
multicast_class.return_value.Start_listen = AsyncMock()
|
||||
connect = connect_class.return_value
|
||||
connect.async_check_interface = AsyncMock(return_value=DEFAULT_INTERFACE)
|
||||
connect.async_connect_gateway = AsyncMock(return_value=True)
|
||||
connect.gateway_device = mock_gateway
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"gateway_device_type",
|
||||
[
|
||||
pytest.param(DEVICE_TYPES_GATEWAY[0], id="reported-gateway-type"),
|
||||
pytest.param(DEVICE_TYPES_WIFI[0], id="unexpected-non-gateway-type"),
|
||||
],
|
||||
)
|
||||
async def test_sub_blind_links_to_gateway_device(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_gateway: Mock,
|
||||
gateway_device_type: str,
|
||||
) -> None:
|
||||
"""Test that a sub-blind device links to the gateway device as its parent.
|
||||
|
||||
The gateway device must be registered up front even when the gateway
|
||||
self-reports a device_type outside DEVICE_TYPES_GATEWAY, so RF (non-Wi-Fi)
|
||||
blinds can still resolve it as their via_device parent.
|
||||
"""
|
||||
mock_gateway.device_type = gateway_device_type
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=TEST_GATEWAY_MAC,
|
||||
data={CONF_HOST: TEST_HOST, CONF_API_KEY: TEST_API_KEY},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
gateway_device = device_registry.async_get_device_by_identifier(
|
||||
(DOMAIN, TEST_GATEWAY_MAC), entry.entry_id
|
||||
)
|
||||
blind_device = device_registry.async_get_device_by_identifier(
|
||||
(DOMAIN, TEST_BLIND_MAC), entry.entry_id
|
||||
)
|
||||
|
||||
assert gateway_device is not None
|
||||
assert blind_device is not None
|
||||
assert blind_device.via_device_id == gateway_device.id
|
||||
Reference in New Issue
Block a user