Add Kaleidescape integration (#67711)

This commit is contained in:
Steve Easley 2022-03-07 15:16:43 -08:00 committed by GitHub
parent c70bed86ff
commit ea82f2e293
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 978 additions and 0 deletions

View File

@ -117,6 +117,7 @@ homeassistant.components.isy994.*
homeassistant.components.iqvia.*
homeassistant.components.jellyfin.*
homeassistant.components.jewish_calendar.*
homeassistant.components.kaleidescape.*
homeassistant.components.knx.*
homeassistant.components.kraken.*
homeassistant.components.lametric.*

View File

@ -513,6 +513,8 @@ tests/components/jewish_calendar/* @tsvi
homeassistant/components/juicenet/* @jesserockz
tests/components/juicenet/* @jesserockz
homeassistant/components/kaiterra/* @Michsior14
homeassistant/components/kaleidescape/* @SteveEasley
tests/components/kaleidescape/* @SteveEasley
homeassistant/components/keba/* @dannerph
homeassistant/components/keenetic_ndms2/* @foxel
tests/components/keenetic_ndms2/* @foxel

View File

@ -0,0 +1,96 @@
"""The Kaleidescape integration."""
from __future__ import annotations
from dataclasses import dataclass
import logging
from typing import TYPE_CHECKING
from kaleidescape import Device as KaleidescapeDevice, KaleidescapeError
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from .const import DOMAIN
if TYPE_CHECKING:
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Event, HomeAssistant
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Kaleidescape from a config entry."""
device = KaleidescapeDevice(
entry.data[CONF_HOST], timeout=5, reconnect=True, reconnect_delay=5
)
try:
await device.connect()
except (KaleidescapeError, ConnectionError) as err:
await device.disconnect()
raise ConfigEntryNotReady(
f"Unable to connect to {entry.data[CONF_HOST]}: {err}"
) from err
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = device
async def disconnect(event: Event) -> None:
await device.disconnect()
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect)
)
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
await hass.data[DOMAIN][entry.entry_id].disconnect()
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
@dataclass
class KaleidescapeDeviceInfo:
"""Metadata for a Kaleidescape device."""
host: str
serial: str
name: str
model: str
server_only: bool
class UnsupportedError(HomeAssistantError):
"""Error for unsupported device types."""
async def validate_host(host: str) -> KaleidescapeDeviceInfo:
"""Validate device host."""
device = KaleidescapeDevice(host)
try:
await device.connect()
except (KaleidescapeError, ConnectionError):
await device.disconnect()
raise
info = KaleidescapeDeviceInfo(
host=device.host,
serial=device.system.serial_number,
name=device.system.friendly_name,
model=device.system.type,
server_only=device.is_server_only,
)
await device.disconnect()
return info

View File

@ -0,0 +1,112 @@
"""Config flow for Kaleidescape."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, cast
from urllib.parse import urlparse
import voluptuous as vol
from homeassistant.components import ssdp
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_HOST
from . import KaleidescapeDeviceInfo, UnsupportedError, validate_host
from .const import DEFAULT_HOST, DOMAIN, NAME as KALEIDESCAPE_NAME
if TYPE_CHECKING:
from homeassistant.data_entry_flow import FlowResult
ERROR_CANNOT_CONNECT = "cannot_connect"
ERROR_UNKNOWN = "unknown"
ERROR_UNSUPPORTED = "unsupported"
class KaleidescapeConfigFlow(ConfigFlow, domain=DOMAIN):
"""Config flow for Kaleidescape integration."""
VERSION = 1
discovered_device: KaleidescapeDeviceInfo
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle user initiated device additions."""
errors = {}
host = DEFAULT_HOST
if user_input is not None:
host = user_input[CONF_HOST].strip()
try:
info = await validate_host(host)
if info.server_only:
raise UnsupportedError
except ConnectionError:
errors["base"] = ERROR_CANNOT_CONNECT
except UnsupportedError:
errors["base"] = ERROR_UNSUPPORTED
else:
host = info.host
await self.async_set_unique_id(info.serial, raise_on_progress=False)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
return self.async_create_entry(
title=f"{KALEIDESCAPE_NAME} ({info.name})",
data={CONF_HOST: host},
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Required(CONF_HOST, default=host): str}),
errors=errors,
)
async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult:
"""Handle discovered device."""
host = cast(str, urlparse(discovery_info.ssdp_location).hostname)
serial_number = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]
await self.async_set_unique_id(serial_number)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
try:
self.discovered_device = await validate_host(host)
if self.discovered_device.server_only:
raise UnsupportedError
except ConnectionError:
return self.async_abort(reason=ERROR_CANNOT_CONNECT)
except UnsupportedError:
return self.async_abort(reason=ERROR_UNSUPPORTED)
self.context.update(
{
"title_placeholders": {
"name": self.discovered_device.name,
"model": self.discovered_device.model,
}
}
)
return await self.async_step_discovery_confirm()
async def async_step_discovery_confirm(
self, user_input: dict | None = None
) -> FlowResult:
"""Handle addition of discovered device."""
if user_input is None:
return self.async_show_form(
step_id="discovery_confirm",
description_placeholders={
"name": self.discovered_device.name,
"model": self.discovered_device.model,
},
errors={},
)
return self.async_create_entry(
title=f"{KALEIDESCAPE_NAME} ({self.discovered_device.name})",
data={CONF_HOST: self.discovered_device.host},
)

View File

@ -0,0 +1,5 @@
"""Constants for the Kaleidescape integration."""
NAME = "Kaleidescape"
DOMAIN = "kaleidescape"
DEFAULT_HOST = "my-kaleidescape.local"

View File

@ -0,0 +1,47 @@
"""Base Entity for Kaleidescape."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from homeassistant.core import callback
from homeassistant.helpers.entity import DeviceInfo, Entity
from .const import DOMAIN as KALEIDESCAPE_DOMAIN, NAME as KALEIDESCAPE_NAME
if TYPE_CHECKING:
from kaleidescape import Device as KaleidescapeDevice
_LOGGER = logging.getLogger(__name__)
class KaleidescapeEntity(Entity):
"""Defines a base Kaleidescape entity."""
def __init__(self, device: KaleidescapeDevice) -> None:
"""Initialize entity."""
self._device = device
self._attr_should_poll = False
self._attr_unique_id = device.serial_number
self._attr_name = f"{KALEIDESCAPE_NAME} {device.system.friendly_name}"
self._attr_device_info = DeviceInfo(
identifiers={(KALEIDESCAPE_DOMAIN, self._device.serial_number)},
name=self.name,
model=self._device.system.type,
manufacturer=KALEIDESCAPE_NAME,
sw_version=f"{self._device.system.kos_version}",
suggested_area="Theater",
configuration_url=f"http://{self._device.host}",
)
async def async_added_to_hass(self) -> None:
"""Register update listener."""
@callback
def _update(event: str) -> None:
"""Handle device state changes."""
self.async_write_ha_state()
self.async_on_remove(self._device.dispatcher.connect(_update).disconnect)

View File

@ -0,0 +1,17 @@
{
"domain": "kaleidescape",
"name": "Kaleidescape",
"config_flow": true,
"ssdp": [
{
"manufacturer": "Kaleidescape, Inc.",
"deviceType": "schemas-upnp-org:device:Basic:1"
}
],
"documentation": "https://www.home-assistant.io/integrations/kaleidescape",
"requirements": ["pykaleidescape==2022.2.6"],
"codeowners": [
"@SteveEasley"
],
"iot_class": "local_push"
}

View File

@ -0,0 +1,158 @@
"""Kaleidescape Media Player."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from kaleidescape import const as kaleidescape_const
from homeassistant.components.media_player import MediaPlayerEntity
from homeassistant.components.media_player.const import (
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PLAY,
SUPPORT_PREVIOUS_TRACK,
SUPPORT_STOP,
SUPPORT_TURN_OFF,
SUPPORT_TURN_ON,
)
from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING
from homeassistant.util.dt import utcnow
from .const import DOMAIN as KALEIDESCAPE_DOMAIN
from .entity import KaleidescapeEntity
if TYPE_CHECKING:
from datetime import datetime
from kaleidescape import Device as KaleidescapeDevice
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
KALEIDESCAPE_PLAYING_STATES = [
kaleidescape_const.PLAY_STATUS_PLAYING,
kaleidescape_const.PLAY_STATUS_FORWARD,
kaleidescape_const.PLAY_STATUS_REVERSE,
]
KALEIDESCAPE_PAUSED_STATES = [kaleidescape_const.PLAY_STATUS_PAUSED]
SUPPORTED_FEATURES = (
SUPPORT_TURN_ON
| SUPPORT_TURN_OFF
| SUPPORT_PLAY
| SUPPORT_PAUSE
| SUPPORT_STOP
| SUPPORT_NEXT_TRACK
| SUPPORT_PREVIOUS_TRACK
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the platform from a config entry."""
entities = [KaleidescapeMediaPlayer(hass.data[KALEIDESCAPE_DOMAIN][entry.entry_id])]
async_add_entities(entities)
class KaleidescapeMediaPlayer(KaleidescapeEntity, MediaPlayerEntity):
"""Representation of a Kaleidescape device."""
def __init__(self, device: KaleidescapeDevice) -> None:
"""Initialize media player."""
super().__init__(device)
self._attr_supported_features = SUPPORTED_FEATURES
async def async_turn_on(self) -> None:
"""Send leave standby command."""
await self._device.leave_standby()
async def async_turn_off(self) -> None:
"""Send enter standby command."""
await self._device.enter_standby()
async def async_media_pause(self) -> None:
"""Send pause command."""
await self._device.pause()
async def async_media_play(self) -> None:
"""Send play command."""
await self._device.play()
async def async_media_stop(self) -> None:
"""Send stop command."""
await self._device.stop()
async def async_media_next_track(self) -> None:
"""Send track next command."""
await self._device.next()
async def async_media_previous_track(self) -> None:
"""Send track previous command."""
await self._device.previous()
@property
def state(self) -> str:
"""State of device."""
if self._device.power.state == kaleidescape_const.DEVICE_POWER_STATE_STANDBY:
return STATE_OFF
if self._device.movie.play_status in KALEIDESCAPE_PLAYING_STATES:
return STATE_PLAYING
if self._device.movie.play_status in KALEIDESCAPE_PAUSED_STATES:
return STATE_PAUSED
return STATE_IDLE
@property
def available(self) -> bool:
"""Return if device is available."""
return self._device.is_connected
@property
def media_content_id(self) -> str | None:
"""Content ID of current playing media."""
if self._device.movie.handle:
return self._device.movie.handle
return None
@property
def media_content_type(self) -> str | None:
"""Content type of current playing media."""
return self._device.movie.media_type
@property
def media_duration(self) -> int | None:
"""Duration of current playing media in seconds."""
if self._device.movie.title_length:
return self._device.movie.title_length
return None
@property
def media_position(self) -> int | None:
"""Position of current playing media in seconds."""
if self._device.movie.title_location:
return self._device.movie.title_location
return None
@property
def media_position_updated_at(self) -> datetime | None:
"""When was the position of the current playing media valid."""
if self._device.movie.play_status in KALEIDESCAPE_PLAYING_STATES:
return utcnow()
return None
@property
def media_image_url(self) -> str:
"""Image url of current playing media."""
return self._device.movie.cover
@property
def media_title(self) -> str:
"""Title of current playing media."""
return self._device.movie.title

View File

@ -0,0 +1,27 @@
{
"config": {
"flow_title": "{model} ({name})",
"step": {
"user": {
"title": "Kaleidescape Setup",
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
},
"discovery_confirm": {
"title": "Kaleidescape",
"description": "Do you want to set up the {model} player named {name}?"
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"unknown": "[%key:common::config_flow::error::unknown%]",
"unsupported": "Unsupported device"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unsupported": "Unsupported device"
}
}
}

View File

@ -0,0 +1,27 @@
{
"config": {
"flow_title": "{model} ({name})",
"step": {
"user": {
"title": "Kaleidescape Setup",
"data": {
"host": "Host"
}
},
"discovery_confirm": {
"title": "Kaleidescape Setup",
"description": "Do you want to set up the {model} player named {name}?"
}
},
"abort": {
"already_configured": "Device is already configured",
"already_in_progress": "Configuration flow is already in progress",
"unknown": "Unexpected error",
"unsupported": "Unsupported device"
},
"error": {
"cannot_connect": "Failed to connect",
"unsupported": "Unsupported device"
}
}
}

View File

@ -167,6 +167,7 @@ FLOWS = [
"izone",
"jellyfin",
"juicenet",
"kaleidescape",
"keenetic_ndms2",
"kmtronic",
"knx",

View File

@ -168,6 +168,12 @@ SSDP = {
"manufacturer": "Universal Devices Inc."
}
],
"kaleidescape": [
{
"deviceType": "schemas-upnp-org:device:Basic:1",
"manufacturer": "Kaleidescape, Inc."
}
],
"keenetic_ndms2": [
{
"deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1",

View File

@ -1088,6 +1088,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.kaleidescape.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.knx.*]
check_untyped_defs = true
disallow_incomplete_defs = true

View File

@ -1560,6 +1560,9 @@ pyisy==3.0.1
# homeassistant.components.itach
pyitachip2ir==0.0.7
# homeassistant.components.kaleidescape
pykaleidescape==2022.2.6
# homeassistant.components.kira
pykira==0.1.1

View File

@ -1013,6 +1013,9 @@ pyiss==1.0.1
# homeassistant.components.isy994
pyisy==3.0.1
# homeassistant.components.kaleidescape
pykaleidescape==2022.2.6
# homeassistant.components.kira
pykira==0.1.1

View File

@ -0,0 +1,18 @@
"""Tests for Kaleidescape integration."""
from homeassistant.components import ssdp
from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_SERIAL
MOCK_HOST = "127.0.0.1"
MOCK_SERIAL = "123456"
MOCK_NAME = "Theater"
MOCK_SSDP_DISCOVERY_INFO = ssdp.SsdpServiceInfo(
ssdp_usn="mock_usn",
ssdp_st="mock_st",
ssdp_location=f"http://{MOCK_HOST}",
upnp={
ATTR_UPNP_FRIENDLY_NAME: MOCK_NAME,
ATTR_UPNP_SERIAL: MOCK_SERIAL,
},
)

View File

@ -0,0 +1,73 @@
"""Fixtures for Kaleidescape integration."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
from kaleidescape import Dispatcher
from kaleidescape.device import Automation, Movie, Power, System
import pytest
from homeassistant.components.kaleidescape.const import DOMAIN
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from . import MOCK_HOST, MOCK_SERIAL
from tests.common import MockConfigEntry
@pytest.fixture(name="mock_device")
def fixture_mock_device() -> Generator[None, AsyncMock, None]:
"""Return a mocked Kaleidescape device."""
with patch(
"homeassistant.components.kaleidescape.KaleidescapeDevice", autospec=True
) as mock:
host = MOCK_HOST
device = mock.return_value
device.dispatcher = Dispatcher()
device.host = host
device.port = 10000
device.serial_number = MOCK_SERIAL
device.is_connected = True
device.is_server_only = False
device.is_movie_player = True
device.is_music_player = False
device.system = System(
ip_address=host,
serial_number=MOCK_SERIAL,
type="Strato",
protocol=16,
kos_version="10.4.2-19218",
friendly_name=f"Device {MOCK_SERIAL}",
movie_zones=1,
music_zones=1,
)
device.power = Power(state="standby", readiness="disabled", zone=["available"])
device.movie = Movie()
device.automation = Automation()
yield device
@pytest.fixture(name="mock_config_entry")
def fixture_mock_config_entry() -> MockConfigEntry:
"""Return a mock config entry."""
return MockConfigEntry(
domain=DOMAIN,
unique_id=MOCK_SERIAL,
version=1,
data={CONF_HOST: MOCK_HOST},
)
@pytest.fixture(name="mock_integration")
async def fixture_mock_integration(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> MockConfigEntry:
"""Return a mock ConfigEntry setup for Kaleidescape integration."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
return mock_config_entry

View File

@ -0,0 +1,130 @@
"""Tests for Kaleidescape config flow."""
import dataclasses
from unittest.mock import AsyncMock
from homeassistant.components.kaleidescape.const import DOMAIN
from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
RESULT_TYPE_CREATE_ENTRY,
RESULT_TYPE_FORM,
)
from . import MOCK_HOST, MOCK_SSDP_DISCOVERY_INFO
from tests.common import MockConfigEntry
async def test_user_config_flow_success(
hass: HomeAssistant, mock_device: AsyncMock
) -> None:
"""Test user config flow success."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_HOST: MOCK_HOST}
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert "data" in result
assert result["data"][CONF_HOST] == MOCK_HOST
async def test_user_config_flow_bad_connect_errors(
hass: HomeAssistant, mock_device: AsyncMock
) -> None:
"""Test errors when connection error occurs."""
mock_device.connect.side_effect = ConnectionError
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "cannot_connect"}
async def test_user_config_flow_unsupported_device_errors(
hass: HomeAssistant, mock_device: AsyncMock
) -> None:
"""Test errors when connecting to unsupported device."""
mock_device.is_server_only = True
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "unsupported"}
async def test_user_config_flow_device_exists_abort(
hass: HomeAssistant, mock_device: AsyncMock, mock_integration: MockConfigEntry
) -> None:
"""Test flow aborts when device already configured."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST}
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
async def test_ssdp_config_flow_success(
hass: HomeAssistant, mock_device: AsyncMock
) -> None:
"""Test ssdp config flow success."""
discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "discovery_confirm"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={}
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert "data" in result
assert result["data"][CONF_HOST] == MOCK_HOST
async def test_ssdp_config_flow_bad_connect_aborts(
hass: HomeAssistant, mock_device: AsyncMock
) -> None:
"""Test abort when connection error occurs."""
mock_device.connect.side_effect = ConnectionError
discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "cannot_connect"
async def test_ssdp_config_flow_unsupported_device_aborts(
hass: HomeAssistant, mock_device: AsyncMock
) -> None:
"""Test abort when connecting to unsupported device."""
mock_device.is_server_only = True
discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "unsupported"

View File

@ -0,0 +1,58 @@
"""Tests for Kaleidescape config entry."""
from unittest.mock import AsyncMock
from homeassistant.components.kaleidescape.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from . import MOCK_SERIAL
from tests.common import MockConfigEntry
async def test_unload_config_entry(
hass: HomeAssistant,
mock_device: AsyncMock,
mock_integration: MockConfigEntry,
) -> None:
"""Test config entry loading and unloading."""
mock_config_entry = mock_integration
assert mock_config_entry.state is ConfigEntryState.LOADED
assert mock_device.connect.call_count == 1
assert mock_device.disconnect.call_count == 0
await hass.config_entries.async_unload(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_device.disconnect.call_count == 1
assert mock_config_entry.entry_id not in hass.data[DOMAIN]
async def test_config_entry_not_ready(
hass: HomeAssistant,
mock_device: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test config entry not ready."""
mock_device.connect.side_effect = ConnectionError
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_device(
hass: HomeAssistant,
mock_device: AsyncMock,
mock_integration: MockConfigEntry,
) -> None:
"""Test device."""
device_registry = await hass.helpers.device_registry.async_get_registry()
device = device_registry.async_get_device(
identifiers={("kaleidescape", MOCK_SERIAL)}
)
assert device is not None
assert device.identifiers == {("kaleidescape", MOCK_SERIAL)}

View File

@ -0,0 +1,183 @@
"""Tests for Kaleidescape media player platform."""
from unittest.mock import MagicMock
from kaleidescape import const as kaleidescape_const
from kaleidescape.device import Movie
from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_PREVIOUS_TRACK,
SERVICE_MEDIA_STOP,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_IDLE,
STATE_OFF,
STATE_PAUSED,
STATE_PLAYING,
)
from homeassistant.core import HomeAssistant
from . import MOCK_SERIAL
from tests.common import MockConfigEntry
ENTITY_ID = f"media_player.kaleidescape_device_{MOCK_SERIAL}"
FRIENDLY_NAME = f"Kaleidescape Device {MOCK_SERIAL}"
async def test_entity(
hass: HomeAssistant,
mock_device: MagicMock,
mock_integration: MockConfigEntry,
) -> None:
"""Test entity attributes."""
entity = hass.states.get(ENTITY_ID)
assert entity is not None
assert entity.state == STATE_OFF
assert entity.attributes["friendly_name"] == FRIENDLY_NAME
async def test_update_state(
hass: HomeAssistant,
mock_device: MagicMock,
mock_integration: MockConfigEntry,
) -> None:
"""Tests dispatched signals update player."""
entity = hass.states.get(ENTITY_ID)
assert entity is not None
assert entity.state == STATE_OFF
# Device turns on
mock_device.power.state = kaleidescape_const.DEVICE_POWER_STATE_ON
mock_device.dispatcher.send(kaleidescape_const.DEVICE_POWER_STATE)
await hass.async_block_till_done()
entity = hass.states.get(ENTITY_ID)
assert entity is not None
assert entity.state == STATE_IDLE
# Devices starts playing
mock_device.movie = Movie(
handle="handle",
title="title",
cover="cover",
cover_hires="cover_hires",
rating="rating",
rating_reason="rating_reason",
year="year",
runtime="runtime",
actors=[],
director="director",
directors=[],
genre="genre",
genres=[],
synopsis="synopsis",
color="color",
country="country",
aspect_ratio="aspect_ratio",
media_type="media_type",
play_status=kaleidescape_const.PLAY_STATUS_PLAYING,
play_speed=1,
title_number=1,
title_length=1,
title_location=1,
chapter_number=1,
chapter_length=1,
chapter_location=1,
)
mock_device.dispatcher.send(kaleidescape_const.PLAY_STATUS)
await hass.async_block_till_done()
entity = hass.states.get(ENTITY_ID)
assert entity is not None
assert entity.state == STATE_PLAYING
# Devices pauses playing
mock_device.movie.play_status = kaleidescape_const.PLAY_STATUS_PAUSED
mock_device.dispatcher.send(kaleidescape_const.PLAY_STATUS)
await hass.async_block_till_done()
entity = hass.states.get(ENTITY_ID)
assert entity is not None
assert entity.state == STATE_PAUSED
async def test_services(
hass: HomeAssistant,
mock_device: MagicMock,
mock_integration: MockConfigEntry,
) -> None:
"""Test service calls."""
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
assert mock_device.leave_standby.call_count == 1
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
assert mock_device.enter_standby.call_count == 1
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_MEDIA_PLAY,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
assert mock_device.play.call_count == 1
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_MEDIA_PAUSE,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
assert mock_device.pause.call_count == 1
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_MEDIA_STOP,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
assert mock_device.stop.call_count == 1
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_MEDIA_NEXT_TRACK,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
assert mock_device.next.call_count == 1
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_MEDIA_PREVIOUS_TRACK,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
assert mock_device.previous.call_count == 1
async def test_device(
hass: HomeAssistant,
mock_device: MagicMock,
mock_integration: MockConfigEntry,
) -> None:
"""Test device attributes."""
device_registry = await hass.helpers.device_registry.async_get_registry()
device = device_registry.async_get_device(
identifiers={("kaleidescape", MOCK_SERIAL)}
)
assert device.name == FRIENDLY_NAME
assert device.model == "Strato"
assert device.sw_version == "10.4.2-19218"
assert device.manufacturer == "Kaleidescape"