From b50281a9173e7fb4a37b3f813ca92876088eaac3 Mon Sep 17 00:00:00 2001 From: On Freund Date: Tue, 24 Mar 2020 02:32:21 +0200 Subject: [PATCH] Options flow for Monoprice sources (#33156) * Options flow for monoprice sources * Fix lint errors --- .../components/monoprice/__init__.py | 7 ++ .../components/monoprice/config_flow.py | 98 ++++++++++++++----- .../components/monoprice/media_player.py | 17 +++- .../components/monoprice/strings.json | 15 +++ .../components/monoprice/test_config_flow.py | 35 ++++++- .../components/monoprice/test_media_player.py | 25 ++++- 6 files changed, 169 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index d18229e3d09..37593f6828e 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -33,6 +33,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.error("Error connecting to Monoprice controller at %s", port) raise ConfigEntryNotReady + entry.add_update_listener(_update_listener) + for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) @@ -53,3 +55,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) return unload_ok + + +async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/monoprice/config_flow.py b/homeassistant/components/monoprice/config_flow.py index 45434fac131..cbabc65a54b 100644 --- a/homeassistant/components/monoprice/config_flow.py +++ b/homeassistant/components/monoprice/config_flow.py @@ -21,17 +21,31 @@ 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, +SOURCES = [ + CONF_SOURCE_1, + CONF_SOURCE_2, + CONF_SOURCE_3, + CONF_SOURCE_4, + CONF_SOURCE_5, + CONF_SOURCE_6, +] + +OPTIONS_FOR_DATA = {vol.Optional(source): str for source in SOURCES} + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_PORT): str, **OPTIONS_FOR_DATA}) + + +@core.callback +def _sources_from_config(data): + sources_config = { + str(idx + 1): data.get(source) for idx, source in enumerate(SOURCES) + } + + return { + index: name.strip() + for index, name in sources_config.items() + if (name is not None and name.strip() != "") } -) async def validate_input(hass: core.HomeAssistant, data): @@ -45,19 +59,8 @@ async def validate_input(hass: core.HomeAssistant, data): _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() != "") - } + sources = _sources_from_config(data) + # Return info that you want to store in the config entry. return {CONF_PORT: data[CONF_PORT], CONF_SOURCES: sources} @@ -86,6 +89,55 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) + @staticmethod + @core.callback + def async_get_options_flow(config_entry): + """Define the config flow to handle options.""" + return MonopriceOptionsFlowHandler(config_entry) + + +@core.callback +def _key_for_source(index, source, previous_sources): + if str(index) in previous_sources: + key = vol.Optional(source, default=previous_sources[str(index)]) + else: + key = vol.Optional(source) + + return key + + +class MonopriceOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a Monoprice options flow.""" + + def __init__(self, config_entry): + """Initialize.""" + self.config_entry = config_entry + + @core.callback + def _previous_sources(self): + if CONF_SOURCES in self.config_entry.options: + previous = self.config_entry.options[CONF_SOURCES] + else: + previous = self.config_entry.data[CONF_SOURCES] + + return previous + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry( + title="", data={CONF_SOURCES: _sources_from_config(user_input)} + ) + + previous_sources = self._previous_sources() + + options = { + _key_for_source(idx + 1, source, previous_sources): str + for idx, source in enumerate(SOURCES) + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(options),) + class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 6e898cd6d4c..d85c219691e 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -1,6 +1,7 @@ """Support for interfacing with Monoprice 6 zone home audio controller.""" import logging +from homeassistant import core from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, @@ -27,7 +28,10 @@ SUPPORT_MONOPRICE = ( ) -def _get_sources(sources_config): +@core.callback +def _get_sources_from_dict(data): + sources_config = data[CONF_SOURCES] + source_id_name = {int(index): name for index, name in sources_config.items()} source_name_id = {v: k for k, v in source_id_name.items()} @@ -37,13 +41,22 @@ def _get_sources(sources_config): return [source_id_name, source_name_id, source_names] +@core.callback +def _get_sources(config_entry): + if CONF_SOURCES in config_entry.options: + data = config_entry.options + else: + data = config_entry.data + return _get_sources_from_dict(data) + + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Monoprice 6-zone amplifier platform.""" port = config_entry.data[CONF_PORT] monoprice = hass.data[DOMAIN][config_entry.entry_id] - sources = _get_sources(config_entry.data.get(CONF_SOURCES)) + sources = _get_sources(config_entry) entities = [] for i in range(1, 4): diff --git a/homeassistant/components/monoprice/strings.json b/homeassistant/components/monoprice/strings.json index d0f5badbeb0..32332c7369a 100644 --- a/homeassistant/components/monoprice/strings.json +++ b/homeassistant/components/monoprice/strings.json @@ -22,5 +22,20 @@ "abort": { "already_configured": "Device is already configured" } + }, + "options": { + "step": { + "init": { + "title": "Configure sources", + "data": { + "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" + } + } + } } } \ No newline at end of file diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py index 234f7538f19..ecafa17e174 100644 --- a/tests/components/monoprice/test_config_flow.py +++ b/tests/components/monoprice/test_config_flow.py @@ -2,7 +2,7 @@ from asynctest import patch from serial import SerialException -from homeassistant import config_entries, setup +from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.monoprice.const import ( CONF_SOURCE_1, CONF_SOURCE_4, @@ -12,6 +12,8 @@ from homeassistant.components.monoprice.const import ( ) from homeassistant.const import CONF_PORT +from tests.common import MockConfigEntry + CONFIG = { CONF_PORT: "/test/port", CONF_SOURCE_1: "one", @@ -45,7 +47,7 @@ async def test_form(hass): 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]}, + 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 @@ -86,3 +88,32 @@ async def test_generic_exception(hass): assert result2["type"] == "form" assert result2["errors"] == {"base": "unknown"} + + +async def test_options_flow(hass): + """Test config flow options.""" + conf = {CONF_PORT: "/test/port", CONF_SOURCES: {"4": "four"}} + + config_entry = MockConfigEntry( + domain=DOMAIN, + # unique_id="abcde12345", + data=conf, + # options={CONF_SHOW_ON_MAP: True}, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.monoprice.async_setup_entry", return_value=True + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_SOURCE_1: "one", CONF_SOURCE_4: "", CONF_SOURCE_5: "five"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options[CONF_SOURCES] == {"1": "one", "5": "five"} diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index 2aad854652b..3778f2af04b 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -37,6 +37,7 @@ from homeassistant.helpers.entity_component import async_update_entity from tests.common import MockConfigEntry MOCK_CONFIG = {CONF_PORT: "fake port", CONF_SOURCES: {"1": "one", "3": "three"}} +MOCK_OPTIONS = {CONF_SOURCES: {"2": "two", "4": "four"}} ZONE_1_ID = "media_player.zone_11" ZONE_2_ID = "media_player.zone_12" @@ -117,6 +118,20 @@ async def _setup_monoprice(hass, monoprice): await hass.async_block_till_done() +async def _setup_monoprice_with_options(hass, monoprice): + with patch( + "homeassistant.components.monoprice.get_monoprice", new=lambda *a: monoprice, + ): + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS + ) + 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() + + async def _call_media_player_service(hass, name, data): await hass.services.async_call( MEDIA_PLAYER_DOMAIN, name, service_data=data, blocking=True @@ -256,7 +271,6 @@ async def test_restore_without_snapshort(hass): async def test_update(hass): """Test updating values from monoprice.""" - """Test snapshot save/restore service calls.""" monoprice = MockMonoprice() await _setup_monoprice(hass, monoprice) @@ -305,6 +319,15 @@ async def test_source_list(hass): assert ["one", "three"] == state.attributes[ATTR_INPUT_SOURCE_LIST] +async def test_source_list_with_options(hass): + """Test source list property.""" + await _setup_monoprice_with_options(hass, MockMonoprice()) + + state = hass.states.get(ZONE_1_ID) + # Note, the list is sorted! + assert ["two", "four"] == state.attributes[ATTR_INPUT_SOURCE_LIST] + + async def test_select_source(hass): """Test source selection methods.""" monoprice = MockMonoprice()