Config Flow and Entity registry support for Monoprice (#30337)

* Entity registry support for monoprice

* Add test for unique_id

* Add unique id namespace to monoprice

* Config Flow for Monoprice

* Update monoprice tests

* Remove TODOs

* Handle entity unloading

* Fix update test

* Streamline entity handling in monoprice services

* Increase coverage

* Remove devices cache

* Async validation in monoprice config flow
This commit is contained in:
On Freund 2020-03-22 03:12:32 +02:00 committed by GitHub
parent 99d732b974
commit e8bd1b9216
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 614 additions and 454 deletions

View file

@ -1 +1,36 @@
"""The monoprice component.""" """The Monoprice 6-Zone Amplifier integration."""
import asyncio
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
PLATFORMS = ["media_player"]
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Monoprice 6-Zone Amplifier component."""
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Monoprice 6-Zone Amplifier from a config entry."""
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
return unload_ok

View file

@ -0,0 +1,95 @@
"""Config flow for Monoprice 6-Zone Amplifier integration."""
import logging
from pymonoprice import get_async_monoprice
from serial import SerialException
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_PORT
from .const import (
CONF_SOURCE_1,
CONF_SOURCE_2,
CONF_SOURCE_3,
CONF_SOURCE_4,
CONF_SOURCE_5,
CONF_SOURCE_6,
CONF_SOURCES,
)
from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_PORT): str,
vol.Optional(CONF_SOURCE_1): str,
vol.Optional(CONF_SOURCE_2): str,
vol.Optional(CONF_SOURCE_3): str,
vol.Optional(CONF_SOURCE_4): str,
vol.Optional(CONF_SOURCE_5): str,
vol.Optional(CONF_SOURCE_6): str,
}
)
async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
try:
await get_async_monoprice(data[CONF_PORT], hass.loop)
except SerialException:
_LOGGER.error("Error connecting to Monoprice controller")
raise CannotConnect
sources_config = {
1: data.get(CONF_SOURCE_1),
2: data.get(CONF_SOURCE_2),
3: data.get(CONF_SOURCE_3),
4: data.get(CONF_SOURCE_4),
5: data.get(CONF_SOURCE_5),
6: data.get(CONF_SOURCE_6),
}
sources = {
index: name.strip()
for index, name in sources_config.items()
if (name is not None and name.strip() != "")
}
# Return info that you want to store in the config entry.
return {CONF_PORT: data[CONF_PORT], CONF_SOURCES: sources}
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Monoprice 6-Zone Amplifier."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
try:
info = await validate_input(self.hass, user_input)
return self.async_create_entry(title=user_input[CONF_PORT], data=info)
except CannotConnect:
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""

View file

@ -1,5 +1,15 @@
"""Constants for the Monoprice 6-Zone Amplifier Media Player component.""" """Constants for the Monoprice 6-Zone Amplifier Media Player component."""
DOMAIN = "monoprice" DOMAIN = "monoprice"
CONF_SOURCES = "sources"
CONF_SOURCE_1 = "source_1"
CONF_SOURCE_2 = "source_2"
CONF_SOURCE_3 = "source_3"
CONF_SOURCE_4 = "source_4"
CONF_SOURCE_5 = "source_5"
CONF_SOURCE_6 = "source_6"
SERVICE_SNAPSHOT = "snapshot" SERVICE_SNAPSHOT = "snapshot"
SERVICE_RESTORE = "restore" SERVICE_RESTORE = "restore"

View file

@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/monoprice", "documentation": "https://www.home-assistant.io/integrations/monoprice",
"requirements": ["pymonoprice==0.3"], "requirements": ["pymonoprice==0.3"],
"dependencies": [], "dependencies": [],
"codeowners": ["@etsinko"] "codeowners": ["@etsinko"],
"config_flow": true
} }

View file

@ -3,9 +3,8 @@ import logging
from pymonoprice import get_monoprice from pymonoprice import get_monoprice
from serial import SerialException from serial import SerialException
import voluptuous as vol
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player import MediaPlayerDevice
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
SUPPORT_SELECT_SOURCE, SUPPORT_SELECT_SOURCE,
SUPPORT_TURN_OFF, SUPPORT_TURN_OFF,
@ -14,16 +13,10 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_STEP,
) )
from homeassistant.const import ( from homeassistant.const import CONF_PORT, STATE_OFF, STATE_ON
ATTR_ENTITY_ID, from homeassistant.helpers import config_validation as cv, entity_platform, service
CONF_NAME,
CONF_PORT,
STATE_OFF,
STATE_ON,
)
import homeassistant.helpers.config_validation as cv
from .const import DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT from .const import CONF_SOURCES, DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -36,104 +29,89 @@ SUPPORT_MONOPRICE = (
| SUPPORT_SELECT_SOURCE | SUPPORT_SELECT_SOURCE
) )
ZONE_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string})
SOURCE_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) def _get_sources(sources_config):
source_id_name = {int(index): name for index, name in sources_config.items()}
CONF_ZONES = "zones" source_name_id = {v: k for k, v in source_id_name.items()}
CONF_SOURCES = "sources"
DATA_MONOPRICE = "monoprice" source_names = sorted(source_name_id.keys(), key=lambda v: source_name_id[v])
# Valid zone ids: 11-16 or 21-26 or 31-36 return [source_id_name, source_name_id, source_names]
ZONE_IDS = vol.All(
vol.Coerce(int),
vol.Any(
vol.Range(min=11, max=16), vol.Range(min=21, max=26), vol.Range(min=31, max=36)
),
)
# Valid source ids: 1-6
SOURCE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=6))
MEDIA_PLAYER_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.comp_entity_ids})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_PORT): cv.string,
vol.Required(CONF_ZONES): vol.Schema({ZONE_IDS: ZONE_SCHEMA}),
vol.Required(CONF_SOURCES): vol.Schema({SOURCE_IDS: SOURCE_SCHEMA}),
}
)
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up the Monoprice 6-zone amplifier platform.""" """Set up the Monoprice 6-zone amplifier platform."""
port = config.get(CONF_PORT) port = config_entry.data.get(CONF_PORT)
try: try:
monoprice = get_monoprice(port) monoprice = await hass.async_add_executor_job(get_monoprice, port)
except SerialException: except SerialException:
_LOGGER.error("Error connecting to Monoprice controller") _LOGGER.error("Error connecting to Monoprice controller")
return return
sources = { sources = _get_sources(config_entry.data.get(CONF_SOURCES))
source_id: extra[CONF_NAME] for source_id, extra in config[CONF_SOURCES].items()
}
hass.data[DATA_MONOPRICE] = [] devices = []
for zone_id, extra in config[CONF_ZONES].items(): for i in range(1, 4):
_LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME]) for j in range(1, 7):
hass.data[DATA_MONOPRICE].append( zone_id = (i * 10) + j
MonopriceZone(monoprice, sources, zone_id, extra[CONF_NAME]) _LOGGER.info("Adding zone %d for port %s", zone_id, port)
) devices.append(
MonopriceZone(monoprice, sources, config_entry.entry_id, zone_id)
)
add_entities(hass.data[DATA_MONOPRICE], True) async_add_devices(devices, True)
def service_handle(service): platform = entity_platform.current_platform.get()
def _call_service(entities, service_call):
for entity in entities:
if service_call.service == SERVICE_SNAPSHOT:
entity.snapshot()
elif service_call.service == SERVICE_RESTORE:
entity.restore()
@service.verify_domain_control(hass, DOMAIN)
async def async_service_handle(service_call):
"""Handle for services.""" """Handle for services."""
entity_ids = service.data.get(ATTR_ENTITY_ID) entities = await platform.async_extract_from_service(service_call)
if entity_ids: if not entities:
devices = [ return
device
for device in hass.data[DATA_MONOPRICE]
if device.entity_id in entity_ids
]
else:
devices = hass.data[DATA_MONOPRICE]
for device in devices: hass.async_add_executor_job(_call_service, entities, service_call)
if service.service == SERVICE_SNAPSHOT:
device.snapshot()
elif service.service == SERVICE_RESTORE:
device.restore()
hass.services.register( hass.services.async_register(
DOMAIN, SERVICE_SNAPSHOT, service_handle, schema=MEDIA_PLAYER_SCHEMA DOMAIN,
SERVICE_SNAPSHOT,
async_service_handle,
schema=cv.make_entity_service_schema({}),
) )
hass.services.register( hass.services.async_register(
DOMAIN, SERVICE_RESTORE, service_handle, schema=MEDIA_PLAYER_SCHEMA DOMAIN,
SERVICE_RESTORE,
async_service_handle,
schema=cv.make_entity_service_schema({}),
) )
class MonopriceZone(MediaPlayerDevice): class MonopriceZone(MediaPlayerDevice):
"""Representation of a Monoprice amplifier zone.""" """Representation of a Monoprice amplifier zone."""
def __init__(self, monoprice, sources, zone_id, zone_name): def __init__(self, monoprice, sources, namespace, zone_id):
"""Initialize new zone.""" """Initialize new zone."""
self._monoprice = monoprice self._monoprice = monoprice
# dict source_id -> source name # dict source_id -> source name
self._source_id_name = sources self._source_id_name = sources[0]
# dict source name -> source_id # dict source name -> source_id
self._source_name_id = {v: k for k, v in sources.items()} self._source_name_id = sources[1]
# ordered list of all source names # ordered list of all source names
self._source_names = sorted( self._source_names = sources[2]
self._source_name_id.keys(), key=lambda v: self._source_name_id[v]
)
self._zone_id = zone_id self._zone_id = zone_id
self._name = zone_name self._unique_id = f"{namespace}_{self._zone_id}"
self._name = f"Zone {self._zone_id}"
self._snapshot = None self._snapshot = None
self._state = None self._state = None
@ -156,6 +134,26 @@ class MonopriceZone(MediaPlayerDevice):
self._source = None self._source = None
return True return True
@property
def entity_registry_enabled_default(self):
"""Return if the entity should be enabled when first added to the entity registry."""
return self._zone_id < 20
@property
def device_info(self):
"""Return device info for this device."""
return {
"identifiers": {(DOMAIN, self.unique_id)},
"name": self.name,
"manufacturer": "Monoprice",
"model": "6-Zone Amplifier",
}
@property
def unique_id(self):
"""Return unique ID for this device."""
return self._unique_id
@property @property
def name(self): def name(self):
"""Return the name of the zone.""" """Return the name of the zone."""

View file

@ -0,0 +1,26 @@
{
"config": {
"title": "Monoprice 6-Zone Amplifier",
"step": {
"user": {
"title": "Connect to the device",
"data": {
"port": "Serial port",
"source_1": "Name of source #1",
"source_2": "Name of source #2",
"source_3": "Name of source #3",
"source_4": "Name of source #4",
"source_5": "Name of source #5",
"source_6": "Name of source #6"
}
}
},
"error": {
"cannot_connect": "Failed to connect, please try again",
"unknown": "Unexpected error"
},
"abort": {
"already_configured": "Device is already configured"
}
}
}

View file

@ -69,6 +69,7 @@ FLOWS = [
"mikrotik", "mikrotik",
"minecraft_server", "minecraft_server",
"mobile_app", "mobile_app",
"monoprice",
"mqtt", "mqtt",
"myq", "myq",
"neato", "neato",

View file

@ -0,0 +1,88 @@
"""Test the Monoprice 6-Zone Amplifier config flow."""
from asynctest import patch
from serial import SerialException
from homeassistant import config_entries, setup
from homeassistant.components.monoprice.const import (
CONF_SOURCE_1,
CONF_SOURCE_4,
CONF_SOURCE_5,
CONF_SOURCES,
DOMAIN,
)
from homeassistant.const import CONF_PORT
CONFIG = {
CONF_PORT: "/test/port",
CONF_SOURCE_1: "one",
CONF_SOURCE_4: "four",
CONF_SOURCE_5: " ",
}
async def test_form(hass):
"""Test we get the form."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
with patch(
"homeassistant.components.monoprice.config_flow.get_async_monoprice",
return_value=True,
), patch(
"homeassistant.components.monoprice.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.monoprice.async_setup_entry", return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], CONFIG
)
assert result2["type"] == "create_entry"
assert result2["title"] == CONFIG[CONF_PORT]
assert result2["data"] == {
CONF_PORT: CONFIG[CONF_PORT],
CONF_SOURCES: {1: CONFIG[CONF_SOURCE_1], 4: CONFIG[CONF_SOURCE_4]},
}
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_cannot_connect(hass):
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.monoprice.config_flow.get_async_monoprice",
side_effect=SerialException,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], CONFIG
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "cannot_connect"}
async def test_generic_exception(hass):
"""Test we handle cannot generic exception."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.monoprice.config_flow.get_async_monoprice",
side_effect=Exception,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], CONFIG
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "unknown"}

View file

@ -1,12 +1,15 @@
"""The tests for Monoprice Media player platform.""" """The tests for Monoprice Media player platform."""
from collections import defaultdict from collections import defaultdict
import unittest
from unittest import mock
import pytest from asynctest import patch
import voluptuous as vol from serial import SerialException
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
ATTR_INPUT_SOURCE,
ATTR_INPUT_SOURCE_LIST,
ATTR_MEDIA_VOLUME_LEVEL,
DOMAIN as MEDIA_PLAYER_DOMAIN,
SERVICE_SELECT_SOURCE,
SUPPORT_SELECT_SOURCE, SUPPORT_SELECT_SOURCE,
SUPPORT_TURN_OFF, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, SUPPORT_TURN_ON,
@ -15,18 +18,28 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_STEP,
) )
from homeassistant.components.monoprice.const import ( from homeassistant.components.monoprice.const import (
CONF_SOURCES,
DOMAIN, DOMAIN,
SERVICE_RESTORE, SERVICE_RESTORE,
SERVICE_SNAPSHOT, SERVICE_SNAPSHOT,
) )
from homeassistant.components.monoprice.media_player import ( from homeassistant.const import (
DATA_MONOPRICE, CONF_PORT,
PLATFORM_SCHEMA, SERVICE_TURN_OFF,
setup_platform, SERVICE_TURN_ON,
SERVICE_VOLUME_DOWN,
SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET,
SERVICE_VOLUME_UP,
) )
from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity_component import async_update_entity
import tests.common from tests.common import MockConfigEntry
MOCK_CONFIG = {CONF_PORT: "fake port", CONF_SOURCES: {"1": "one", "3": "three"}}
ZONE_1_ID = "media_player.zone_11"
ZONE_2_ID = "media_player.zone_12"
class AttrDict(dict): class AttrDict(dict):
@ -77,426 +90,319 @@ class MockMonoprice:
self.zones[zone.zone] = AttrDict(zone) self.zones[zone.zone] = AttrDict(zone)
class TestMonopriceSchema(unittest.TestCase): async def test_cannot_connect(hass):
"""Test Monoprice schema.""" """Test connection error."""
def test_valid_schema(self): with patch(
"""Test valid schema.""" "homeassistant.components.monoprice.media_player.get_monoprice",
valid_schema = { side_effect=SerialException,
"platform": "monoprice", ):
"port": "/dev/ttyUSB0", config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG)
"zones": { config_entry.add_to_hass(hass)
11: {"name": "a"}, await hass.config_entries.async_setup(config_entry.entry_id)
12: {"name": "a"}, # setup_component(self.hass, DOMAIN, MOCK_CONFIG)
13: {"name": "a"}, # self.hass.async_block_till_done()
14: {"name": "a"}, await hass.async_block_till_done()
15: {"name": "a"}, assert hass.states.get(ZONE_1_ID) is None
16: {"name": "a"},
21: {"name": "a"},
22: {"name": "a"},
23: {"name": "a"},
24: {"name": "a"},
25: {"name": "a"},
26: {"name": "a"},
31: {"name": "a"},
32: {"name": "a"},
33: {"name": "a"},
34: {"name": "a"},
35: {"name": "a"},
36: {"name": "a"},
},
"sources": {
1: {"name": "a"},
2: {"name": "a"},
3: {"name": "a"},
4: {"name": "a"},
5: {"name": "a"},
6: {"name": "a"},
},
}
PLATFORM_SCHEMA(valid_schema)
def test_invalid_schemas(self):
"""Test invalid schemas."""
schemas = (
{}, # Empty
None, # None
# Missing port
{
"platform": "monoprice",
"name": "Name",
"zones": {11: {"name": "a"}},
"sources": {1: {"name": "b"}},
},
# Invalid zone number
{
"platform": "monoprice",
"port": "aaa",
"name": "Name",
"zones": {10: {"name": "a"}},
"sources": {1: {"name": "b"}},
},
# Invalid source number
{
"platform": "monoprice",
"port": "aaa",
"name": "Name",
"zones": {11: {"name": "a"}},
"sources": {0: {"name": "b"}},
},
# Zone missing name
{
"platform": "monoprice",
"port": "aaa",
"name": "Name",
"zones": {11: {}},
"sources": {1: {"name": "b"}},
},
# Source missing name
{
"platform": "monoprice",
"port": "aaa",
"name": "Name",
"zones": {11: {"name": "a"}},
"sources": {1: {}},
},
)
for value in schemas:
with pytest.raises(vol.MultipleInvalid):
PLATFORM_SCHEMA(value)
class TestMonopriceMediaPlayer(unittest.TestCase): async def _setup_monoprice(hass, monoprice):
"""Test the media_player module.""" with patch(
"homeassistant.components.monoprice.media_player.get_monoprice",
new=lambda *a: monoprice,
):
config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
# setup_component(self.hass, DOMAIN, MOCK_CONFIG)
# self.hass.async_block_till_done()
await hass.async_block_till_done()
def setUp(self):
"""Set up the test case."""
self.monoprice = MockMonoprice()
self.hass = tests.common.get_test_home_assistant()
self.hass.start()
# Note, source dictionary is unsorted!
with mock.patch(
"homeassistant.components.monoprice.media_player.get_monoprice",
new=lambda *a: self.monoprice,
):
setup_platform(
self.hass,
{
"platform": "monoprice",
"port": "/dev/ttyS0",
"name": "Name",
"zones": {12: {"name": "Zone name"}},
"sources": {
1: {"name": "one"},
3: {"name": "three"},
2: {"name": "two"},
},
},
lambda *args, **kwargs: None,
{},
)
self.hass.block_till_done()
self.media_player = self.hass.data[DATA_MONOPRICE][0]
self.media_player.hass = self.hass
self.media_player.entity_id = "media_player.zone_1"
def tearDown(self): async def _call_media_player_service(hass, name, data):
"""Tear down the test case.""" await hass.services.async_call(
self.hass.stop() MEDIA_PLAYER_DOMAIN, name, service_data=data, blocking=True
)
def test_setup_platform(self, *args):
"""Test setting up platform."""
# Two services must be registered
assert self.hass.services.has_service(DOMAIN, SERVICE_RESTORE)
assert self.hass.services.has_service(DOMAIN, SERVICE_SNAPSHOT)
assert len(self.hass.data[DATA_MONOPRICE]) == 1
assert self.hass.data[DATA_MONOPRICE][0].name == "Zone name"
def test_service_calls_with_entity_id(self): async def _call_homeassistant_service(hass, name, data):
"""Test snapshot save/restore service calls.""" await hass.services.async_call(
self.media_player.update() "homeassistant", name, service_data=data, blocking=True
assert "Zone name" == self.media_player.name )
assert STATE_ON == self.media_player.state
assert 0.0 == self.media_player.volume_level, 0.0001
assert self.media_player.is_volume_muted
assert "one" == self.media_player.source
# Saving default values
self.hass.services.call(
DOMAIN,
SERVICE_SNAPSHOT,
{"entity_id": "media_player.zone_1"},
blocking=True,
)
# self.hass.block_till_done()
# Changing media player to new state async def _call_monoprice_service(hass, name, data):
self.media_player.set_volume_level(1) await hass.services.async_call(DOMAIN, name, service_data=data, blocking=True)
self.media_player.select_source("two")
self.media_player.mute_volume(False)
self.media_player.turn_off()
# Checking that values were indeed changed
self.media_player.update()
assert "Zone name" == self.media_player.name
assert STATE_OFF == self.media_player.state
assert 1.0 == self.media_player.volume_level, 0.0001
assert not self.media_player.is_volume_muted
assert "two" == self.media_player.source
# Restoring wrong media player to its previous state async def test_service_calls_with_entity_id(hass):
# Nothing should be done """Test snapshot save/restore service calls."""
self.hass.services.call( await _setup_monoprice(hass, MockMonoprice())
DOMAIN, SERVICE_RESTORE, {"entity_id": "media.not_existing"}, blocking=True
)
# self.hass.block_till_done()
# Checking that values were not (!) restored # Changing media player to new state
self.media_player.update() await _call_media_player_service(
assert "Zone name" == self.media_player.name hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0}
assert STATE_OFF == self.media_player.state )
assert 1.0 == self.media_player.volume_level, 0.0001 await _call_media_player_service(
assert not self.media_player.is_volume_muted hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"}
assert "two" == self.media_player.source )
# Restoring media player to its previous state # Saving existing values
self.hass.services.call( await _call_monoprice_service(hass, SERVICE_SNAPSHOT, {"entity_id": ZONE_1_ID})
DOMAIN, SERVICE_RESTORE, {"entity_id": "media_player.zone_1"}, blocking=True
)
self.hass.block_till_done()
# Checking that values were restored # Changing media player to new state
assert "Zone name" == self.media_player.name await _call_media_player_service(
assert STATE_ON == self.media_player.state hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0}
assert 0.0 == self.media_player.volume_level, 0.0001 )
assert self.media_player.is_volume_muted await _call_media_player_service(
assert "one" == self.media_player.source hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "three"}
)
def test_service_calls_without_entity_id(self): # Restoring other media player to its previous state
"""Test snapshot save/restore service calls.""" # The zone should not be restored
self.media_player.update() await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_2_ID})
assert "Zone name" == self.media_player.name await hass.async_block_till_done()
assert STATE_ON == self.media_player.state
assert 0.0 == self.media_player.volume_level, 0.0001
assert self.media_player.is_volume_muted
assert "one" == self.media_player.source
# Restoring media player # Checking that values were not (!) restored
# since there is no snapshot, nothing should be done state = hass.states.get(ZONE_1_ID)
self.hass.services.call(DOMAIN, SERVICE_RESTORE, blocking=True)
self.hass.block_till_done()
self.media_player.update()
assert "Zone name" == self.media_player.name
assert STATE_ON == self.media_player.state
assert 0.0 == self.media_player.volume_level, 0.0001
assert self.media_player.is_volume_muted
assert "one" == self.media_player.source
# Saving default values assert 1.0 == state.attributes[ATTR_MEDIA_VOLUME_LEVEL]
self.hass.services.call(DOMAIN, SERVICE_SNAPSHOT, blocking=True) assert "three" == state.attributes[ATTR_INPUT_SOURCE]
self.hass.block_till_done()
# Changing media player to new state # Restoring media player to its previous state
self.media_player.set_volume_level(1) await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID})
self.media_player.select_source("two") await hass.async_block_till_done()
self.media_player.mute_volume(False)
self.media_player.turn_off()
# Checking that values were indeed changed state = hass.states.get(ZONE_1_ID)
self.media_player.update()
assert "Zone name" == self.media_player.name
assert STATE_OFF == self.media_player.state
assert 1.0 == self.media_player.volume_level, 0.0001
assert not self.media_player.is_volume_muted
assert "two" == self.media_player.source
# Restoring media player to its previous state assert 0.0 == state.attributes[ATTR_MEDIA_VOLUME_LEVEL]
self.hass.services.call(DOMAIN, SERVICE_RESTORE, blocking=True) assert "one" == state.attributes[ATTR_INPUT_SOURCE]
self.hass.block_till_done()
# Checking that values were restored
assert "Zone name" == self.media_player.name
assert STATE_ON == self.media_player.state
assert 0.0 == self.media_player.volume_level, 0.0001
assert self.media_player.is_volume_muted
assert "one" == self.media_player.source
def test_update(self): async def test_service_calls_with_all_entities(hass):
"""Test updating values from monoprice.""" """Test snapshot save/restore service calls."""
assert self.media_player.state is None await _setup_monoprice(hass, MockMonoprice())
assert self.media_player.volume_level is None
assert self.media_player.is_volume_muted is None
assert self.media_player.source is None
self.media_player.update() # Changing media player to new state
await _call_media_player_service(
hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0}
)
await _call_media_player_service(
hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"}
)
assert STATE_ON == self.media_player.state # Saving existing values
assert 0.0 == self.media_player.volume_level, 0.0001 await _call_monoprice_service(hass, SERVICE_SNAPSHOT, {"entity_id": "all"})
assert self.media_player.is_volume_muted
assert "one" == self.media_player.source
def test_name(self): # Changing media player to new state
"""Test name property.""" await _call_media_player_service(
assert "Zone name" == self.media_player.name hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0}
)
await _call_media_player_service(
hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "three"}
)
def test_state(self): # Restoring media player to its previous state
"""Test state property.""" await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": "all"})
assert self.media_player.state is None await hass.async_block_till_done()
self.media_player.update() state = hass.states.get(ZONE_1_ID)
assert STATE_ON == self.media_player.state
self.monoprice.zones[12].power = False assert 0.0 == state.attributes[ATTR_MEDIA_VOLUME_LEVEL]
self.media_player.update() assert "one" == state.attributes[ATTR_INPUT_SOURCE]
assert STATE_OFF == self.media_player.state
def test_volume_level(self):
"""Test volume level property."""
assert self.media_player.volume_level is None
self.media_player.update()
assert 0.0 == self.media_player.volume_level, 0.0001
self.monoprice.zones[12].volume = 38 async def test_service_calls_without_relevant_entities(hass):
self.media_player.update() """Test snapshot save/restore service calls."""
assert 1.0 == self.media_player.volume_level, 0.0001 await _setup_monoprice(hass, MockMonoprice())
self.monoprice.zones[12].volume = 19 # Changing media player to new state
self.media_player.update() await _call_media_player_service(
assert 0.5 == self.media_player.volume_level, 0.0001 hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0}
)
await _call_media_player_service(
hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"}
)
def test_is_volume_muted(self): # Saving existing values
"""Test volume muted property.""" await _call_monoprice_service(hass, SERVICE_SNAPSHOT, {"entity_id": "all"})
assert self.media_player.is_volume_muted is None
self.media_player.update() # Changing media player to new state
assert self.media_player.is_volume_muted await _call_media_player_service(
hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0}
)
await _call_media_player_service(
hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "three"}
)
self.monoprice.zones[12].mute = False # Restoring media player to its previous state
self.media_player.update() await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": "light.demo"})
assert not self.media_player.is_volume_muted await hass.async_block_till_done()
def test_supported_features(self): state = hass.states.get(ZONE_1_ID)
"""Test supported features property."""
assert (
SUPPORT_VOLUME_MUTE
| SUPPORT_VOLUME_SET
| SUPPORT_VOLUME_STEP
| SUPPORT_TURN_ON
| SUPPORT_TURN_OFF
| SUPPORT_SELECT_SOURCE
== self.media_player.supported_features
)
def test_source(self): assert 1.0 == state.attributes[ATTR_MEDIA_VOLUME_LEVEL]
"""Test source property.""" assert "three" == state.attributes[ATTR_INPUT_SOURCE]
assert self.media_player.source is None
self.media_player.update()
assert "one" == self.media_player.source
def test_media_title(self):
"""Test media title property."""
assert self.media_player.media_title is None
self.media_player.update()
assert "one" == self.media_player.media_title
def test_source_list(self): async def test_restore_without_snapshort(hass):
"""Test source list property.""" """Test restore when snapshot wasn't called."""
# Note, the list is sorted! await _setup_monoprice(hass, MockMonoprice())
assert ["one", "two", "three"] == self.media_player.source_list
def test_select_source(self): with patch.object(MockMonoprice, "restore_zone") as method_call:
"""Test source selection methods.""" await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID})
self.media_player.update() await hass.async_block_till_done()
assert "one" == self.media_player.source assert not method_call.called
self.media_player.select_source("two")
assert 2 == self.monoprice.zones[12].source
self.media_player.update()
assert "two" == self.media_player.source
# Trying to set unknown source async def test_update(hass):
self.media_player.select_source("no name") """Test updating values from monoprice."""
assert 2 == self.monoprice.zones[12].source """Test snapshot save/restore service calls."""
self.media_player.update() monoprice = MockMonoprice()
assert "two" == self.media_player.source await _setup_monoprice(hass, monoprice)
def test_turn_on(self): # Changing media player to new state
"""Test turning on the zone.""" await _call_media_player_service(
self.monoprice.zones[12].power = False hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0}
self.media_player.update() )
assert STATE_OFF == self.media_player.state await _call_media_player_service(
hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"}
)
self.media_player.turn_on() monoprice.set_source(11, 3)
assert self.monoprice.zones[12].power monoprice.set_volume(11, 38)
self.media_player.update()
assert STATE_ON == self.media_player.state
def test_turn_off(self): await async_update_entity(hass, ZONE_1_ID)
"""Test turning off the zone.""" await hass.async_block_till_done()
self.monoprice.zones[12].power = True
self.media_player.update()
assert STATE_ON == self.media_player.state
self.media_player.turn_off() state = hass.states.get(ZONE_1_ID)
assert not self.monoprice.zones[12].power
self.media_player.update()
assert STATE_OFF == self.media_player.state
def test_mute_volume(self): assert 1.0 == state.attributes[ATTR_MEDIA_VOLUME_LEVEL]
"""Test mute functionality.""" assert "three" == state.attributes[ATTR_INPUT_SOURCE]
self.monoprice.zones[12].mute = True
self.media_player.update()
assert self.media_player.is_volume_muted
self.media_player.mute_volume(False)
assert not self.monoprice.zones[12].mute
self.media_player.update()
assert not self.media_player.is_volume_muted
self.media_player.mute_volume(True) async def test_supported_features(hass):
assert self.monoprice.zones[12].mute """Test supported features property."""
self.media_player.update() await _setup_monoprice(hass, MockMonoprice())
assert self.media_player.is_volume_muted
def test_set_volume_level(self): state = hass.states.get(ZONE_1_ID)
"""Test set volume level.""" assert (
self.media_player.set_volume_level(1.0) SUPPORT_VOLUME_MUTE
assert 38 == self.monoprice.zones[12].volume | SUPPORT_VOLUME_SET
assert isinstance(self.monoprice.zones[12].volume, int) | SUPPORT_VOLUME_STEP
| SUPPORT_TURN_ON
| SUPPORT_TURN_OFF
| SUPPORT_SELECT_SOURCE
== state.attributes["supported_features"]
)
self.media_player.set_volume_level(0.0)
assert 0 == self.monoprice.zones[12].volume
assert isinstance(self.monoprice.zones[12].volume, int)
self.media_player.set_volume_level(0.5) async def test_source_list(hass):
assert 19 == self.monoprice.zones[12].volume """Test source list property."""
assert isinstance(self.monoprice.zones[12].volume, int) await _setup_monoprice(hass, MockMonoprice())
def test_volume_up(self): state = hass.states.get(ZONE_1_ID)
"""Test increasing volume by one.""" # Note, the list is sorted!
self.monoprice.zones[12].volume = 37 assert ["one", "three"] == state.attributes[ATTR_INPUT_SOURCE_LIST]
self.media_player.update()
self.media_player.volume_up()
assert 38 == self.monoprice.zones[12].volume
assert isinstance(self.monoprice.zones[12].volume, int)
# Try to raise value beyond max
self.media_player.update()
self.media_player.volume_up()
assert 38 == self.monoprice.zones[12].volume
assert isinstance(self.monoprice.zones[12].volume, int)
def test_volume_down(self): async def test_select_source(hass):
"""Test decreasing volume by one.""" """Test source selection methods."""
self.monoprice.zones[12].volume = 1 monoprice = MockMonoprice()
self.media_player.update() await _setup_monoprice(hass, monoprice)
self.media_player.volume_down()
assert 0 == self.monoprice.zones[12].volume
assert isinstance(self.monoprice.zones[12].volume, int)
# Try to lower value beyond minimum await _call_media_player_service(
self.media_player.update() hass,
self.media_player.volume_down() SERVICE_SELECT_SOURCE,
assert 0 == self.monoprice.zones[12].volume {"entity_id": ZONE_1_ID, ATTR_INPUT_SOURCE: "three"},
assert isinstance(self.monoprice.zones[12].volume, int) )
assert 3 == monoprice.zones[11].source
# Trying to set unknown source
await _call_media_player_service(
hass,
SERVICE_SELECT_SOURCE,
{"entity_id": ZONE_1_ID, ATTR_INPUT_SOURCE: "no name"},
)
assert 3 == monoprice.zones[11].source
async def test_unknown_source(hass):
"""Test behavior when device has unknown source."""
monoprice = MockMonoprice()
await _setup_monoprice(hass, monoprice)
monoprice.set_source(11, 5)
await async_update_entity(hass, ZONE_1_ID)
await hass.async_block_till_done()
state = hass.states.get(ZONE_1_ID)
assert state.attributes.get(ATTR_INPUT_SOURCE) is None
async def test_turn_on_off(hass):
"""Test turning on the zone."""
monoprice = MockMonoprice()
await _setup_monoprice(hass, monoprice)
await _call_media_player_service(hass, SERVICE_TURN_OFF, {"entity_id": ZONE_1_ID})
assert not monoprice.zones[11].power
await _call_media_player_service(hass, SERVICE_TURN_ON, {"entity_id": ZONE_1_ID})
assert monoprice.zones[11].power
async def test_mute_volume(hass):
"""Test mute functionality."""
monoprice = MockMonoprice()
await _setup_monoprice(hass, monoprice)
await _call_media_player_service(
hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.5}
)
await _call_media_player_service(
hass, SERVICE_VOLUME_MUTE, {"entity_id": ZONE_1_ID, "is_volume_muted": False}
)
assert not monoprice.zones[11].mute
await _call_media_player_service(
hass, SERVICE_VOLUME_MUTE, {"entity_id": ZONE_1_ID, "is_volume_muted": True}
)
assert monoprice.zones[11].mute
async def test_volume_up_down(hass):
"""Test increasing volume by one."""
monoprice = MockMonoprice()
await _setup_monoprice(hass, monoprice)
await _call_media_player_service(
hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0}
)
assert 0 == monoprice.zones[11].volume
await _call_media_player_service(
hass, SERVICE_VOLUME_DOWN, {"entity_id": ZONE_1_ID}
)
# should not go below zero
assert 0 == monoprice.zones[11].volume
await _call_media_player_service(hass, SERVICE_VOLUME_UP, {"entity_id": ZONE_1_ID})
assert 1 == monoprice.zones[11].volume
await _call_media_player_service(
hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0}
)
assert 38 == monoprice.zones[11].volume
await _call_media_player_service(hass, SERVICE_VOLUME_UP, {"entity_id": ZONE_1_ID})
# should not go above 38
assert 38 == monoprice.zones[11].volume
await _call_media_player_service(
hass, SERVICE_VOLUME_DOWN, {"entity_id": ZONE_1_ID}
)
assert 37 == monoprice.zones[11].volume