Add Cambridge Audio integration (#125642)

* Add Cambridge Audio integration

* Add zeroconf discovery to Cambridge Audio

* Bump aiostreammagic to 2.0.1

* Bump aiostreammagic to 2.0.3

* Add tests to Cambridge Audio

* Fix package names for Cambridge Audio

* Removed unnecessary mock from Cambridge Audio tests

* Clean up Cambridge Audio integration

* Add additional zeroconf tests for Cambridge Audio

* Update tests/components/cambridge_audio/test_config_flow.py

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Noah Husby 2024-09-10 12:27:51 -04:00 committed by GitHub
parent 457cb7ace0
commit 650c92a3cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 793 additions and 0 deletions

View File

@ -238,6 +238,8 @@ build.json @home-assistant/supervisor
/tests/components/button/ @home-assistant/core
/homeassistant/components/calendar/ @home-assistant/core
/tests/components/calendar/ @home-assistant/core
/homeassistant/components/cambridge_audio/ @noahhusby
/tests/components/cambridge_audio/ @noahhusby
/homeassistant/components/camera/ @home-assistant/core
/tests/components/camera/ @home-assistant/core
/homeassistant/components/cast/ @emontnemery

View File

@ -0,0 +1,46 @@
"""The Cambridge Audio integration."""
from __future__ import annotations
import asyncio
from aiostreammagic import StreamMagicClient
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import CONNECT_TIMEOUT, STREAM_MAGIC_EXCEPTIONS
PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER]
type CambridgeAudioConfigEntry = ConfigEntry[StreamMagicClient]
async def async_setup_entry(
hass: HomeAssistant, entry: CambridgeAudioConfigEntry
) -> bool:
"""Set up Cambridge Audio integration from a config entry."""
client = StreamMagicClient(entry.data[CONF_HOST])
try:
async with asyncio.timeout(CONNECT_TIMEOUT):
await client.connect()
except STREAM_MAGIC_EXCEPTIONS as err:
raise ConfigEntryNotReady(f"Error while connecting to {client.host}") from err
entry.runtime_data = client
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(
hass: HomeAssistant, entry: CambridgeAudioConfigEntry
) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
await entry.runtime_data.disconnect()
return unload_ok

View File

@ -0,0 +1,93 @@
"""Config flow for Cambridge Audio."""
import asyncio
from typing import Any
from aiostreammagic import StreamMagicClient
import voluptuous as vol
from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_NAME
from .const import CONNECT_TIMEOUT, DOMAIN, STREAM_MAGIC_EXCEPTIONS
class CambridgeAudioConfigFlow(ConfigFlow, domain=DOMAIN):
"""Cambridge Audio configuration flow."""
VERSION = 1
def __init__(self) -> None:
"""Initialize the config flow."""
self.data: dict[str, Any] = {}
async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle zeroconf discovery."""
self.data[CONF_HOST] = host = discovery_info.host
await self.async_set_unique_id(discovery_info.properties["serial"])
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
client = StreamMagicClient(host)
try:
async with asyncio.timeout(CONNECT_TIMEOUT):
await client.connect()
except STREAM_MAGIC_EXCEPTIONS:
return self.async_abort(reason="cannot_connect")
self.data[CONF_NAME] = client.info.name
self.context["title_placeholders"] = {
"name": self.data[CONF_NAME],
}
await client.disconnect()
return await self.async_step_discovery_confirm()
async def async_step_discovery_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm discovery."""
if user_input is not None:
return self.async_create_entry(
title=self.data[CONF_NAME],
data={CONF_HOST: self.data[CONF_HOST]},
)
self._set_confirm_only()
return self.async_show_form(
step_id="discovery_confirm",
description_placeholders={
"name": self.data[CONF_NAME],
},
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
errors: dict[str, str] = {}
if user_input:
client = StreamMagicClient(user_input[CONF_HOST])
try:
async with asyncio.timeout(CONNECT_TIMEOUT):
await client.connect()
except STREAM_MAGIC_EXCEPTIONS:
errors["base"] = "cannot_connect"
else:
await self.async_set_unique_id(
client.info.unit_id, raise_on_progress=False
)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=client.info.name,
data={CONF_HOST: user_input[CONF_HOST]},
)
finally:
await client.disconnect()
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
errors=errors,
)

View File

@ -0,0 +1,19 @@
"""Constants for the Cambridge Audio integration."""
import asyncio
import logging
from aiostreammagic import StreamMagicConnectionError, StreamMagicError
DOMAIN = "cambridge_audio"
LOGGER = logging.getLogger(__package__)
STREAM_MAGIC_EXCEPTIONS = (
StreamMagicConnectionError,
StreamMagicError,
asyncio.CancelledError,
TimeoutError,
)
CONNECT_TIMEOUT = 5

View File

@ -0,0 +1,26 @@
"""Base class for Cambridge Audio entities."""
from aiostreammagic import StreamMagicClient
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from .const import DOMAIN
class CambridgeAudioEntity(Entity):
"""Defines a base Cambridge Audio entity."""
_attr_has_entity_name = True
def __init__(self, client: StreamMagicClient) -> None:
"""Initialize Cambridge Audio entity."""
self.client = client
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, client.info.unit_id)},
name=client.info.name,
manufacturer="Cambridge Audio",
model=client.info.model,
serial_number=client.info.unit_id,
configuration_url=f"http://{client.host}",
)

View File

@ -0,0 +1,12 @@
{
"domain": "cambridge_audio",
"name": "Cambridge Audio",
"codeowners": ["@noahhusby"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cambridge_audio",
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["aiostreammagic"],
"requirements": ["aiostreammagic==2.0.3"],
"zeroconf": ["_stream-magic._tcp.local.", "_smoip._tcp.local."]
}

View File

@ -0,0 +1,190 @@
"""Support for Cambridge Audio AV Receiver."""
from __future__ import annotations
from datetime import datetime
from aiostreammagic import StreamMagicClient
from homeassistant.components.media_player import (
MediaPlayerEntity,
MediaPlayerEntityFeature,
MediaPlayerState,
MediaType,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .entity import CambridgeAudioEntity
BASE_FEATURES = (
MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.TURN_OFF
| MediaPlayerEntityFeature.TURN_ON
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Cambridge Audio device based on a config entry."""
client: StreamMagicClient = entry.runtime_data
async_add_entities([CambridgeAudioDevice(client)])
class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
"""Representation of a Cambridge Audio Media Player Device."""
_attr_name = None
_attr_media_content_type = MediaType.MUSIC
def __init__(self, client: StreamMagicClient) -> None:
"""Initialize an Cambridge Audio entity."""
super().__init__(client)
self._attr_unique_id = client.info.unit_id
async def _state_update_callback(self, _client: StreamMagicClient) -> None:
"""Call when the device is notified of changes."""
self.schedule_update_ha_state()
async def async_added_to_hass(self) -> None:
"""Register callback handlers."""
await self.client.register_state_update_callbacks(self._state_update_callback)
async def async_will_remove_from_hass(self) -> None:
"""Remove callbacks."""
await self.client.unregister_state_update_callbacks(self._state_update_callback)
@property
def supported_features(self) -> MediaPlayerEntityFeature:
"""Supported features for the media player."""
controls = self.client.now_playing.controls
features = BASE_FEATURES
if "play_pause" in controls:
features |= MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PAUSE
if "play" in controls:
features |= MediaPlayerEntityFeature.PLAY
if "pause" in controls:
features |= MediaPlayerEntityFeature.PAUSE
if "track_next" in controls:
features |= MediaPlayerEntityFeature.NEXT_TRACK
if "track_previous" in controls:
features |= MediaPlayerEntityFeature.PREVIOUS_TRACK
return features
@property
def state(self) -> MediaPlayerState:
"""Return the state of the device."""
media_state = self.client.play_state.state
if media_state == "NETWORK":
return MediaPlayerState.STANDBY
if self.client.state.power:
if media_state == "play":
return MediaPlayerState.PLAYING
if media_state == "pause":
return MediaPlayerState.PAUSED
if media_state == "connecting":
return MediaPlayerState.BUFFERING
if media_state in ("stop", "ready"):
return MediaPlayerState.IDLE
return MediaPlayerState.ON
return MediaPlayerState.OFF
@property
def source_list(self) -> list[str]:
"""Return a list of available input sources."""
return [item.name for item in self.client.sources]
@property
def source(self) -> str | None:
"""Return the current input source."""
return next(
(
item.name
for item in self.client.sources
if item.id == self.client.state.source
),
None,
)
@property
def media_title(self) -> str | None:
"""Title of current playing media."""
return self.client.play_state.metadata.title
@property
def media_artist(self) -> str | None:
"""Artist of current playing media, music track only."""
return self.client.play_state.metadata.artist
@property
def media_album_name(self) -> str | None:
"""Album name of current playing media, music track only."""
return self.client.play_state.metadata.album
@property
def media_image_url(self) -> str | None:
"""Image url of current playing media."""
return self.client.play_state.metadata.art_url
@property
def media_duration(self) -> int | None:
"""Duration of the current media."""
return self.client.play_state.metadata.duration
@property
def media_position(self) -> int | None:
"""Position of the current media."""
return self.client.play_state.position
@property
def media_position_updated_at(self) -> datetime:
"""Last time the media position was updated."""
return self.client.position_last_updated
async def async_media_play_pause(self) -> None:
"""Toggle play/pause the current media."""
await self.client.play_pause()
async def async_media_pause(self) -> None:
"""Pause the current media."""
controls = self.client.now_playing.controls
if "pause" not in controls and "play_pause" in controls:
await self.client.play_pause()
else:
await self.client.pause()
async def async_media_stop(self) -> None:
"""Stop the current media."""
await self.client.stop()
async def async_media_play(self) -> None:
"""Play the current media."""
if self.state == MediaPlayerState.PAUSED:
await self.client.play_pause()
async def async_media_next_track(self) -> None:
"""Skip to the next track."""
await self.client.next_track()
async def async_media_previous_track(self) -> None:
"""Skip to the previous track."""
await self.client.previous_track()
async def async_select_source(self, source: str) -> None:
"""Select the source."""
for src in self.client.sources:
if src.name == source:
await self.client.set_source_by_id(src.id)
break
async def async_turn_on(self) -> None:
"""Power on the device."""
await self.client.power_on()
async def async_turn_off(self) -> None:
"""Power off the device."""
await self.client.power_off()

View File

@ -0,0 +1,26 @@
{
"config": {
"flow_title": "{name}",
"step": {
"user": {
"description": "Set up your Cambridge Audio Streamer to integrate with Home Assistant.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The hostname or IP address of the Cambridge Audio Streamer."
}
},
"discovery_confirm": {
"description": "Do you want to setup {name}?"
}
},
"error": {
"cannot_connect": "Failed to connect to Cambridge Audio device. Please make sure the device is powered up and connected to the network. Try power-cycling the device if it does not connect."
},
"abort": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}

View File

@ -100,6 +100,7 @@ FLOWS = {
"bthome",
"buienradar",
"caldav",
"cambridge_audio",
"canary",
"cast",
"ccm15",

View File

@ -849,6 +849,12 @@
"config_flow": true,
"iot_class": "cloud_polling"
},
"cambridge_audio": {
"name": "Cambridge Audio",
"integration_type": "device",
"config_flow": true,
"iot_class": "local_push"
},
"canary": {
"name": "Canary",
"integration_type": "hub",

View File

@ -764,6 +764,11 @@ ZEROCONF = {
"name": "slzb-06*",
},
],
"_smoip._tcp.local.": [
{
"domain": "cambridge_audio",
},
],
"_sonos._tcp.local.": [
{
"domain": "sonos",
@ -793,6 +798,11 @@ ZEROCONF = {
"name": "smappee50*",
},
],
"_stream-magic._tcp.local.": [
{
"domain": "cambridge_audio",
},
],
"_system-bridge._tcp.local.": [
{
"domain": "system_bridge",

View File

@ -373,6 +373,9 @@ aiosolaredge==0.2.0
# homeassistant.components.steamist
aiosteamist==1.0.0
# homeassistant.components.cambridge_audio
aiostreammagic==2.0.3
# homeassistant.components.switcher_kis
aioswitcher==4.0.3

View File

@ -355,6 +355,9 @@ aiosolaredge==0.2.0
# homeassistant.components.steamist
aiosteamist==1.0.0
# homeassistant.components.cambridge_audio
aiostreammagic==2.0.3
# homeassistant.components.switcher_kis
aioswitcher==4.0.3

View File

@ -0,0 +1,13 @@
"""Tests for the Cambridge Audio integration."""
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
"""Fixture for setting up the component."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()

View File

@ -0,0 +1,55 @@
"""Cambridge Audio tests configuration."""
from collections.abc import Generator
from unittest.mock import Mock, patch
from aiostreammagic.models import Info
import pytest
from homeassistant.components.cambridge_audio.const import DOMAIN
from homeassistant.const import CONF_HOST
from tests.common import MockConfigEntry, load_fixture
from tests.components.smhi.common import AsyncMock
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.cambridge_audio.async_setup_entry",
return_value=True,
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture
def mock_stream_magic_client() -> Generator[AsyncMock]:
"""Mock an Cambridge Audio client."""
with (
patch(
"homeassistant.components.cambridge_audio.StreamMagicClient",
autospec=True,
) as mock_client,
patch(
"homeassistant.components.cambridge_audio.config_flow.StreamMagicClient",
new=mock_client,
),
):
client = mock_client.return_value
client.host = "192.168.20.218"
client.info = Info.from_json(load_fixture("get_info.json", DOMAIN))
client.is_connected = Mock(return_value=True)
yield client
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Mock a config entry."""
return MockConfigEntry(
domain=DOMAIN,
title="Cambridge Audio CXNv2",
data={CONF_HOST: "192.168.20.218"},
unique_id="0020c2d8",
)

View File

@ -0,0 +1,32 @@
{
"name": "Cambridge Audio CXNv2",
"timezone": "America/Chicago",
"locale": "en_GB",
"usage_reports": true,
"setup": true,
"sources_setup": true,
"versions": [
{
"component": "cast",
"version": "1.52.272222"
},
{
"component": "MCU",
"version": "3.1+0.5+36"
},
{
"component": "service-pack",
"version": "v022-a-151+a"
},
{
"component": "application",
"version": "1.0+gitAUTOINC+a94a3e2ad8"
}
],
"udn": "02680b5c-1320-4d54-9f7c-3cfe915ad4c3",
"hcv": 3764,
"model": "CXNv2",
"unit_id": "0020c2d8",
"max_http_body_size": 65536,
"api": "1.8"
}

View File

@ -0,0 +1,33 @@
# serializer version: 1
# name: test_device_info
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': 'http://192.168.20.218',
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'cambridge_audio',
'0020c2d8',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Cambridge Audio',
'model': 'CXNv2',
'model_id': None,
'name': 'Cambridge Audio CXNv2',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': '0020c2d8',
'suggested_area': None,
'sw_version': None,
'via_device_id': None,
})
# ---

View File

@ -0,0 +1,194 @@
"""Tests for the Cambridge Audio config flow."""
from ipaddress import ip_address
from unittest.mock import AsyncMock
from aiostreammagic import StreamMagicError
from homeassistant.components.cambridge_audio.const import DOMAIN
from homeassistant.components.zeroconf import ZeroconfServiceInfo
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry
ZEROCONF_DISCOVERY = ZeroconfServiceInfo(
ip_address=ip_address("192.168.20.218"),
ip_addresses=[ip_address("192.168.20.218")],
hostname="cambridge_CXNv2.local.",
name="cambridge_CXNv2._stream-magic._tcp.local.",
port=80,
type="_stream-magic._tcp.local.",
properties={
"serial": "0020c2d8",
"hcv": "3764",
"software": "v022-a-151+a",
"model": "CXNv2",
"udn": "02680b5c-1320-4d54-9f7c-3cfe915ad4c3",
},
)
async def test_full_flow(
hass: HomeAssistant,
mock_stream_magic_client: AsyncMock,
mock_setup_entry: AsyncMock,
) -> None:
"""Test full flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: "192.168.20.218"},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Cambridge Audio CXNv2"
assert result["data"] == {
CONF_HOST: "192.168.20.218",
}
assert result["result"].unique_id == "0020c2d8"
async def test_flow_errors(
hass: HomeAssistant,
mock_stream_magic_client: AsyncMock,
mock_setup_entry: AsyncMock,
) -> None:
"""Test flow errors."""
mock_stream_magic_client.connect.side_effect = StreamMagicError
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: "192.168.20.218"},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "cannot_connect"}
mock_stream_magic_client.connect.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: "192.168.20.218"},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_duplicate(
hass: HomeAssistant,
mock_stream_magic_client: AsyncMock,
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test duplicate flow."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: "192.168.20.218"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
async def test_zeroconf_flow(
hass: HomeAssistant,
mock_stream_magic_client: AsyncMock,
mock_setup_entry: AsyncMock,
) -> None:
"""Test zeroconf flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data=ZEROCONF_DISCOVERY,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "discovery_confirm"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Cambridge Audio CXNv2"
assert result["data"] == {
CONF_HOST: "192.168.20.218",
}
assert result["result"].unique_id == "0020c2d8"
async def test_zeroconf_flow_errors(
hass: HomeAssistant,
mock_stream_magic_client: AsyncMock,
mock_setup_entry: AsyncMock,
) -> None:
"""Test zeroconf flow."""
mock_stream_magic_client.connect.side_effect = StreamMagicError
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data=ZEROCONF_DISCOVERY,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "cannot_connect"
mock_stream_magic_client.connect.side_effect = None
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data=ZEROCONF_DISCOVERY,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "discovery_confirm"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Cambridge Audio CXNv2"
assert result["data"] == {
CONF_HOST: "192.168.20.218",
}
assert result["result"].unique_id == "0020c2d8"
async def test_zeroconf_duplicate(
hass: HomeAssistant,
mock_stream_magic_client: AsyncMock,
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test duplicate flow."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data=ZEROCONF_DISCOVERY,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"

View File

@ -0,0 +1,29 @@
"""Tests for the Cambridge Audio integration."""
from unittest.mock import AsyncMock
from syrupy import SnapshotAssertion
from homeassistant.components.cambridge_audio.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import setup_integration
from tests.common import MockConfigEntry
async def test_device_info(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_stream_magic_client: AsyncMock,
mock_config_entry: MockConfigEntry,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test device registry integration."""
await setup_integration(hass, mock_config_entry)
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, mock_config_entry.unique_id)}
)
assert device_entry is not None
assert device_entry == snapshot