mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Fix via_device race in lyric (#177935)
This commit is contained in:
committed by
Bram Kragten
parent
1622d8c771
commit
7eaae601bd
@@ -9,6 +9,7 @@ from homeassistant.helpers import (
|
||||
aiohttp_client,
|
||||
config_entry_oauth2_flow,
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
)
|
||||
|
||||
from .api import (
|
||||
@@ -18,6 +19,7 @@ from .api import (
|
||||
)
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LyricConfigEntry, LyricDataUpdateCoordinator
|
||||
from .entity import create_thermostat_device_info
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
@@ -59,6 +61,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: LyricConfigEntry) -> boo
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
# Register the thermostat devices up front so the accessory (room sensor)
|
||||
# entities can resolve them as their via_device parent regardless of the
|
||||
# order the platforms are set up in.
|
||||
device_registry = dr.async_get(hass)
|
||||
for location in coordinator.data.locations:
|
||||
for device in location.devices:
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
**create_thermostat_device_info(device),
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
@@ -50,6 +50,17 @@ class LyricEntity(CoordinatorEntity[LyricDataUpdateCoordinator]):
|
||||
return self.location.devices_dict[self._mac_id]
|
||||
|
||||
|
||||
def create_thermostat_device_info(device: LyricDevice) -> DeviceInfo:
|
||||
"""Return the device info for a Lyric thermostat."""
|
||||
return DeviceInfo(
|
||||
identifiers={(dr.CONNECTION_NETWORK_MAC, device.mac_id)},
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device.mac_id)},
|
||||
manufacturer="Honeywell",
|
||||
model=device.device_model,
|
||||
name=f"{device.name} Thermostat",
|
||||
)
|
||||
|
||||
|
||||
class LyricDeviceEntity(LyricEntity):
|
||||
"""Defines a Honeywell Lyric device entity."""
|
||||
|
||||
@@ -57,13 +68,7 @@ class LyricDeviceEntity(LyricEntity):
|
||||
@override
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device information about this Honeywell Lyric instance."""
|
||||
return DeviceInfo(
|
||||
identifiers={(dr.CONNECTION_NETWORK_MAC, self._mac_id)},
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, self._mac_id)},
|
||||
manufacturer="Honeywell",
|
||||
model=self.device.device_model,
|
||||
name=f"{self.device.name} Thermostat",
|
||||
)
|
||||
return create_thermostat_device_info(self.device)
|
||||
|
||||
|
||||
class LyricAccessoryEntity(LyricDeviceEntity):
|
||||
@@ -97,7 +102,11 @@ class LyricAccessoryEntity(LyricDeviceEntity):
|
||||
manufacturer="Honeywell",
|
||||
model="RCHTSENSOR",
|
||||
name=f"{self.room.room_name} Sensor",
|
||||
via_device=(dr.CONNECTION_NETWORK_MAC, self._mac_id),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
self.hass,
|
||||
(dr.CONNECTION_NETWORK_MAC, self._mac_id),
|
||||
config_entry_id=self.coordinator.config_entry.entry_id,
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
"""Tests for the Honeywell Lyric sensor platform."""
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from aiolyric import Lyric
|
||||
from aiolyric.objects.location import LyricLocation
|
||||
from aiolyric.objects.priority import LyricRoom
|
||||
|
||||
from homeassistant.components.lyric.api import LyricLocalOAuth2Implementation
|
||||
from homeassistant.components.lyric.const import DOMAIN
|
||||
from homeassistant.components.lyric.sensor import get_datetime_from_future_time
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
_MAC = "AABBCCDDEEFF"
|
||||
|
||||
|
||||
def test_get_datetime_from_future_time_none() -> None:
|
||||
@@ -19,3 +34,92 @@ def test_get_datetime_from_future_time_valid() -> None:
|
||||
"""Test that a valid time string returns a datetime."""
|
||||
result = get_datetime_from_future_time("13:30:00")
|
||||
assert isinstance(result, datetime)
|
||||
|
||||
|
||||
def _mock_lyric() -> MagicMock:
|
||||
"""Build a fake aiolyric Lyric client with one thermostat and one room accessory."""
|
||||
client = MagicMock()
|
||||
location = LyricLocation(
|
||||
client,
|
||||
{
|
||||
"locationID": 1234,
|
||||
"name": "Home",
|
||||
"devices": [
|
||||
# A thermostat with no device-level sensors: the sensor platform
|
||||
# creates no thermostat entity, so the thermostat device can only
|
||||
# come from the up-front registration in async_setup_entry.
|
||||
{
|
||||
"deviceID": f"LCC-{_MAC}",
|
||||
"deviceClass": "Thermostat",
|
||||
"macID": _MAC,
|
||||
"name": "Thermostat",
|
||||
"deviceModel": "T5-T6",
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
room = LyricRoom(
|
||||
{
|
||||
"id": 1,
|
||||
"roomName": "Living Room",
|
||||
"roomAvgTemp": 21,
|
||||
"roomAvgHumidity": 40,
|
||||
"accessories": [{"id": 1, "type": "IndoorAirSensor", "temperature": 21}],
|
||||
}
|
||||
)
|
||||
|
||||
lyric = MagicMock(spec=Lyric)
|
||||
lyric.get_locations = AsyncMock()
|
||||
lyric.get_thermostat_rooms = AsyncMock()
|
||||
lyric.locations = [location]
|
||||
lyric.locations_dict = {1234: location}
|
||||
lyric.rooms_dict = {_MAC: {1: room}}
|
||||
return lyric
|
||||
|
||||
|
||||
async def test_accessory_links_to_thermostat_via_device(
|
||||
hass: HomeAssistant, device_registry: dr.DeviceRegistry
|
||||
) -> None:
|
||||
"""Test room accessory devices resolve the thermostat as their via_device."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"auth_implementation": DOMAIN,
|
||||
"token": {
|
||||
"access_token": "mock-access-token",
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"expires_at": 9999999999,
|
||||
"token_type": "Bearer",
|
||||
},
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
implementation = MagicMock(spec=LyricLocalOAuth2Implementation)
|
||||
implementation.client_id = "client-id"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow."
|
||||
"async_get_config_entry_implementation",
|
||||
return_value=implementation,
|
||||
),
|
||||
patch("homeassistant.components.lyric.PLATFORMS", [Platform.SENSOR]),
|
||||
patch("homeassistant.components.lyric.Lyric", return_value=_mock_lyric()),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
thermostat = device_registry.async_get_device_by_identifier(
|
||||
(dr.CONNECTION_NETWORK_MAC, _MAC), entry.entry_id
|
||||
)
|
||||
assert thermostat is not None
|
||||
|
||||
accessory = device_registry.async_get_device_by_identifier(
|
||||
(f"{dr.CONNECTION_NETWORK_MAC}_room_accessory", f"{_MAC}_room1_accessory1"),
|
||||
entry.entry_id,
|
||||
)
|
||||
assert accessory is not None
|
||||
assert accessory.via_device_id == thermostat.id
|
||||
|
||||
Reference in New Issue
Block a user