diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 1eb58c96670..7dc4b19fa83 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -43,7 +43,8 @@ def _get_config_schema(input_dict: Dict[str, Any] = None) -> vol.Schema: """ Return schema defaults for init step based on user input/config dict. - Retain info already provided for future form views by setting them as defaults in schema. + Retain info already provided for future form views by setting them + as defaults in schema. """ if input_dict is None: input_dict = {} @@ -70,7 +71,8 @@ def _get_pairing_schema(input_dict: Dict[str, Any] = None) -> vol.Schema: """ Return schema defaults for pairing data based on user input. - Retain info already provided for future form views by setting them as defaults in schema. + Retain info already provided for future form views by setting + them as defaults in schema. """ if input_dict is None: input_dict = {} @@ -97,6 +99,16 @@ class VizioOptionsConfigFlow(config_entries.OptionsFlow): ) -> Dict[str, Any]: """Manage the vizio options.""" if user_input is not None: + if user_input.get(CONF_APPS_TO_INCLUDE_OR_EXCLUDE): + user_input[CONF_APPS] = { + user_input[CONF_INCLUDE_OR_EXCLUDE]: user_input[ + CONF_APPS_TO_INCLUDE_OR_EXCLUDE + ].copy() + } + + user_input.pop(CONF_INCLUDE_OR_EXCLUDE) + user_input.pop(CONF_APPS_TO_INCLUDE_OR_EXCLUDE) + return self.async_create_entry(title="", data=user_input) options = { @@ -108,6 +120,30 @@ class VizioOptionsConfigFlow(config_entries.OptionsFlow): ): vol.All(vol.Coerce(int), vol.Range(min=1, max=10)) } + if self.config_entry.data[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV: + default_include_or_exclude = ( + CONF_EXCLUDE + if self.config_entry.options + and CONF_EXCLUDE in self.config_entry.options.get(CONF_APPS) + else CONF_EXCLUDE + ) + options.update( + { + vol.Optional( + CONF_INCLUDE_OR_EXCLUDE, + default=default_include_or_exclude.title(), + ): vol.All( + vol.In([CONF_INCLUDE.title(), CONF_EXCLUDE.title()]), vol.Lower + ), + vol.Optional( + CONF_APPS_TO_INCLUDE_OR_EXCLUDE, + default=self.config_entry.options.get(CONF_APPS, {}).get( + default_include_or_exclude, [] + ), + ): cv.multi_select(VizioAsync.get_apps_list()), + } + ) + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) @@ -135,7 +171,11 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _create_entry_if_unique( self, input_dict: Dict[str, Any] ) -> Dict[str, Any]: - """Check if unique_id doesn't already exist. If it does, abort. If it doesn't, create entry.""" + """ + Create entry if ID is unique. + + If it is, create entry. If it isn't, abort config flow. + """ # Remove extra keys that will not be used by entry setup input_dict.pop(CONF_APPS_TO_INCLUDE_OR_EXCLUDE, None) input_dict.pop(CONF_INCLUDE_OR_EXCLUDE, None) @@ -195,13 +235,6 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) self._abort_if_unique_id_configured() - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - if ( - user_input[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV - and self.context["source"] != SOURCE_IMPORT - ): - self._data = copy.deepcopy(user_input) - return await self.async_step_tv_apps() return await self._create_entry_if_unique(user_input) # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 elif self._must_show_form and self.context["source"] == SOURCE_IMPORT: @@ -250,7 +283,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not import_config.get(CONF_APPS): remove_apps = True else: - updated_data[CONF_APPS] = import_config[CONF_APPS] + updated_options[CONF_APPS] = import_config[CONF_APPS] if entry.data.get(CONF_VOLUME_STEP) != import_config[CONF_VOLUME_STEP]: updated_options[CONF_VOLUME_STEP] = import_config[CONF_VOLUME_STEP] @@ -261,6 +294,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if remove_apps: new_data.pop(CONF_APPS) + new_options.pop(CONF_APPS) if updated_data: new_data.update(updated_data) @@ -319,7 +353,11 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pair_tv( self, user_input: Dict[str, Any] = None ) -> Dict[str, Any]: - """Start pairing process and ask user for PIN to complete pairing process.""" + """ + Start pairing process for TV. + + Ask user for PIN to complete pairing process. + """ errors = {} # Start pairing process if it hasn't already started @@ -382,7 +420,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # If user is pairing via config import, show different message return await self.async_step_pairing_complete_import() - return await self.async_step_tv_apps() + return await self.async_step_pairing_complete() # If no data was retrieved, it's assumed that the pairing attempt was not # successful @@ -394,43 +432,35 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_pairing_complete_import( - self, user_input: Dict[str, Any] = None - ) -> Dict[str, Any]: - """Complete import config flow by displaying final message to show user access token and give further instructions.""" + async def _pairing_complete(self, step_id: str) -> Dict[str, Any]: + """Handle config flow completion.""" if not self._must_show_form: return await self._create_entry_if_unique(self._data) self._must_show_form = False return self.async_show_form( - step_id="pairing_complete_import", + step_id=step_id, data_schema=vol.Schema({}), description_placeholders={"access_token": self._data[CONF_ACCESS_TOKEN]}, ) - async def async_step_tv_apps( + async def async_step_pairing_complete( self, user_input: Dict[str, Any] = None ) -> Dict[str, Any]: - """Handle app configuration to complete TV configuration.""" - if user_input is not None: - if user_input.get(CONF_APPS_TO_INCLUDE_OR_EXCLUDE): - # Update stored apps with user entry config keys - self._apps[user_input[CONF_INCLUDE_OR_EXCLUDE].lower()] = user_input[ - CONF_APPS_TO_INCLUDE_OR_EXCLUDE - ].copy() + """ + Complete non-import sourced config flow. - return await self._create_entry_if_unique(self._data) + Display final message to user confirming pairing. + """ + return await self._pairing_complete("pairing_complete") - return self.async_show_form( - step_id="tv_apps", - data_schema=vol.Schema( - { - vol.Optional( - CONF_INCLUDE_OR_EXCLUDE, default=CONF_INCLUDE.title(), - ): vol.In([CONF_INCLUDE.title(), CONF_EXCLUDE.title()]), - vol.Optional(CONF_APPS_TO_INCLUDE_OR_EXCLUDE): cv.multi_select( - VizioAsync.get_apps_list() - ), - } - ), - ) + async def async_step_pairing_complete_import( + self, user_input: Dict[str, Any] = None + ) -> Dict[str, Any]: + """ + Complete import sourced config flow. + + Display final message to user confirming pairing and displaying + access token. + """ + return await self._pairing_complete("pairing_complete_import") diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index d013f41403a..a46a4c9a2d1 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -60,7 +60,6 @@ async def async_setup_entry( token = config_entry.data.get(CONF_ACCESS_TOKEN) name = config_entry.data[CONF_NAME] device_class = config_entry.data[CONF_DEVICE_CLASS] - conf_apps = config_entry.data.get(CONF_APPS, {}) # If config entry options not set up, set them up, otherwise assign values managed in options volume_step = config_entry.options.get( @@ -70,6 +69,20 @@ async def async_setup_entry( params = {} if not config_entry.options: params["options"] = {CONF_VOLUME_STEP: volume_step} + include_or_exclude_key = next( + ( + key + for key in config_entry.data.get(CONF_APPS, {}) + if key in [CONF_INCLUDE, CONF_EXCLUDE] + ), + None, + ) + if include_or_exclude_key: + params["options"][CONF_APPS] = { + include_or_exclude_key: config_entry.data[CONF_APPS][ + include_or_exclude_key + ].copy() + } if not config_entry.data.get(CONF_VOLUME_STEP): new_data = config_entry.data.copy() @@ -93,9 +106,7 @@ async def async_setup_entry( _LOGGER.warning("Failed to connect to %s", host) raise PlatformNotReady - entity = VizioDevice( - config_entry, device, name, volume_step, device_class, conf_apps, - ) + entity = VizioDevice(config_entry, device, name, device_class,) async_add_entities([entity], update_before_add=True) @@ -108,9 +119,7 @@ class VizioDevice(MediaPlayerDevice): config_entry: ConfigEntry, device: VizioAsync, name: str, - volume_step: int, device_class: str, - conf_apps: Dict[str, List[Any]], ) -> None: """Initialize Vizio device.""" self._config_entry = config_entry @@ -119,14 +128,16 @@ class VizioDevice(MediaPlayerDevice): self._name = name self._state = None self._volume_level = None - self._volume_step = volume_step + self._volume_step = config_entry.options[CONF_VOLUME_STEP] self._is_muted = None self._current_input = None self._current_app = None self._available_inputs = [] self._available_apps = [] - self._conf_apps = conf_apps - self._additional_app_configs = self._conf_apps.get(CONF_ADDITIONAL_CONFIGS, []) + self._conf_apps = config_entry.options.get(CONF_APPS, {}) + self._additional_app_configs = config_entry.data.get(CONF_APPS, {}).get( + CONF_ADDITIONAL_CONFIGS, [] + ) self._device_class = device_class self._supported_commands = SUPPORTED_COMMANDS[device_class] self._device = device @@ -248,6 +259,7 @@ class VizioDevice(MediaPlayerDevice): async def _async_update_options(self, config_entry: ConfigEntry) -> None: """Update options if the update signal comes from this entity.""" self._volume_step = config_entry.options[CONF_VOLUME_STEP] + self._conf_apps.update(config_entry.options.get(CONF_APPS, {})) async def async_added_to_hass(self): """Register callbacks when entity is added.""" diff --git a/homeassistant/components/vizio/strings.json b/homeassistant/components/vizio/strings.json index 61db7b49665..b6f6f53cf79 100644 --- a/homeassistant/components/vizio/strings.json +++ b/homeassistant/components/vizio/strings.json @@ -19,17 +19,13 @@ "pin": "PIN" } }, + "pairing_complete": { + "title": "Pairing Complete", + "description": "Your Vizio SmartCast device is now connected to Home Assistant." + }, "pairing_complete_import": { "title": "Pairing Complete", "description": "Your Vizio SmartCast TV is now connected to Home Assistant.\n\nYour Access Token is '**{access_token}**'." - }, - "tv_apps": { - "title": "Configure Apps for Smart TV", - "description": "If you have a Smart TV, you can optionally filter your source list by choosing which apps to include or exclude in your source list. You can skip this step for TVs that don't support apps.", - "data": { - "include_or_exclude": "Include or Exclude Apps?", - "apps_to_include_or_exclude": "Apps to Include or Exclude" - } } }, "error": { @@ -48,8 +44,11 @@ "step": { "init": { "title": "Update Vizo SmartCast Options", + "description": "If you have a Smart TV, you can optionally filter your source list by choosing which apps to include or exclude in your source list.", "data": { - "volume_step": "Volume Step Size" + "volume_step": "Volume Step Size", + "include_or_exclude": "Include or Exclude Apps?", + "apps_to_include_or_exclude": "Apps to Include or Exclude" } } } diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index e773035447a..a8a760d8ca2 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -9,9 +9,7 @@ from homeassistant.components.media_player import DEVICE_CLASS_SPEAKER, DEVICE_C from homeassistant.components.vizio.config_flow import _get_config_schema from homeassistant.components.vizio.const import ( CONF_APPS, - CONF_APPS_TO_INCLUDE_OR_EXCLUDE, CONF_INCLUDE, - CONF_INCLUDE_OR_EXCLUDE, CONF_VOLUME_STEP, DEFAULT_NAME, DEFAULT_VOLUME_STEP, @@ -39,6 +37,7 @@ from .const import ( MOCK_PIN_CONFIG, MOCK_SPEAKER_CONFIG, MOCK_TV_CONFIG_NO_TOKEN, + MOCK_TV_WITH_ADDITIONAL_APPS_CONFIG, MOCK_TV_WITH_EXCLUDE_CONFIG, MOCK_USER_VALID_TV_CONFIG, MOCK_ZEROCONF_SERVICE_INFO, @@ -95,52 +94,17 @@ async def test_user_flow_all_fields( result["flow_id"], user_input=MOCK_USER_VALID_TV_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "tv_apps" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_INCLUDE_APPS - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_TV assert result["data"][CONF_ACCESS_TOKEN] == ACCESS_TOKEN - assert result["data"][CONF_APPS][CONF_INCLUDE] == [CURRENT_APP] + assert CONF_APPS not in result["data"] -async def test_user_apps_with_tv( - hass: HomeAssistantType, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, -) -> None: - """Test TV can have selected apps during user setup.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=MOCK_IMPORT_VALID_TV_CONFIG - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "tv_apps" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_INCLUDE_APPS - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == NAME - assert result["data"][CONF_NAME] == NAME - assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_TV - assert result["data"][CONF_ACCESS_TOKEN] == ACCESS_TOKEN - assert result["data"][CONF_APPS][CONF_INCLUDE] == [CURRENT_APP] - assert CONF_APPS_TO_INCLUDE_OR_EXCLUDE not in result["data"] - assert CONF_INCLUDE_OR_EXCLUDE not in result["data"] - - -async def test_options_flow(hass: HomeAssistantType) -> None: - """Test options config flow.""" +async def test_speaker_options_flow(hass: HomeAssistantType) -> None: + """Test options config flow for speaker.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_SPEAKER_CONFIG) entry.add_to_hass(hass) @@ -158,6 +122,58 @@ async def test_options_flow(hass: HomeAssistantType) -> None: assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP + assert CONF_APPS not in result["data"] + + +async def test_tv_options_flow_no_apps(hass: HomeAssistantType) -> None: + """Test options config flow for TV without providing apps option.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG) + entry.add_to_hass(hass) + + assert not entry.options + + result = await hass.config_entries.options.async_init(entry.entry_id, data=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + options = {CONF_VOLUME_STEP: VOLUME_STEP} + options.update(MOCK_INCLUDE_NO_APPS) + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input=options + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "" + assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP + assert CONF_APPS not in result["data"] + + +async def test_tv_options_flow_with_apps(hass: HomeAssistantType) -> None: + """Test options config flow for TV with providing apps option.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG) + entry.add_to_hass(hass) + + assert not entry.options + + result = await hass.config_entries.options.async_init(entry.entry_id, data=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + options = {CONF_VOLUME_STEP: VOLUME_STEP} + options.update(MOCK_INCLUDE_APPS) + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input=options + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "" + assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP + assert CONF_APPS in result["data"] + assert result["data"][CONF_APPS] == {CONF_INCLUDE: [CURRENT_APP]} async def test_user_host_already_configured( @@ -282,11 +298,9 @@ async def test_user_tv_pairing_no_apps( ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "tv_apps" + assert result["step_id"] == "pairing_complete" - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_INCLUDE_NO_APPS - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == NAME @@ -427,10 +441,8 @@ async def test_import_flow_update_options( assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "updated_entry" - assert ( - hass.config_entries.async_get_entry(entry_id).options[CONF_VOLUME_STEP] - == VOLUME_STEP + 1 - ) + config_entry = hass.config_entries.async_get_entry(entry_id) + assert config_entry.options[CONF_VOLUME_STEP] == VOLUME_STEP + 1 async def test_import_flow_update_name_and_apps( @@ -461,10 +473,10 @@ async def test_import_flow_update_name_and_apps( assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "updated_entry" - assert hass.config_entries.async_get_entry(entry_id).data[CONF_NAME] == NAME2 - assert hass.config_entries.async_get_entry(entry_id).data[CONF_APPS] == { - CONF_INCLUDE: [CURRENT_APP] - } + config_entry = hass.config_entries.async_get_entry(entry_id) + assert config_entry.data[CONF_NAME] == NAME2 + assert config_entry.data[CONF_APPS] == {CONF_INCLUDE: [CURRENT_APP]} + assert config_entry.options[CONF_APPS] == {CONF_INCLUDE: [CURRENT_APP]} async def test_import_flow_update_remove_apps( @@ -482,7 +494,9 @@ async def test_import_flow_update_remove_apps( assert result["result"].data[CONF_NAME] == NAME assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - entry_id = result["result"].entry_id + config_entry = hass.config_entries.async_get_entry(result["result"].entry_id) + assert CONF_APPS in config_entry.data + assert CONF_APPS in config_entry.options updated_config = MOCK_TV_WITH_EXCLUDE_CONFIG.copy() updated_config.pop(CONF_APPS) @@ -494,7 +508,8 @@ async def test_import_flow_update_remove_apps( assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "updated_entry" - assert hass.config_entries.async_get_entry(entry_id).data.get(CONF_APPS) is None + assert CONF_APPS not in config_entry.data + assert CONF_APPS not in config_entry.options async def test_import_needs_pairing( @@ -577,6 +592,26 @@ async def test_import_with_apps_needs_pairing( assert result["data"][CONF_APPS][CONF_INCLUDE] == [CURRENT_APP] +async def test_import_flow_additional_configs( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_update: pytest.fixture, +) -> None: + """Test import config flow with additional configs defined in CONF_APPS.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=vol.Schema(VIZIO_SCHEMA)(MOCK_TV_WITH_ADDITIONAL_APPS_CONFIG), + ) + await hass.async_block_till_done() + + assert result["result"].data[CONF_NAME] == NAME + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + config_entry = hass.config_entries.async_get_entry(result["result"].entry_id) + assert CONF_APPS in config_entry.data + assert CONF_APPS not in config_entry.options + + async def test_import_error( hass: HomeAssistantType, vizio_connect: pytest.fixture,