mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
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:
parent
457cb7ace0
commit
650c92a3cf
@ -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
|
||||
|
46
homeassistant/components/cambridge_audio/__init__.py
Normal file
46
homeassistant/components/cambridge_audio/__init__.py
Normal 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
|
93
homeassistant/components/cambridge_audio/config_flow.py
Normal file
93
homeassistant/components/cambridge_audio/config_flow.py
Normal 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,
|
||||
)
|
19
homeassistant/components/cambridge_audio/const.py
Normal file
19
homeassistant/components/cambridge_audio/const.py
Normal 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
|
26
homeassistant/components/cambridge_audio/entity.py
Normal file
26
homeassistant/components/cambridge_audio/entity.py
Normal 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}",
|
||||
)
|
12
homeassistant/components/cambridge_audio/manifest.json
Normal file
12
homeassistant/components/cambridge_audio/manifest.json
Normal 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."]
|
||||
}
|
190
homeassistant/components/cambridge_audio/media_player.py
Normal file
190
homeassistant/components/cambridge_audio/media_player.py
Normal 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()
|
26
homeassistant/components/cambridge_audio/strings.json
Normal file
26
homeassistant/components/cambridge_audio/strings.json
Normal 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%]"
|
||||
}
|
||||
}
|
||||
}
|
@ -100,6 +100,7 @@ FLOWS = {
|
||||
"bthome",
|
||||
"buienradar",
|
||||
"caldav",
|
||||
"cambridge_audio",
|
||||
"canary",
|
||||
"cast",
|
||||
"ccm15",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
13
tests/components/cambridge_audio/__init__.py
Normal file
13
tests/components/cambridge_audio/__init__.py
Normal 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()
|
55
tests/components/cambridge_audio/conftest.py
Normal file
55
tests/components/cambridge_audio/conftest.py
Normal 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",
|
||||
)
|
32
tests/components/cambridge_audio/fixtures/get_info.json
Normal file
32
tests/components/cambridge_audio/fixtures/get_info.json
Normal 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"
|
||||
}
|
33
tests/components/cambridge_audio/snapshots/test_init.ambr
Normal file
33
tests/components/cambridge_audio/snapshots/test_init.ambr
Normal 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,
|
||||
})
|
||||
# ---
|
194
tests/components/cambridge_audio/test_config_flow.py
Normal file
194
tests/components/cambridge_audio/test_config_flow.py
Normal 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"
|
29
tests/components/cambridge_audio/test_init.py
Normal file
29
tests/components/cambridge_audio/test_init.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user