mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 08:47:57 +00:00
Songpal code and test improvement (#35318)
* Use tests.async_mock instead of asynctest * Remove unnecessary existence check * Improve songpal service registering * Add tests * Seperate device api from entity api * Improve disconnect log messages * Improve tests * Rename SongpalDevice to SongpalEntity * Improve reconnecting * Remove logging and sleep patch from tests * Test unavailable state when disconnected * Rename SongpalEntity.dev to _dev * Add quality scale to manifest
This commit is contained in:
parent
3af3900581
commit
8994931ec4
@ -696,8 +696,6 @@ omit =
|
||||
homeassistant/components/somfy/*
|
||||
homeassistant/components/somfy_mylink/*
|
||||
homeassistant/components/sonarr/sensor.py
|
||||
homeassistant/components/songpal/__init__.py
|
||||
homeassistant/components/songpal/media_player.py
|
||||
homeassistant/components/sonos/*
|
||||
homeassistant/components/sony_projector/switch.py
|
||||
homeassistant/components/spc/*
|
||||
|
@ -10,5 +10,6 @@
|
||||
"st": "urn:schemas-sony-com:service:ScalarWebAPI:1",
|
||||
"manufacturer": "Sony Corporation"
|
||||
}
|
||||
]
|
||||
],
|
||||
"quality_scale": "gold"
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import asyncio
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
|
||||
import async_timeout
|
||||
from songpal import (
|
||||
ConnectChange,
|
||||
ContentChange,
|
||||
@ -23,15 +24,13 @@ from homeassistant.components.media_player.const import (
|
||||
SUPPORT_VOLUME_STEP,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_NAME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
entity_platform,
|
||||
)
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from .const import CONF_ENDPOINT, DOMAIN, SET_SOUND_SETTING
|
||||
@ -50,13 +49,7 @@ SUPPORT_SONGPAL = (
|
||||
| SUPPORT_TURN_OFF
|
||||
)
|
||||
|
||||
SET_SOUND_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(PARAM_NAME): cv.string,
|
||||
vol.Required(PARAM_VALUE): cv.string,
|
||||
}
|
||||
)
|
||||
INITIAL_RETRY_DELAY = 10
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
@ -72,58 +65,38 @@ async def async_setup_entry(
|
||||
hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities
|
||||
) -> None:
|
||||
"""Set up songpal media player."""
|
||||
if DOMAIN not in hass.data:
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
name = config_entry.data[CONF_NAME]
|
||||
endpoint = config_entry.data[CONF_ENDPOINT]
|
||||
|
||||
if endpoint in hass.data[DOMAIN]:
|
||||
_LOGGER.debug("The endpoint exists already, skipping setup.")
|
||||
return
|
||||
|
||||
device = SongpalDevice(name, endpoint)
|
||||
device = Device(endpoint)
|
||||
try:
|
||||
await device.initialize()
|
||||
except SongpalException as ex:
|
||||
_LOGGER.error("Unable to get methods from songpal: %s", ex)
|
||||
async with async_timeout.timeout(
|
||||
10
|
||||
): # set timeout to avoid blocking the setup process
|
||||
await device.get_supported_methods()
|
||||
except (SongpalException, asyncio.TimeoutError) as ex:
|
||||
_LOGGER.warning("[%s(%s)] Unable to connect.", name, endpoint)
|
||||
_LOGGER.debug("Unable to get methods from songpal: %s", ex)
|
||||
raise PlatformNotReady
|
||||
|
||||
hass.data[DOMAIN][endpoint] = device
|
||||
songpal_entity = SongpalEntity(name, device)
|
||||
async_add_entities([songpal_entity], True)
|
||||
|
||||
async_add_entities([device], True)
|
||||
|
||||
async def async_service_handler(service):
|
||||
"""Service handler."""
|
||||
entity_id = service.data.get("entity_id", None)
|
||||
params = {
|
||||
key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID
|
||||
}
|
||||
|
||||
for device in hass.data[DOMAIN].values():
|
||||
if device.entity_id == entity_id or entity_id is None:
|
||||
_LOGGER.debug(
|
||||
"Calling %s (entity: %s) with params %s", service, entity_id, params
|
||||
)
|
||||
|
||||
await device.async_set_sound_setting(
|
||||
params[PARAM_NAME], params[PARAM_VALUE]
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SET_SOUND_SETTING, async_service_handler, schema=SET_SOUND_SCHEMA
|
||||
platform = entity_platform.current_platform.get()
|
||||
platform.async_register_entity_service(
|
||||
SET_SOUND_SETTING,
|
||||
{vol.Required(PARAM_NAME): cv.string, vol.Required(PARAM_VALUE): cv.string},
|
||||
"async_set_sound_setting",
|
||||
)
|
||||
|
||||
|
||||
class SongpalDevice(MediaPlayerEntity):
|
||||
class SongpalEntity(MediaPlayerEntity):
|
||||
"""Class representing a Songpal device."""
|
||||
|
||||
def __init__(self, name, endpoint, poll=False):
|
||||
def __init__(self, name, device):
|
||||
"""Init."""
|
||||
self._name = name
|
||||
self._endpoint = endpoint
|
||||
self._poll = poll
|
||||
self.dev = Device(self._endpoint)
|
||||
self._dev = device
|
||||
self._sysinfo = None
|
||||
self._model = None
|
||||
|
||||
@ -143,19 +116,15 @@ class SongpalDevice(MediaPlayerEntity):
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return True if the device should be polled."""
|
||||
return self._poll
|
||||
return False
|
||||
|
||||
async def initialize(self):
|
||||
"""Initialize the device."""
|
||||
await self.dev.get_supported_methods()
|
||||
self._sysinfo = await self.dev.get_system_info()
|
||||
interface_info = await self.dev.get_interface_information()
|
||||
self._model = interface_info.modelName
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity is added to hass."""
|
||||
await self.async_activate_websocket()
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Run when entity will be removed from hass."""
|
||||
self.hass.data[DOMAIN].pop(self._endpoint)
|
||||
await self.dev.stop_listen_notifications()
|
||||
await self._dev.stop_listen_notifications()
|
||||
|
||||
async def async_activate_websocket(self):
|
||||
"""Activate websocket for listening if wanted."""
|
||||
@ -182,40 +151,48 @@ class SongpalDevice(MediaPlayerEntity):
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def _try_reconnect(connect: ConnectChange):
|
||||
_LOGGER.error(
|
||||
"Got disconnected with %s, trying to reconnect.", connect.exception
|
||||
_LOGGER.warning(
|
||||
"[%s(%s)] Got disconnected, trying to reconnect.",
|
||||
self.name,
|
||||
self._dev.endpoint,
|
||||
)
|
||||
_LOGGER.debug("Disconnected: %s", connect.exception)
|
||||
self._available = False
|
||||
self.dev.clear_notification_callbacks()
|
||||
self.async_write_ha_state()
|
||||
|
||||
# Try to reconnect forever, a successful reconnect will initialize
|
||||
# the websocket connection again.
|
||||
delay = 10
|
||||
delay = INITIAL_RETRY_DELAY
|
||||
while not self._available:
|
||||
_LOGGER.debug("Trying to reconnect in %s seconds", delay)
|
||||
await asyncio.sleep(delay)
|
||||
# We need to inform HA about the state in case we are coming
|
||||
# back from a disconnected state.
|
||||
await self.async_update_ha_state(force_refresh=True)
|
||||
delay = min(2 * delay, 300)
|
||||
|
||||
_LOGGER.info("Reconnected to %s", self.name)
|
||||
try:
|
||||
await self._dev.get_supported_methods()
|
||||
except SongpalException as ex:
|
||||
_LOGGER.debug("Failed to reconnect: %s", ex)
|
||||
delay = min(2 * delay, 300)
|
||||
else:
|
||||
# We need to inform HA about the state in case we are coming
|
||||
# back from a disconnected state.
|
||||
await self.async_update_ha_state(force_refresh=True)
|
||||
|
||||
self.dev.on_notification(VolumeChange, _volume_changed)
|
||||
self.dev.on_notification(ContentChange, _source_changed)
|
||||
self.dev.on_notification(PowerChange, _power_changed)
|
||||
self.dev.on_notification(ConnectChange, _try_reconnect)
|
||||
self.hass.loop.create_task(self._dev.listen_notifications())
|
||||
_LOGGER.warning(
|
||||
"[%s(%s)] Connection reestablished.", self.name, self._dev.endpoint
|
||||
)
|
||||
|
||||
async def listen_events():
|
||||
await self.dev.listen_notifications()
|
||||
self._dev.on_notification(VolumeChange, _volume_changed)
|
||||
self._dev.on_notification(ContentChange, _source_changed)
|
||||
self._dev.on_notification(PowerChange, _power_changed)
|
||||
self._dev.on_notification(ConnectChange, _try_reconnect)
|
||||
|
||||
async def handle_stop(event):
|
||||
await self.dev.stop_listen_notifications()
|
||||
await self._dev.stop_listen_notifications()
|
||||
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop)
|
||||
|
||||
self.hass.loop.create_task(listen_events())
|
||||
self.hass.loop.create_task(self._dev.listen_notifications())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -246,12 +223,20 @@ class SongpalDevice(MediaPlayerEntity):
|
||||
|
||||
async def async_set_sound_setting(self, name, value):
|
||||
"""Change a setting on the device."""
|
||||
await self.dev.set_sound_settings(name, value)
|
||||
_LOGGER.debug("Calling set_sound_setting with %s: %s", name, value)
|
||||
await self._dev.set_sound_settings(name, value)
|
||||
|
||||
async def async_update(self):
|
||||
"""Fetch updates from the device."""
|
||||
try:
|
||||
volumes = await self.dev.get_volume_information()
|
||||
if self._sysinfo is None:
|
||||
self._sysinfo = await self._dev.get_system_info()
|
||||
|
||||
if self._model is None:
|
||||
interface_info = await self._dev.get_interface_information()
|
||||
self._model = interface_info.modelName
|
||||
|
||||
volumes = await self._dev.get_volume_information()
|
||||
if not volumes:
|
||||
_LOGGER.error("Got no volume controls, bailing out")
|
||||
self._available = False
|
||||
@ -269,11 +254,11 @@ class SongpalDevice(MediaPlayerEntity):
|
||||
self._volume_control = volume
|
||||
self._is_muted = self._volume_control.is_muted
|
||||
|
||||
status = await self.dev.get_power()
|
||||
status = await self._dev.get_power()
|
||||
self._state = status.status
|
||||
_LOGGER.debug("Got state: %s", status)
|
||||
|
||||
inputs = await self.dev.get_inputs()
|
||||
inputs = await self._dev.get_inputs()
|
||||
_LOGGER.debug("Got ins: %s", inputs)
|
||||
|
||||
self._sources = OrderedDict()
|
||||
@ -286,9 +271,6 @@ class SongpalDevice(MediaPlayerEntity):
|
||||
|
||||
self._available = True
|
||||
|
||||
# activate notifications if wanted
|
||||
if not self._poll:
|
||||
await self.hass.async_create_task(self.async_activate_websocket())
|
||||
except SongpalException as ex:
|
||||
_LOGGER.error("Unable to update: %s", ex)
|
||||
self._available = False
|
||||
@ -342,11 +324,11 @@ class SongpalDevice(MediaPlayerEntity):
|
||||
|
||||
async def async_turn_on(self):
|
||||
"""Turn the device on."""
|
||||
return await self.dev.set_power(True)
|
||||
return await self._dev.set_power(True)
|
||||
|
||||
async def async_turn_off(self):
|
||||
"""Turn the device off."""
|
||||
return await self.dev.set_power(False)
|
||||
return await self._dev.set_power(False)
|
||||
|
||||
async def async_mute_volume(self, mute):
|
||||
"""Mute or unmute the device."""
|
||||
|
@ -1 +1,104 @@
|
||||
"""Test the songpal integration."""
|
||||
from songpal import SongpalException
|
||||
|
||||
from homeassistant.components.songpal.const import CONF_ENDPOINT
|
||||
from homeassistant.const import CONF_NAME
|
||||
|
||||
from tests.async_mock import AsyncMock, MagicMock, patch
|
||||
|
||||
FRIENDLY_NAME = "name"
|
||||
ENTITY_ID = f"media_player.{FRIENDLY_NAME}"
|
||||
HOST = "0.0.0.0"
|
||||
ENDPOINT = f"http://{HOST}:10000/sony"
|
||||
MODEL = "model"
|
||||
MAC = "mac"
|
||||
SW_VERSION = "sw_ver"
|
||||
|
||||
CONF_DATA = {
|
||||
CONF_NAME: FRIENDLY_NAME,
|
||||
CONF_ENDPOINT: ENDPOINT,
|
||||
}
|
||||
|
||||
|
||||
def _create_mocked_device(throw_exception=False):
|
||||
mocked_device = MagicMock()
|
||||
|
||||
type(mocked_device).get_supported_methods = AsyncMock(
|
||||
side_effect=SongpalException("Unable to do POST request: ")
|
||||
if throw_exception
|
||||
else None
|
||||
)
|
||||
|
||||
interface_info = MagicMock()
|
||||
interface_info.modelName = MODEL
|
||||
type(mocked_device).get_interface_information = AsyncMock(
|
||||
return_value=interface_info
|
||||
)
|
||||
|
||||
sys_info = MagicMock()
|
||||
sys_info.macAddr = MAC
|
||||
sys_info.version = SW_VERSION
|
||||
type(mocked_device).get_system_info = AsyncMock(return_value=sys_info)
|
||||
|
||||
volume1 = MagicMock()
|
||||
volume1.maxVolume = 100
|
||||
volume1.minVolume = 0
|
||||
volume1.volume = 50
|
||||
volume1.is_muted = False
|
||||
volume1.set_volume = AsyncMock()
|
||||
volume1.set_mute = AsyncMock()
|
||||
volume2 = MagicMock()
|
||||
volume2.maxVolume = 100
|
||||
volume2.minVolume = 0
|
||||
volume2.volume = 20
|
||||
volume2.is_muted = True
|
||||
mocked_device.volume1 = volume1
|
||||
type(mocked_device).get_volume_information = AsyncMock(
|
||||
return_value=[volume1, volume2]
|
||||
)
|
||||
|
||||
power = MagicMock()
|
||||
power.status = True
|
||||
type(mocked_device).get_power = AsyncMock(return_value=power)
|
||||
|
||||
input1 = MagicMock()
|
||||
input1.title = "title1"
|
||||
input1.uri = "uri1"
|
||||
input1.active = False
|
||||
input1.activate = AsyncMock()
|
||||
mocked_device.input1 = input1
|
||||
input2 = MagicMock()
|
||||
input2.title = "title2"
|
||||
input2.uri = "uri2"
|
||||
input2.active = True
|
||||
type(mocked_device).get_inputs = AsyncMock(return_value=[input1, input2])
|
||||
|
||||
type(mocked_device).set_power = AsyncMock()
|
||||
type(mocked_device).set_sound_settings = AsyncMock()
|
||||
type(mocked_device).listen_notifications = AsyncMock()
|
||||
type(mocked_device).stop_listen_notifications = AsyncMock()
|
||||
|
||||
notification_callbacks = {}
|
||||
mocked_device.notification_callbacks = notification_callbacks
|
||||
|
||||
def _on_notification(name, callback):
|
||||
notification_callbacks[name] = callback
|
||||
|
||||
type(mocked_device).on_notification = MagicMock(side_effect=_on_notification)
|
||||
type(mocked_device).clear_notification_callbacks = MagicMock()
|
||||
|
||||
return mocked_device
|
||||
|
||||
|
||||
def _patch_config_flow_device(mocked_device):
|
||||
return patch(
|
||||
"homeassistant.components.songpal.config_flow.Device",
|
||||
return_value=mocked_device,
|
||||
)
|
||||
|
||||
|
||||
def _patch_media_player_device(mocked_device):
|
||||
return patch(
|
||||
"homeassistant.components.songpal.media_player.Device",
|
||||
return_value=mocked_device,
|
||||
)
|
||||
|
@ -1,10 +1,6 @@
|
||||
"""Test the songpal config flow."""
|
||||
import copy
|
||||
|
||||
from asynctest import MagicMock, patch
|
||||
from songpal import SongpalException
|
||||
from songpal.containers import InterfaceInfo
|
||||
|
||||
from homeassistant.components import ssdp
|
||||
from homeassistant.components.songpal.const import CONF_ENDPOINT, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER
|
||||
@ -15,13 +11,20 @@ from homeassistant.data_entry_flow import (
|
||||
RESULT_TYPE_FORM,
|
||||
)
|
||||
|
||||
from . import (
|
||||
CONF_DATA,
|
||||
ENDPOINT,
|
||||
FRIENDLY_NAME,
|
||||
HOST,
|
||||
MODEL,
|
||||
_create_mocked_device,
|
||||
_patch_config_flow_device,
|
||||
)
|
||||
|
||||
from tests.async_mock import patch
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
UDN = "uuid:1234"
|
||||
FRIENDLY_NAME = "friendly name"
|
||||
HOST = "0.0.0.0"
|
||||
ENDPOINT = f"http://{HOST}:10000/sony"
|
||||
MODEL = "model"
|
||||
|
||||
SSDP_DATA = {
|
||||
ssdp.ATTR_UPNP_UDN: UDN,
|
||||
@ -35,52 +38,6 @@ SSDP_DATA = {
|
||||
},
|
||||
}
|
||||
|
||||
CONF_DATA = {
|
||||
CONF_NAME: FRIENDLY_NAME,
|
||||
CONF_ENDPOINT: ENDPOINT,
|
||||
}
|
||||
|
||||
|
||||
async def _async_return_value():
|
||||
pass
|
||||
|
||||
|
||||
def _get_supported_methods(throw_exception):
|
||||
def get_supported_methods():
|
||||
if throw_exception:
|
||||
raise SongpalException("Unable to do POST request: ")
|
||||
return _async_return_value()
|
||||
|
||||
return get_supported_methods
|
||||
|
||||
|
||||
async def _get_interface_information():
|
||||
return InterfaceInfo(
|
||||
productName="product name",
|
||||
modelName=MODEL,
|
||||
productCategory="product category",
|
||||
interfaceVersion="interface version",
|
||||
serverName="server name",
|
||||
)
|
||||
|
||||
|
||||
def _create_mocked_device(throw_exception=False):
|
||||
mocked_device = MagicMock()
|
||||
type(mocked_device).get_supported_methods = MagicMock(
|
||||
side_effect=_get_supported_methods(throw_exception)
|
||||
)
|
||||
type(mocked_device).get_interface_information = MagicMock(
|
||||
side_effect=_get_interface_information
|
||||
)
|
||||
return mocked_device
|
||||
|
||||
|
||||
def _patch_config_flow_device(mocked_device):
|
||||
return patch(
|
||||
"homeassistant.components.songpal.config_flow.Device",
|
||||
return_value=mocked_device,
|
||||
)
|
||||
|
||||
|
||||
def _flow_next(hass, flow_id):
|
||||
return next(
|
||||
@ -90,6 +47,12 @@ def _flow_next(hass, flow_id):
|
||||
)
|
||||
|
||||
|
||||
def _patch_setup():
|
||||
return patch(
|
||||
"homeassistant.components.songpal.async_setup_entry", return_value=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_flow_ssdp(hass):
|
||||
"""Test working ssdp flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@ -104,19 +67,20 @@ async def test_flow_ssdp(hass):
|
||||
flow = _flow_next(hass, result["flow_id"])
|
||||
assert flow["context"]["unique_id"] == UDN
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == FRIENDLY_NAME
|
||||
assert result["data"] == CONF_DATA
|
||||
with _patch_setup():
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == FRIENDLY_NAME
|
||||
assert result["data"] == CONF_DATA
|
||||
|
||||
|
||||
async def test_flow_user(hass):
|
||||
"""Test working user initialized flow."""
|
||||
mocked_device = _create_mocked_device()
|
||||
|
||||
with _patch_config_flow_device(mocked_device):
|
||||
with _patch_config_flow_device(mocked_device), _patch_setup():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER},
|
||||
)
|
||||
@ -143,7 +107,7 @@ async def test_flow_import(hass):
|
||||
"""Test working import flow."""
|
||||
mocked_device = _create_mocked_device()
|
||||
|
||||
with _patch_config_flow_device(mocked_device):
|
||||
with _patch_config_flow_device(mocked_device), _patch_setup():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA
|
||||
)
|
||||
@ -155,6 +119,22 @@ async def test_flow_import(hass):
|
||||
mocked_device.get_interface_information.assert_not_called()
|
||||
|
||||
|
||||
async def test_flow_import_without_name(hass):
|
||||
"""Test import flow without optional name."""
|
||||
mocked_device = _create_mocked_device()
|
||||
|
||||
with _patch_config_flow_device(mocked_device), _patch_setup():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_ENDPOINT: ENDPOINT}
|
||||
)
|
||||
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == MODEL
|
||||
assert result["data"] == {CONF_NAME: MODEL, CONF_ENDPOINT: ENDPOINT}
|
||||
|
||||
mocked_device.get_supported_methods.assert_called_once()
|
||||
mocked_device.get_interface_information.assert_called_once()
|
||||
|
||||
|
||||
def _create_mock_config_entry(hass):
|
||||
MockConfigEntry(domain=DOMAIN, unique_id="uuid:0000", data=CONF_DATA,).add_to_hass(
|
||||
hass
|
||||
|
66
tests/components/songpal/test_init.py
Normal file
66
tests/components/songpal/test_init.py
Normal file
@ -0,0 +1,66 @@
|
||||
"""Tests songpal setup."""
|
||||
from homeassistant.components import songpal
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import (
|
||||
CONF_DATA,
|
||||
_create_mocked_device,
|
||||
_patch_config_flow_device,
|
||||
_patch_media_player_device,
|
||||
)
|
||||
|
||||
from tests.async_mock import patch
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
def _patch_media_setup():
|
||||
"""Patch media_player.async_setup_entry."""
|
||||
|
||||
async def _async_return():
|
||||
return True
|
||||
|
||||
return patch(
|
||||
"homeassistant.components.songpal.media_player.async_setup_entry",
|
||||
side_effect=_async_return,
|
||||
)
|
||||
|
||||
|
||||
async def test_setup_empty(hass):
|
||||
"""Test setup without any configuration."""
|
||||
with _patch_media_setup() as setup:
|
||||
assert await async_setup_component(hass, songpal.DOMAIN, {}) is True
|
||||
await hass.async_block_till_done()
|
||||
setup.assert_not_called()
|
||||
|
||||
|
||||
async def test_setup(hass):
|
||||
"""Test setup the platform."""
|
||||
mocked_device = _create_mocked_device()
|
||||
|
||||
with _patch_config_flow_device(mocked_device), _patch_media_setup() as setup:
|
||||
assert (
|
||||
await async_setup_component(
|
||||
hass, songpal.DOMAIN, {songpal.DOMAIN: [CONF_DATA]}
|
||||
)
|
||||
is True
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mocked_device.get_supported_methods.assert_called_once()
|
||||
setup.assert_called_once()
|
||||
|
||||
|
||||
async def test_unload(hass):
|
||||
"""Test unload entity."""
|
||||
entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA)
|
||||
entry.add_to_hass(hass)
|
||||
mocked_device = _create_mocked_device()
|
||||
|
||||
with _patch_config_flow_device(mocked_device), _patch_media_player_device(
|
||||
mocked_device
|
||||
):
|
||||
assert await async_setup_component(hass, songpal.DOMAIN, {}) is True
|
||||
await hass.async_block_till_done()
|
||||
mocked_device.listen_notifications.assert_called_once()
|
||||
assert await songpal.async_unload_entry(hass, entry)
|
||||
await hass.async_block_till_done()
|
||||
mocked_device.stop_listen_notifications.assert_called_once()
|
267
tests/components/songpal/test_media_player.py
Normal file
267
tests/components/songpal/test_media_player.py
Normal file
@ -0,0 +1,267 @@
|
||||
"""Test songpal media_player."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from songpal import (
|
||||
ConnectChange,
|
||||
ContentChange,
|
||||
PowerChange,
|
||||
SongpalException,
|
||||
VolumeChange,
|
||||
)
|
||||
|
||||
from homeassistant.components import media_player, songpal
|
||||
from homeassistant.components.songpal.const import SET_SOUND_SETTING
|
||||
from homeassistant.components.songpal.media_player import SUPPORT_SONGPAL
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import (
|
||||
CONF_DATA,
|
||||
CONF_ENDPOINT,
|
||||
CONF_NAME,
|
||||
ENDPOINT,
|
||||
ENTITY_ID,
|
||||
FRIENDLY_NAME,
|
||||
MAC,
|
||||
MODEL,
|
||||
SW_VERSION,
|
||||
_create_mocked_device,
|
||||
_patch_media_player_device,
|
||||
)
|
||||
|
||||
from tests.async_mock import AsyncMock, MagicMock, call, patch
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
def _get_attributes(hass):
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
return state.as_dict()["attributes"]
|
||||
|
||||
|
||||
async def test_setup_platform(hass):
|
||||
"""Test the legacy setup platform."""
|
||||
mocked_device = _create_mocked_device(throw_exception=True)
|
||||
with _patch_media_player_device(mocked_device):
|
||||
await async_setup_component(
|
||||
hass,
|
||||
media_player.DOMAIN,
|
||||
{
|
||||
media_player.DOMAIN: [
|
||||
{
|
||||
"platform": songpal.DOMAIN,
|
||||
CONF_NAME: FRIENDLY_NAME,
|
||||
CONF_ENDPOINT: ENDPOINT,
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# No device is set up
|
||||
mocked_device.assert_not_called()
|
||||
all_states = hass.states.async_all()
|
||||
assert len(all_states) == 0
|
||||
|
||||
|
||||
async def test_setup_failed(hass, caplog):
|
||||
"""Test failed to set up the entity."""
|
||||
mocked_device = _create_mocked_device(throw_exception=True)
|
||||
entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with _patch_media_player_device(mocked_device):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
all_states = hass.states.async_all()
|
||||
assert len(all_states) == 0
|
||||
warning_records = [x for x in caplog.records if x.levelno == logging.WARNING]
|
||||
assert len(warning_records) == 2
|
||||
assert not any(x.levelno == logging.ERROR for x in caplog.records)
|
||||
caplog.clear()
|
||||
|
||||
utcnow = dt_util.utcnow()
|
||||
type(mocked_device).get_supported_methods = AsyncMock()
|
||||
with _patch_media_player_device(mocked_device):
|
||||
async_fire_time_changed(hass, utcnow + timedelta(seconds=30))
|
||||
await hass.async_block_till_done()
|
||||
all_states = hass.states.async_all()
|
||||
assert len(all_states) == 1
|
||||
assert not any(x.levelno == logging.WARNING for x in caplog.records)
|
||||
assert not any(x.levelno == logging.ERROR for x in caplog.records)
|
||||
|
||||
|
||||
async def test_state(hass):
|
||||
"""Test state of the entity."""
|
||||
mocked_device = _create_mocked_device()
|
||||
entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with _patch_media_player_device(mocked_device):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.name == FRIENDLY_NAME
|
||||
assert state.state == STATE_ON
|
||||
attributes = state.as_dict()["attributes"]
|
||||
assert attributes["volume_level"] == 0.5
|
||||
assert attributes["is_volume_muted"] is False
|
||||
assert attributes["source_list"] == ["title1", "title2"]
|
||||
assert attributes["source"] == "title2"
|
||||
assert attributes["supported_features"] == SUPPORT_SONGPAL
|
||||
|
||||
device_registry = await dr.async_get_registry(hass)
|
||||
device = device_registry.async_get_device(
|
||||
identifiers={(songpal.DOMAIN, MAC)}, connections={}
|
||||
)
|
||||
assert device.connections == {(dr.CONNECTION_NETWORK_MAC, MAC)}
|
||||
assert device.manufacturer == "Sony Corporation"
|
||||
assert device.name == FRIENDLY_NAME
|
||||
assert device.sw_version == SW_VERSION
|
||||
assert device.model == MODEL
|
||||
|
||||
entity_registry = await er.async_get_registry(hass)
|
||||
entity = entity_registry.async_get(ENTITY_ID)
|
||||
assert entity.unique_id == MAC
|
||||
|
||||
|
||||
async def test_services(hass):
|
||||
"""Test services."""
|
||||
mocked_device = _create_mocked_device()
|
||||
entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with _patch_media_player_device(mocked_device):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
async def _call(service, **argv):
|
||||
await hass.services.async_call(
|
||||
media_player.DOMAIN,
|
||||
service,
|
||||
{"entity_id": ENTITY_ID, **argv},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
await _call(media_player.SERVICE_TURN_ON)
|
||||
await _call(media_player.SERVICE_TURN_OFF)
|
||||
await _call(media_player.SERVICE_TOGGLE)
|
||||
assert mocked_device.set_power.call_count == 3
|
||||
mocked_device.set_power.assert_has_calls([call(True), call(False), call(False)])
|
||||
|
||||
await _call(media_player.SERVICE_VOLUME_SET, volume_level=0.6)
|
||||
await _call(media_player.SERVICE_VOLUME_UP)
|
||||
await _call(media_player.SERVICE_VOLUME_DOWN)
|
||||
assert mocked_device.volume1.set_volume.call_count == 3
|
||||
mocked_device.volume1.set_volume.assert_has_calls(
|
||||
[call(60), call("+1"), call("-1")]
|
||||
)
|
||||
|
||||
await _call(media_player.SERVICE_VOLUME_MUTE, is_volume_muted=True)
|
||||
mocked_device.volume1.set_mute.assert_called_once_with(True)
|
||||
|
||||
await _call(media_player.SERVICE_SELECT_SOURCE, source="none")
|
||||
mocked_device.input1.activate.assert_not_called()
|
||||
await _call(media_player.SERVICE_SELECT_SOURCE, source="title1")
|
||||
mocked_device.input1.activate.assert_called_once()
|
||||
|
||||
await hass.services.async_call(
|
||||
songpal.DOMAIN,
|
||||
SET_SOUND_SETTING,
|
||||
{"entity_id": ENTITY_ID, "name": "name", "value": "value"},
|
||||
blocking=True,
|
||||
)
|
||||
mocked_device.set_sound_settings.assert_called_once_with("name", "value")
|
||||
mocked_device.set_sound_settings.reset_mock()
|
||||
|
||||
mocked_device2 = _create_mocked_device()
|
||||
sys_info = MagicMock()
|
||||
sys_info.macAddr = "mac2"
|
||||
sys_info.version = SW_VERSION
|
||||
type(mocked_device2).get_system_info = AsyncMock(return_value=sys_info)
|
||||
entry2 = MockConfigEntry(
|
||||
domain=songpal.DOMAIN, data={CONF_NAME: "d2", CONF_ENDPOINT: ENDPOINT}
|
||||
)
|
||||
entry2.add_to_hass(hass)
|
||||
with _patch_media_player_device(mocked_device2):
|
||||
await hass.config_entries.async_setup(entry2.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
songpal.DOMAIN,
|
||||
SET_SOUND_SETTING,
|
||||
{"entity_id": "all", "name": "name", "value": "value"},
|
||||
blocking=True,
|
||||
)
|
||||
mocked_device.set_sound_settings.assert_called_once_with("name", "value")
|
||||
mocked_device2.set_sound_settings.assert_called_once_with("name", "value")
|
||||
|
||||
|
||||
async def test_websocket_events(hass):
|
||||
"""Test websocket events."""
|
||||
mocked_device = _create_mocked_device()
|
||||
entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with _patch_media_player_device(mocked_device):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mocked_device.listen_notifications.assert_called_once()
|
||||
assert mocked_device.on_notification.call_count == 4
|
||||
|
||||
notification_callbacks = mocked_device.notification_callbacks
|
||||
|
||||
volume_change = MagicMock()
|
||||
volume_change.mute = True
|
||||
volume_change.volume = 20
|
||||
await notification_callbacks[VolumeChange](volume_change)
|
||||
attributes = _get_attributes(hass)
|
||||
assert attributes["is_volume_muted"] is True
|
||||
assert attributes["volume_level"] == 0.2
|
||||
|
||||
content_change = MagicMock()
|
||||
content_change.is_input = False
|
||||
content_change.uri = "uri1"
|
||||
await notification_callbacks[ContentChange](content_change)
|
||||
assert _get_attributes(hass)["source"] == "title2"
|
||||
content_change.is_input = True
|
||||
await notification_callbacks[ContentChange](content_change)
|
||||
assert _get_attributes(hass)["source"] == "title1"
|
||||
|
||||
power_change = MagicMock()
|
||||
power_change.status = False
|
||||
await notification_callbacks[PowerChange](power_change)
|
||||
assert hass.states.get(ENTITY_ID).state == STATE_OFF
|
||||
|
||||
|
||||
async def test_disconnected(hass, caplog):
|
||||
"""Test disconnected behavior."""
|
||||
mocked_device = _create_mocked_device()
|
||||
entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with _patch_media_player_device(mocked_device):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
async def _assert_state():
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
connect_change = MagicMock()
|
||||
connect_change.exception = "disconnected"
|
||||
type(mocked_device).get_supported_methods = AsyncMock(
|
||||
side_effect=[SongpalException(""), SongpalException(""), _assert_state]
|
||||
)
|
||||
notification_callbacks = mocked_device.notification_callbacks
|
||||
with patch("homeassistant.components.songpal.media_player.INITIAL_RETRY_DELAY", 0):
|
||||
await notification_callbacks[ConnectChange](connect_change)
|
||||
warning_records = [x for x in caplog.records if x.levelno == logging.WARNING]
|
||||
assert len(warning_records) == 2
|
||||
assert warning_records[0].message.endswith("Got disconnected, trying to reconnect.")
|
||||
assert warning_records[1].message.endswith("Connection reestablished.")
|
||||
assert not any(x.levelno == logging.ERROR for x in caplog.records)
|
Loading…
x
Reference in New Issue
Block a user