Update more nest tests to use common fixtures (#73303)

Update nest tests to use fixtures
This commit is contained in:
Allen Porter 2022-06-09 22:14:43 -07:00 committed by GitHub
parent 15621bee3f
commit 7a5fa8eb58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 357 additions and 447 deletions

View File

@ -5,7 +5,6 @@ import copy
from dataclasses import dataclass from dataclasses import dataclass
import time import time
from typing import Any, Generator, TypeVar from typing import Any, Generator, TypeVar
from unittest.mock import patch
from google_nest_sdm.auth import AbstractAuth from google_nest_sdm.auth import AbstractAuth
from google_nest_sdm.device import Device from google_nest_sdm.device import Device
@ -16,7 +15,6 @@ from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber
from homeassistant.components.nest import DOMAIN from homeassistant.components.nest import DOMAIN
from homeassistant.components.nest.const import SDM_SCOPES from homeassistant.components.nest.const import SDM_SCOPES
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -198,29 +196,3 @@ class CreateDevice:
data.update(raw_data if raw_data else {}) data.update(raw_data if raw_data else {})
data["traits"].update(raw_traits if raw_traits else {}) data["traits"].update(raw_traits if raw_traits else {})
self.device_manager.add_device(Device.MakeDevice(data, auth=self.auth)) self.device_manager.add_device(Device.MakeDevice(data, auth=self.auth))
async def async_setup_sdm_platform(
hass,
platform,
devices={},
):
"""Set up the platform and prerequisites."""
create_config_entry().add_to_hass(hass)
subscriber = FakeSubscriber()
device_manager = await subscriber.async_get_device_manager()
if devices:
for device in devices.values():
device_manager.add_device(device)
platforms = []
if platform:
platforms = [platform]
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation"
), patch("homeassistant.components.nest.PLATFORMS", platforms), patch(
"homeassistant.components.nest.api.GoogleNestSubscriber",
return_value=subscriber,
):
assert await async_setup_component(hass, DOMAIN, CONFIG)
await hass.async_block_till_done()
return subscriber

View File

@ -1,5 +1,4 @@
"""The tests for Nest device triggers.""" """The tests for Nest device triggers."""
from google_nest_sdm.device import Device
from google_nest_sdm.event import EventMessage from google_nest_sdm.event import EventMessage
import pytest import pytest
@ -10,11 +9,12 @@ from homeassistant.components.device_automation.exceptions import (
) )
from homeassistant.components.nest import DOMAIN from homeassistant.components.nest import DOMAIN
from homeassistant.components.nest.events import NEST_EVENT from homeassistant.components.nest.events import NEST_EVENT
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .common import async_setup_sdm_platform from .common import DEVICE_ID, CreateDevice, FakeSubscriber, PlatformSetup
from tests.common import ( from tests.common import (
assert_lists_same, assert_lists_same,
@ -22,11 +22,16 @@ from tests.common import (
async_mock_service, async_mock_service,
) )
DEVICE_ID = "some-device-id"
DEVICE_NAME = "My Camera" DEVICE_NAME = "My Camera"
DATA_MESSAGE = {"message": "service-called"} DATA_MESSAGE = {"message": "service-called"}
@pytest.fixture
def platforms() -> list[str]:
"""Fixture to setup the platforms to test."""
return ["camera"]
def make_camera(device_id, name=DEVICE_NAME, traits={}): def make_camera(device_id, name=DEVICE_NAME, traits={}):
"""Create a nest camera.""" """Create a nest camera."""
traits = traits.copy() traits = traits.copy()
@ -45,21 +50,11 @@ def make_camera(device_id, name=DEVICE_NAME, traits={}):
}, },
} }
) )
return Device.MakeDevice( return {
{
"name": device_id, "name": device_id,
"type": "sdm.devices.types.CAMERA", "type": "sdm.devices.types.CAMERA",
"traits": traits, "traits": traits,
}, }
auth=None,
)
async def async_setup_camera(hass, devices=None):
"""Set up the platform and prerequisites for testing available triggers."""
if not devices:
devices = {DEVICE_ID: make_camera(device_id=DEVICE_ID)}
return await async_setup_sdm_platform(hass, "camera", devices)
async def setup_automation(hass, device_id, trigger_type): async def setup_automation(hass, device_id, trigger_type):
@ -92,16 +87,20 @@ def calls(hass):
return async_mock_service(hass, "test", "automation") return async_mock_service(hass, "test", "automation")
async def test_get_triggers(hass): async def test_get_triggers(
hass: HomeAssistant, create_device: CreateDevice, setup_platform: PlatformSetup
) -> None:
"""Test we get the expected triggers from a nest.""" """Test we get the expected triggers from a nest."""
camera = make_camera( create_device.create(
raw_data=make_camera(
device_id=DEVICE_ID, device_id=DEVICE_ID,
traits={ traits={
"sdm.devices.traits.CameraMotion": {}, "sdm.devices.traits.CameraMotion": {},
"sdm.devices.traits.CameraPerson": {}, "sdm.devices.traits.CameraPerson": {},
}, },
) )
await async_setup_camera(hass, {DEVICE_ID: camera}) )
await setup_platform()
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device_entry = device_registry.async_get_device({("nest", DEVICE_ID)}) device_entry = device_registry.async_get_device({("nest", DEVICE_ID)})
@ -128,23 +127,29 @@ async def test_get_triggers(hass):
assert_lists_same(triggers, expected_triggers) assert_lists_same(triggers, expected_triggers)
async def test_multiple_devices(hass): async def test_multiple_devices(
hass: HomeAssistant, create_device: CreateDevice, setup_platform: PlatformSetup
) -> None:
"""Test we get the expected triggers from a nest.""" """Test we get the expected triggers from a nest."""
camera1 = make_camera( create_device.create(
raw_data=make_camera(
device_id="device-id-1", device_id="device-id-1",
name="Camera 1", name="Camera 1",
traits={ traits={
"sdm.devices.traits.CameraSound": {}, "sdm.devices.traits.CameraSound": {},
}, },
) )
camera2 = make_camera( )
create_device.create(
raw_data=make_camera(
device_id="device-id-2", device_id="device-id-2",
name="Camera 2", name="Camera 2",
traits={ traits={
"sdm.devices.traits.DoorbellChime": {}, "sdm.devices.traits.DoorbellChime": {},
}, },
) )
await async_setup_camera(hass, {"device-id-1": camera1, "device-id-2": camera2}) )
await setup_platform()
registry = er.async_get(hass) registry = er.async_get(hass)
entry1 = registry.async_get("camera.camera_1") entry1 = registry.async_get("camera.camera_1")
@ -177,16 +182,20 @@ async def test_multiple_devices(hass):
} }
async def test_triggers_for_invalid_device_id(hass): async def test_triggers_for_invalid_device_id(
hass: HomeAssistant, create_device: CreateDevice, setup_platform: PlatformSetup
) -> None:
"""Get triggers for a device not found in the API.""" """Get triggers for a device not found in the API."""
camera = make_camera( create_device.create(
raw_data=make_camera(
device_id=DEVICE_ID, device_id=DEVICE_ID,
traits={ traits={
"sdm.devices.traits.CameraMotion": {}, "sdm.devices.traits.CameraMotion": {},
"sdm.devices.traits.CameraPerson": {}, "sdm.devices.traits.CameraPerson": {},
}, },
) )
await async_setup_camera(hass, {DEVICE_ID: camera}) )
await setup_platform()
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device_entry = device_registry.async_get_device({("nest", DEVICE_ID)}) device_entry = device_registry.async_get_device({("nest", DEVICE_ID)})
@ -207,14 +216,16 @@ async def test_triggers_for_invalid_device_id(hass):
) )
async def test_no_triggers(hass): async def test_no_triggers(
hass: HomeAssistant, create_device: CreateDevice, setup_platform: PlatformSetup
) -> None:
"""Test we get the expected triggers from a nest.""" """Test we get the expected triggers from a nest."""
camera = make_camera(device_id=DEVICE_ID, traits={}) create_device.create(raw_data=make_camera(device_id=DEVICE_ID, traits={}))
await async_setup_camera(hass, {DEVICE_ID: camera}) await setup_platform()
registry = er.async_get(hass) registry = er.async_get(hass)
entry = registry.async_get("camera.my_camera") entry = registry.async_get("camera.my_camera")
assert entry.unique_id == "some-device-id-camera" assert entry.unique_id == f"{DEVICE_ID}-camera"
triggers = await async_get_device_automations( triggers = await async_get_device_automations(
hass, DeviceAutomationType.TRIGGER, entry.device_id hass, DeviceAutomationType.TRIGGER, entry.device_id
@ -294,15 +305,23 @@ async def test_trigger_for_wrong_event_type(hass, calls):
assert len(calls) == 0 assert len(calls) == 0
async def test_subscriber_automation(hass, calls): async def test_subscriber_automation(
hass: HomeAssistant,
calls: list,
create_device: CreateDevice,
setup_platform: PlatformSetup,
subscriber: FakeSubscriber,
) -> None:
"""Test end to end subscriber triggers automation.""" """Test end to end subscriber triggers automation."""
camera = make_camera( create_device.create(
raw_data=make_camera(
device_id=DEVICE_ID, device_id=DEVICE_ID,
traits={ traits={
"sdm.devices.traits.CameraMotion": {}, "sdm.devices.traits.CameraMotion": {},
}, },
) )
subscriber = await async_setup_camera(hass, {DEVICE_ID: camera}) )
await setup_platform()
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device_entry = device_registry.async_get_device({("nest", DEVICE_ID)}) device_entry = device_registry.async_get_device({("nest", DEVICE_ID)})

View File

@ -13,11 +13,12 @@ from unittest.mock import patch
from google_nest_sdm.device import Device from google_nest_sdm.device import Device
from google_nest_sdm.event import EventMessage from google_nest_sdm.event import EventMessage
import pytest
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .common import async_setup_sdm_platform from .common import CreateDevice
from tests.common import async_capture_events from tests.common import async_capture_events
@ -31,26 +32,43 @@ EVENT_ID = "FWWVQVUdGNUlTU2V4MGV2aTNXV..."
EVENT_KEYS = {"device_id", "type", "timestamp", "zones"} EVENT_KEYS = {"device_id", "type", "timestamp", "zones"}
@pytest.fixture
def platforms() -> list[str]:
"""Fixture for platforms to setup."""
return [PLATFORM]
@pytest.fixture
def device_type() -> str:
"""Fixture for the type of device under test."""
return "sdm.devices.types.DOORBELL"
@pytest.fixture
def device_traits() -> list[str]:
"""Fixture for the present traits of the device under test."""
return ["sdm.devices.traits.DoorbellChime"]
@pytest.fixture(autouse=True)
def device(
device_type: str, device_traits: dict[str, Any], create_device: CreateDevice
) -> None:
"""Fixture to create a device under test."""
return create_device.create(
raw_data={
"name": DEVICE_ID,
"type": device_type,
"traits": create_device_traits(device_traits),
}
)
def event_view(d: Mapping[str, Any]) -> Mapping[str, Any]: def event_view(d: Mapping[str, Any]) -> Mapping[str, Any]:
"""View of an event with relevant keys for testing.""" """View of an event with relevant keys for testing."""
return {key: value for key, value in d.items() if key in EVENT_KEYS} return {key: value for key, value in d.items() if key in EVENT_KEYS}
async def async_setup_devices(hass, device_type, traits={}, auth=None):
"""Set up the platform and prerequisites."""
devices = {
DEVICE_ID: Device.MakeDevice(
{
"name": DEVICE_ID,
"type": device_type,
"traits": traits,
},
auth=auth,
),
}
return await async_setup_sdm_platform(hass, PLATFORM, devices=devices)
def create_device_traits(event_traits=[]): def create_device_traits(event_traits=[]):
"""Create fake traits for a device.""" """Create fake traits for a device."""
result = { result = {
@ -98,15 +116,45 @@ def create_events(events, device_id=DEVICE_ID, timestamp=None):
) )
async def test_doorbell_chime_event(hass, auth): @pytest.mark.parametrize(
"device_type,device_traits,event_trait,expected_model,expected_type",
[
(
"sdm.devices.types.DOORBELL",
["sdm.devices.traits.DoorbellChime"],
"sdm.devices.events.DoorbellChime.Chime",
"Doorbell",
"doorbell_chime",
),
(
"sdm.devices.types.CAMERA",
["sdm.devices.traits.CameraMotion"],
"sdm.devices.events.CameraMotion.Motion",
"Camera",
"camera_motion",
),
(
"sdm.devices.types.CAMERA",
["sdm.devices.traits.CameraPerson"],
"sdm.devices.events.CameraPerson.Person",
"Camera",
"camera_person",
),
(
"sdm.devices.types.CAMERA",
["sdm.devices.traits.CameraSound"],
"sdm.devices.events.CameraSound.Sound",
"Camera",
"camera_sound",
),
],
)
async def test_event(
hass, auth, setup_platform, subscriber, event_trait, expected_model, expected_type
):
"""Test a pubsub message for a doorbell event.""" """Test a pubsub message for a doorbell event."""
events = async_capture_events(hass, NEST_EVENT) events = async_capture_events(hass, NEST_EVENT)
subscriber = await async_setup_devices( await setup_platform()
hass,
"sdm.devices.types.DOORBELL",
create_device_traits(["sdm.devices.traits.DoorbellChime"]),
auth,
)
registry = er.async_get(hass) registry = er.async_get(hass)
entry = registry.async_get("camera.front") entry = registry.async_get("camera.front")
@ -118,115 +166,32 @@ async def test_doorbell_chime_event(hass, auth):
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id) device = device_registry.async_get(entry.device_id)
assert device.name == "Front" assert device.name == "Front"
assert device.model == "Doorbell" assert device.model == expected_model
assert device.identifiers == {("nest", DEVICE_ID)} assert device.identifiers == {("nest", DEVICE_ID)}
timestamp = utcnow() timestamp = utcnow()
await subscriber.async_receive_event( await subscriber.async_receive_event(create_event(event_trait, timestamp=timestamp))
create_event("sdm.devices.events.DoorbellChime.Chime", timestamp=timestamp)
)
await hass.async_block_till_done() await hass.async_block_till_done()
event_time = timestamp.replace(microsecond=0) event_time = timestamp.replace(microsecond=0)
assert len(events) == 1 assert len(events) == 1
assert event_view(events[0].data) == { assert event_view(events[0].data) == {
"device_id": entry.device_id, "device_id": entry.device_id,
"type": "doorbell_chime", "type": expected_type,
"timestamp": event_time, "timestamp": event_time,
} }
async def test_camera_motion_event(hass): @pytest.mark.parametrize(
"""Test a pubsub message for a camera motion event.""" "device_traits",
events = async_capture_events(hass, NEST_EVENT) [
subscriber = await async_setup_devices( ["sdm.devices.traits.CameraMotion", "sdm.devices.traits.CameraPerson"],
hass, ],
"sdm.devices.types.CAMERA",
create_device_traits(["sdm.devices.traits.CameraMotion"]),
) )
registry = er.async_get(hass) async def test_camera_multiple_event(hass, subscriber, setup_platform):
entry = registry.async_get("camera.front")
assert entry is not None
timestamp = utcnow()
await subscriber.async_receive_event(
create_event("sdm.devices.events.CameraMotion.Motion", timestamp=timestamp)
)
await hass.async_block_till_done()
event_time = timestamp.replace(microsecond=0)
assert len(events) == 1
assert event_view(events[0].data) == {
"device_id": entry.device_id,
"type": "camera_motion",
"timestamp": event_time,
}
async def test_camera_sound_event(hass):
"""Test a pubsub message for a camera sound event."""
events = async_capture_events(hass, NEST_EVENT)
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.CAMERA",
create_device_traits(["sdm.devices.traits.CameraSound"]),
)
registry = er.async_get(hass)
entry = registry.async_get("camera.front")
assert entry is not None
timestamp = utcnow()
await subscriber.async_receive_event(
create_event("sdm.devices.events.CameraSound.Sound", timestamp=timestamp)
)
await hass.async_block_till_done()
event_time = timestamp.replace(microsecond=0)
assert len(events) == 1
assert event_view(events[0].data) == {
"device_id": entry.device_id,
"type": "camera_sound",
"timestamp": event_time,
}
async def test_camera_person_event(hass):
"""Test a pubsub message for a camera person event.""" """Test a pubsub message for a camera person event."""
events = async_capture_events(hass, NEST_EVENT) events = async_capture_events(hass, NEST_EVENT)
subscriber = await async_setup_devices( await setup_platform()
hass,
"sdm.devices.types.DOORBELL",
create_device_traits(["sdm.devices.traits.CameraPerson"]),
)
registry = er.async_get(hass)
entry = registry.async_get("camera.front")
assert entry is not None
timestamp = utcnow()
await subscriber.async_receive_event(
create_event("sdm.devices.events.CameraPerson.Person", timestamp=timestamp)
)
await hass.async_block_till_done()
event_time = timestamp.replace(microsecond=0)
assert len(events) == 1
assert event_view(events[0].data) == {
"device_id": entry.device_id,
"type": "camera_person",
"timestamp": event_time,
}
async def test_camera_multiple_event(hass):
"""Test a pubsub message for a camera person event."""
events = async_capture_events(hass, NEST_EVENT)
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.DOORBELL",
create_device_traits(
["sdm.devices.traits.CameraMotion", "sdm.devices.traits.CameraPerson"]
),
)
registry = er.async_get(hass) registry = er.async_get(hass)
entry = registry.async_get("camera.front") entry = registry.async_get("camera.front")
assert entry is not None assert entry is not None
@ -260,28 +225,20 @@ async def test_camera_multiple_event(hass):
} }
async def test_unknown_event(hass): async def test_unknown_event(hass, subscriber, setup_platform):
"""Test a pubsub message for an unknown event type.""" """Test a pubsub message for an unknown event type."""
events = async_capture_events(hass, NEST_EVENT) events = async_capture_events(hass, NEST_EVENT)
subscriber = await async_setup_devices( await setup_platform()
hass,
"sdm.devices.types.DOORBELL",
create_device_traits(["sdm.devices.traits.DoorbellChime"]),
)
await subscriber.async_receive_event(create_event("some-event-id")) await subscriber.async_receive_event(create_event("some-event-id"))
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(events) == 0 assert len(events) == 0
async def test_unknown_device_id(hass): async def test_unknown_device_id(hass, subscriber, setup_platform):
"""Test a pubsub message for an unknown event type.""" """Test a pubsub message for an unknown event type."""
events = async_capture_events(hass, NEST_EVENT) events = async_capture_events(hass, NEST_EVENT)
subscriber = await async_setup_devices( await setup_platform()
hass,
"sdm.devices.types.DOORBELL",
create_device_traits(["sdm.devices.traits.DoorbellChime"]),
)
await subscriber.async_receive_event( await subscriber.async_receive_event(
create_event("sdm.devices.events.DoorbellChime.Chime", "invalid-device-id") create_event("sdm.devices.events.DoorbellChime.Chime", "invalid-device-id")
) )
@ -290,14 +247,10 @@ async def test_unknown_device_id(hass):
assert len(events) == 0 assert len(events) == 0
async def test_event_message_without_device_event(hass): async def test_event_message_without_device_event(hass, subscriber, setup_platform):
"""Test a pubsub message for an unknown event type.""" """Test a pubsub message for an unknown event type."""
events = async_capture_events(hass, NEST_EVENT) events = async_capture_events(hass, NEST_EVENT)
subscriber = await async_setup_devices( await setup_platform()
hass,
"sdm.devices.types.DOORBELL",
create_device_traits(["sdm.devices.traits.DoorbellChime"]),
)
timestamp = utcnow() timestamp = utcnow()
event = EventMessage( event = EventMessage(
{ {
@ -312,20 +265,16 @@ async def test_event_message_without_device_event(hass):
assert len(events) == 0 assert len(events) == 0
async def test_doorbell_event_thread(hass, auth): @pytest.mark.parametrize(
"device_traits",
[
["sdm.devices.traits.CameraClipPreview", "sdm.devices.traits.CameraPerson"],
],
)
async def test_doorbell_event_thread(hass, subscriber, setup_platform):
"""Test a series of pubsub messages in the same thread.""" """Test a series of pubsub messages in the same thread."""
events = async_capture_events(hass, NEST_EVENT) events = async_capture_events(hass, NEST_EVENT)
subscriber = await async_setup_devices( await setup_platform()
hass,
"sdm.devices.types.DOORBELL",
create_device_traits(
[
"sdm.devices.traits.CameraClipPreview",
"sdm.devices.traits.CameraPerson",
]
),
auth,
)
registry = er.async_get(hass) registry = er.async_get(hass)
entry = registry.async_get("camera.front") entry = registry.async_get("camera.front")
assert entry is not None assert entry is not None
@ -381,21 +330,20 @@ async def test_doorbell_event_thread(hass, auth):
} }
async def test_doorbell_event_session_update(hass, auth): @pytest.mark.parametrize(
"""Test a pubsub message with updates to an existing session.""" "device_traits",
events = async_capture_events(hass, NEST_EVENT) [
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.DOORBELL",
create_device_traits(
[ [
"sdm.devices.traits.CameraClipPreview", "sdm.devices.traits.CameraClipPreview",
"sdm.devices.traits.CameraPerson", "sdm.devices.traits.CameraPerson",
"sdm.devices.traits.CameraMotion", "sdm.devices.traits.CameraMotion",
] ],
), ],
auth,
) )
async def test_doorbell_event_session_update(hass, subscriber, setup_platform):
"""Test a pubsub message with updates to an existing session."""
events = async_capture_events(hass, NEST_EVENT)
await setup_platform()
registry = er.async_get(hass) registry = er.async_get(hass)
entry = registry.async_get("camera.front") entry = registry.async_get("camera.front")
assert entry is not None assert entry is not None
@ -454,14 +402,10 @@ async def test_doorbell_event_session_update(hass, auth):
} }
async def test_structure_update_event(hass): async def test_structure_update_event(hass, subscriber, setup_platform):
"""Test a pubsub message for a new device being added.""" """Test a pubsub message for a new device being added."""
events = async_capture_events(hass, NEST_EVENT) events = async_capture_events(hass, NEST_EVENT)
subscriber = await async_setup_devices( await setup_platform()
hass,
"sdm.devices.types.DOORBELL",
create_device_traits(["sdm.devices.traits.DoorbellChime"]),
)
# Entity for first device is registered # Entity for first device is registered
registry = er.async_get(hass) registry = er.async_get(hass)
@ -516,14 +460,16 @@ async def test_structure_update_event(hass):
assert not registry.async_get("camera.back") assert not registry.async_get("camera.back")
async def test_event_zones(hass): @pytest.mark.parametrize(
"device_traits",
[
["sdm.devices.traits.CameraMotion"],
],
)
async def test_event_zones(hass, subscriber, setup_platform):
"""Test events published with zone information.""" """Test events published with zone information."""
events = async_capture_events(hass, NEST_EVENT) events = async_capture_events(hass, NEST_EVENT)
subscriber = await async_setup_devices( await setup_platform()
hass,
"sdm.devices.types.DOORBELL",
create_device_traits(["sdm.devices.traits.CameraMotion"]),
)
registry = er.async_get(hass) registry = er.async_get(hass)
entry = registry.async_get("camera.front") entry = registry.async_get("camera.front")
assert entry is not None assert entry is not None

View File

@ -8,11 +8,11 @@ from collections.abc import Generator
import datetime import datetime
from http import HTTPStatus from http import HTTPStatus
import io import io
from typing import Any
from unittest.mock import patch from unittest.mock import patch
import aiohttp import aiohttp
import av import av
from google_nest_sdm.device import Device
from google_nest_sdm.event import EventMessage from google_nest_sdm.event import EventMessage
import numpy as np import numpy as np
import pytest import pytest
@ -27,17 +27,11 @@ from homeassistant.helpers.template import DATE_STR_FORMAT
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from .common import ( from .common import DEVICE_ID, CreateDevice, FakeSubscriber
CONFIG,
FakeSubscriber,
async_setup_sdm_platform,
create_config_entry,
)
from tests.common import async_capture_events from tests.common import async_capture_events
DOMAIN = "nest" DOMAIN = "nest"
DEVICE_ID = "example/api/device/id"
DEVICE_NAME = "Front" DEVICE_NAME = "Front"
PLATFORM = "camera" PLATFORM = "camera"
NEST_EVENT = "nest_event" NEST_EVENT = "nest_event"
@ -90,10 +84,42 @@ def frame_image_data(frame_i, total_frames):
return img return img
@pytest.fixture
def platforms() -> list[str]:
"""Fixture for platforms to setup."""
return [PLATFORM]
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
async def setup_media_source(hass) -> None: async def setup_components(hass) -> None:
"""Set up media source.""" """Fixture to initialize the integration."""
assert await async_setup_component(hass, "media_source", {}) await async_setup_component(hass, "media_source", {})
@pytest.fixture
def device_type() -> str:
"""Fixture for the type of device under test."""
return CAMERA_DEVICE_TYPE
@pytest.fixture
def device_traits() -> dict[str, Any]:
"""Fixture for the present traits of the device under test."""
return CAMERA_TRAITS
@pytest.fixture(autouse=True)
def device(
device_type: str, device_traits: dict[str, Any], create_device: CreateDevice
) -> None:
"""Fixture to create a device under test."""
return create_device.create(
raw_data={
"name": DEVICE_ID,
"type": device_type,
"traits": device_traits,
}
)
@pytest.fixture @pytest.fixture
@ -128,22 +154,23 @@ def mp4() -> io.BytesIO:
return output return output
async def async_setup_devices(hass, auth, device_type, traits={}, events=[]): @pytest.fixture(autouse=True)
"""Set up the platform and prerequisites.""" def enable_prefetch(subscriber: FakeSubscriber) -> None:
devices = { """Fixture to enable media fetching for tests to exercise."""
DEVICE_ID: Device.MakeDevice(
{
"name": DEVICE_ID,
"type": device_type,
"traits": traits,
},
auth=auth,
),
}
subscriber = await async_setup_sdm_platform(hass, PLATFORM, devices=devices)
# Enable feature for fetching media
subscriber.cache_policy.fetch = True subscriber.cache_policy.fetch = True
return subscriber
@pytest.fixture
def cache_size() -> int:
"""Fixture for overrideing cache size."""
return 100
@pytest.fixture(autouse=True)
def apply_cache_size(cache_size):
"""Fixture for patching the cache size."""
with patch("homeassistant.components.nest.EVENT_MEDIA_CACHE_SIZE", new=cache_size):
yield
def create_event( def create_event(
@ -194,17 +221,20 @@ def create_battery_event_data(
} }
async def test_no_eligible_devices(hass, auth): @pytest.mark.parametrize(
"""Test a media source with no eligible camera devices.""" "device_type,device_traits",
await async_setup_devices( [
hass, (
auth,
"sdm.devices.types.THERMOSTAT", "sdm.devices.types.THERMOSTAT",
{ {
"sdm.devices.traits.Temperature": {}, "sdm.devices.traits.Temperature": {},
}, },
) )
],
)
async def test_no_eligible_devices(hass, setup_platform):
"""Test a media source with no eligible camera devices."""
await setup_platform()
browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}")
assert browse.domain == DOMAIN assert browse.domain == DOMAIN
assert browse.identifier == "" assert browse.identifier == ""
@ -212,10 +242,10 @@ async def test_no_eligible_devices(hass, auth):
assert not browse.children assert not browse.children
@pytest.mark.parametrize("traits", [CAMERA_TRAITS, BATTERY_CAMERA_TRAITS]) @pytest.mark.parametrize("device_traits", [CAMERA_TRAITS, BATTERY_CAMERA_TRAITS])
async def test_supported_device(hass, auth, traits): async def test_supported_device(hass, setup_platform):
"""Test a media source with a supported camera.""" """Test a media source with a supported camera."""
await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, traits) await setup_platform()
assert len(hass.states.async_all()) == 1 assert len(hass.states.async_all()) == 1
camera = hass.states.get("camera.front") camera = hass.states.get("camera.front")
@ -245,14 +275,9 @@ async def test_supported_device(hass, auth, traits):
assert len(browse.children) == 0 assert len(browse.children) == 0
async def test_integration_unloaded(hass, auth): async def test_integration_unloaded(hass, auth, setup_platform):
"""Test the media player loads, but has no devices, when config unloaded.""" """Test the media player loads, but has no devices, when config unloaded."""
await async_setup_devices( await setup_platform()
hass,
auth,
CAMERA_DEVICE_TYPE,
CAMERA_TRAITS,
)
browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}")
assert browse.domain == DOMAIN assert browse.domain == DOMAIN
@ -276,11 +301,9 @@ async def test_integration_unloaded(hass, auth):
assert len(browse.children) == 0 assert len(browse.children) == 0
async def test_camera_event(hass, auth, hass_client): async def test_camera_event(hass, hass_client, subscriber, auth, setup_platform):
"""Test a media source and image created for an event.""" """Test a media source and image created for an event."""
subscriber = await async_setup_devices( await setup_platform()
hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS
)
assert len(hass.states.async_all()) == 1 assert len(hass.states.async_all()) == 1
camera = hass.states.get("camera.front") camera = hass.states.get("camera.front")
@ -380,11 +403,9 @@ async def test_camera_event(hass, auth, hass_client):
assert media.mime_type == "image/jpeg" assert media.mime_type == "image/jpeg"
async def test_event_order(hass, auth): async def test_event_order(hass, auth, subscriber, setup_platform):
"""Test multiple events are in descending timestamp order.""" """Test multiple events are in descending timestamp order."""
subscriber = await async_setup_devices( await setup_platform()
hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS
)
auth.responses = [ auth.responses = [
aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE),
@ -449,14 +470,15 @@ async def test_event_order(hass, auth):
assert not browse.children[1].can_play assert not browse.children[1].can_play
async def test_multiple_image_events_in_session(hass, auth, hass_client): async def test_multiple_image_events_in_session(
hass, auth, hass_client, subscriber, setup_platform
):
"""Test multiple events published within the same event session.""" """Test multiple events published within the same event session."""
await setup_platform()
event_session_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_session_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..."
event_timestamp1 = dt_util.now() event_timestamp1 = dt_util.now()
event_timestamp2 = event_timestamp1 + datetime.timedelta(seconds=5) event_timestamp2 = event_timestamp1 + datetime.timedelta(seconds=5)
subscriber = await async_setup_devices(
hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS
)
assert len(hass.states.async_all()) == 1 assert len(hass.states.async_all()) == 1
camera = hass.states.get("camera.front") camera = hass.states.get("camera.front")
@ -560,13 +582,19 @@ async def test_multiple_image_events_in_session(hass, auth, hass_client):
assert contents == IMAGE_BYTES_FROM_EVENT + b"-1" assert contents == IMAGE_BYTES_FROM_EVENT + b"-1"
async def test_multiple_clip_preview_events_in_session(hass, auth, hass_client): @pytest.mark.parametrize("device_traits", [BATTERY_CAMERA_TRAITS])
async def test_multiple_clip_preview_events_in_session(
hass,
auth,
hass_client,
subscriber,
setup_platform,
):
"""Test multiple events published within the same event session.""" """Test multiple events published within the same event session."""
await setup_platform()
event_timestamp1 = dt_util.now() event_timestamp1 = dt_util.now()
event_timestamp2 = event_timestamp1 + datetime.timedelta(seconds=5) event_timestamp2 = event_timestamp1 + datetime.timedelta(seconds=5)
subscriber = await async_setup_devices(
hass, auth, CAMERA_DEVICE_TYPE, BATTERY_CAMERA_TRAITS
)
assert len(hass.states.async_all()) == 1 assert len(hass.states.async_all()) == 1
camera = hass.states.get("camera.front") camera = hass.states.get("camera.front")
@ -656,9 +684,9 @@ async def test_multiple_clip_preview_events_in_session(hass, auth, hass_client):
assert contents == IMAGE_BYTES_FROM_EVENT assert contents == IMAGE_BYTES_FROM_EVENT
async def test_browse_invalid_device_id(hass, auth): async def test_browse_invalid_device_id(hass, auth, setup_platform):
"""Test a media source request for an invalid device id.""" """Test a media source request for an invalid device id."""
await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) await setup_platform()
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)})
@ -676,9 +704,9 @@ async def test_browse_invalid_device_id(hass, auth):
) )
async def test_browse_invalid_event_id(hass, auth): async def test_browse_invalid_event_id(hass, auth, setup_platform):
"""Test a media source browsing for an invalid event id.""" """Test a media source browsing for an invalid event id."""
await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) await setup_platform()
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)})
@ -699,9 +727,9 @@ async def test_browse_invalid_event_id(hass, auth):
) )
async def test_resolve_missing_event_id(hass, auth): async def test_resolve_missing_event_id(hass, auth, setup_platform):
"""Test a media source request missing an event id.""" """Test a media source request missing an event id."""
await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) await setup_platform()
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)})
@ -716,10 +744,9 @@ async def test_resolve_missing_event_id(hass, auth):
) )
async def test_resolve_invalid_device_id(hass, auth): async def test_resolve_invalid_device_id(hass, auth, setup_platform):
"""Test resolving media for an invalid event id.""" """Test resolving media for an invalid event id."""
await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) await setup_platform()
with pytest.raises(Unresolvable): with pytest.raises(Unresolvable):
await media_source.async_resolve_media( await media_source.async_resolve_media(
hass, hass,
@ -728,9 +755,9 @@ async def test_resolve_invalid_device_id(hass, auth):
) )
async def test_resolve_invalid_event_id(hass, auth): async def test_resolve_invalid_event_id(hass, auth, setup_platform):
"""Test resolving media for an invalid event id.""" """Test resolving media for an invalid event id."""
await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) await setup_platform()
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)})
@ -750,14 +777,14 @@ async def test_resolve_invalid_event_id(hass, auth):
assert media.mime_type == "image/jpeg" assert media.mime_type == "image/jpeg"
async def test_camera_event_clip_preview(hass, auth, hass_client, mp4): @pytest.mark.parametrize("device_traits", [BATTERY_CAMERA_TRAITS])
async def test_camera_event_clip_preview(
hass, auth, hass_client, mp4, subscriber, setup_platform
):
"""Test an event for a battery camera video clip.""" """Test an event for a battery camera video clip."""
subscriber = await async_setup_devices(
hass, auth, CAMERA_DEVICE_TYPE, BATTERY_CAMERA_TRAITS
)
# Capture any events published # Capture any events published
received_events = async_capture_events(hass, NEST_EVENT) received_events = async_capture_events(hass, NEST_EVENT)
await setup_platform()
auth.responses = [ auth.responses = [
aiohttp.web.Response(body=mp4.getvalue()), aiohttp.web.Response(body=mp4.getvalue()),
@ -857,10 +884,11 @@ async def test_camera_event_clip_preview(hass, auth, hass_client, mp4):
await response.read() # Animated gif format not tested await response.read() # Animated gif format not tested
async def test_event_media_render_invalid_device_id(hass, auth, hass_client): async def test_event_media_render_invalid_device_id(
hass, auth, hass_client, setup_platform
):
"""Test event media API called with an invalid device id.""" """Test event media API called with an invalid device id."""
await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) await setup_platform()
client = await hass_client() client = await hass_client()
response = await client.get("/api/nest/event_media/invalid-device-id") response = await client.get("/api/nest/event_media/invalid-device-id")
assert response.status == HTTPStatus.NOT_FOUND, ( assert response.status == HTTPStatus.NOT_FOUND, (
@ -868,10 +896,11 @@ async def test_event_media_render_invalid_device_id(hass, auth, hass_client):
) )
async def test_event_media_render_invalid_event_id(hass, auth, hass_client): async def test_event_media_render_invalid_event_id(
hass, auth, hass_client, setup_platform
):
"""Test event media API called with an invalid device id.""" """Test event media API called with an invalid device id."""
await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) await setup_platform()
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)})
assert device assert device
@ -884,13 +913,11 @@ async def test_event_media_render_invalid_event_id(hass, auth, hass_client):
) )
async def test_event_media_failure(hass, auth, hass_client): async def test_event_media_failure(hass, auth, hass_client, subscriber, setup_platform):
"""Test event media fetch sees a failure from the server.""" """Test event media fetch sees a failure from the server."""
subscriber = await async_setup_devices(
hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS
)
received_events = async_capture_events(hass, NEST_EVENT) received_events = async_capture_events(hass, NEST_EVENT)
await setup_platform()
# Failure from server when fetching media # Failure from server when fetching media
auth.responses = [ auth.responses = [
aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR), aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR),
@ -937,10 +964,11 @@ async def test_event_media_failure(hass, auth, hass_client):
) )
async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin_user): async def test_media_permission_unauthorized(
hass, auth, hass_client, hass_admin_user, setup_platform
):
"""Test case where user does not have permissions to view media.""" """Test case where user does not have permissions to view media."""
await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) await setup_platform()
assert len(hass.states.async_all()) == 1 assert len(hass.states.async_all()) == 1
camera = hass.states.get("camera.front") camera = hass.states.get("camera.front")
assert camera is not None assert camera is not None
@ -962,33 +990,22 @@ async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin
) )
async def test_multiple_devices(hass, auth, hass_client): async def test_multiple_devices(
hass, auth, hass_client, create_device, subscriber, setup_platform
):
"""Test events received for multiple devices.""" """Test events received for multiple devices."""
device_id1 = f"{DEVICE_ID}-1"
device_id2 = f"{DEVICE_ID}-2" device_id2 = f"{DEVICE_ID}-2"
create_device.create(
devices = { raw_data={
device_id1: Device.MakeDevice(
{
"name": device_id1,
"type": CAMERA_DEVICE_TYPE,
"traits": CAMERA_TRAITS,
},
auth=auth,
),
device_id2: Device.MakeDevice(
{
"name": device_id2, "name": device_id2,
"type": CAMERA_DEVICE_TYPE, "type": CAMERA_DEVICE_TYPE,
"traits": CAMERA_TRAITS, "traits": CAMERA_TRAITS,
},
auth=auth,
),
} }
subscriber = await async_setup_sdm_platform(hass, PLATFORM, devices=devices) )
await setup_platform()
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device1 = device_registry.async_get_device({(DOMAIN, device_id1)}) device1 = device_registry.async_get_device({(DOMAIN, DEVICE_ID)})
assert device1 assert device1
device2 = device_registry.async_get_device({(DOMAIN, device_id2)}) device2 = device_registry.async_get_device({(DOMAIN, device_id2)})
assert device2 assert device2
@ -1018,7 +1035,7 @@ async def test_multiple_devices(hass, auth, hass_client):
f"event-session-id-{i}", f"event-session-id-{i}",
f"event-id-{i}", f"event-id-{i}",
PERSON_EVENT, PERSON_EVENT,
device_id=device_id1, device_id=DEVICE_ID,
) )
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1073,34 +1090,18 @@ def event_store() -> Generator[None, None, None]:
yield yield
async def test_media_store_persistence(hass, auth, hass_client, event_store): @pytest.mark.parametrize("device_traits", [BATTERY_CAMERA_TRAITS])
"""Test the disk backed media store persistence.""" async def test_media_store_persistence(
nest_device = Device.MakeDevice( hass,
{ auth,
"name": DEVICE_ID, hass_client,
"type": CAMERA_DEVICE_TYPE, event_store,
"traits": BATTERY_CAMERA_TRAITS, subscriber,
}, setup_platform,
auth=auth, config_entry,
)
subscriber = FakeSubscriber()
device_manager = await subscriber.async_get_device_manager()
device_manager.add_device(nest_device)
# Fetch media for events when published
subscriber.cache_policy.fetch = True
config_entry = create_config_entry()
config_entry.add_to_hass(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation"
), patch("homeassistant.components.nest.PLATFORMS", [PLATFORM]), patch(
"homeassistant.components.nest.api.GoogleNestSubscriber",
return_value=subscriber,
): ):
assert await async_setup_component(hass, DOMAIN, CONFIG) """Test the disk backed media store persistence."""
await hass.async_block_till_done() await setup_platform()
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)})
@ -1154,16 +1155,6 @@ async def test_media_store_persistence(hass, auth, hass_client, event_store):
# Now rebuild the entire integration and verify that all persisted storage # Now rebuild the entire integration and verify that all persisted storage
# can be re-loaded from disk. # can be re-loaded from disk.
subscriber = FakeSubscriber()
device_manager = await subscriber.async_get_device_manager()
device_manager.add_device(nest_device)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation"
), patch("homeassistant.components.nest.PLATFORMS", [PLATFORM]), patch(
"homeassistant.components.nest.api.GoogleNestSubscriber",
return_value=subscriber,
):
await hass.config_entries.async_reload(config_entry.entry_id) await hass.config_entries.async_reload(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1197,11 +1188,12 @@ async def test_media_store_persistence(hass, auth, hass_client, event_store):
assert contents == IMAGE_BYTES_FROM_EVENT assert contents == IMAGE_BYTES_FROM_EVENT
async def test_media_store_save_filesystem_error(hass, auth, hass_client): @pytest.mark.parametrize("device_traits", [BATTERY_CAMERA_TRAITS])
async def test_media_store_save_filesystem_error(
hass, auth, hass_client, subscriber, setup_platform
):
"""Test a filesystem error writing event media.""" """Test a filesystem error writing event media."""
subscriber = await async_setup_devices( await setup_platform()
hass, auth, CAMERA_DEVICE_TYPE, BATTERY_CAMERA_TRAITS
)
auth.responses = [ auth.responses = [
aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT),
@ -1250,11 +1242,11 @@ async def test_media_store_save_filesystem_error(hass, auth, hass_client):
) )
async def test_media_store_load_filesystem_error(hass, auth, hass_client): async def test_media_store_load_filesystem_error(
hass, auth, hass_client, subscriber, setup_platform
):
"""Test a filesystem error reading event media.""" """Test a filesystem error reading event media."""
subscriber = await async_setup_devices( await setup_platform()
hass, auth, CAMERA_DEVICE_TYPE, BATTERY_CAMERA_TRAITS
)
assert len(hass.states.async_all()) == 1 assert len(hass.states.async_all()) == 1
camera = hass.states.get("camera.front") camera = hass.states.get("camera.front")
@ -1299,17 +1291,12 @@ async def test_media_store_load_filesystem_error(hass, auth, hass_client):
) )
async def test_camera_event_media_eviction(hass, auth, hass_client): @pytest.mark.parametrize("device_traits,cache_size", [(BATTERY_CAMERA_TRAITS, 5)])
async def test_camera_event_media_eviction(
hass, auth, hass_client, subscriber, setup_platform
):
"""Test media files getting evicted from the cache.""" """Test media files getting evicted from the cache."""
await setup_platform()
# Set small cache size for testing eviction
with patch("homeassistant.components.nest.EVENT_MEDIA_CACHE_SIZE", new=5):
subscriber = await async_setup_devices(
hass,
auth,
CAMERA_DEVICE_TYPE,
BATTERY_CAMERA_TRAITS,
)
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)})
@ -1384,23 +1371,9 @@ async def test_camera_event_media_eviction(hass, auth, hass_client):
await hass.async_block_till_done() await hass.async_block_till_done()
async def test_camera_image_resize(hass, auth, hass_client): async def test_camera_image_resize(hass, auth, hass_client, subscriber, setup_platform):
"""Test scaling a thumbnail for an event image.""" """Test scaling a thumbnail for an event image."""
event_timestamp = dt_util.now() await setup_platform()
subscriber = await async_setup_devices(
hass,
auth,
CAMERA_DEVICE_TYPE,
CAMERA_TRAITS,
events=[
create_event(
EVENT_SESSION_ID,
EVENT_ID,
PERSON_EVENT,
timestamp=event_timestamp,
),
],
)
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)})