Move apps configuration to options flow for vizio integration (#32543)

* move apps configuration to options flow

* add additional assertion to new test

* add additional assertions for options update

* update docstrings, config validation, and tests based on review
This commit is contained in:
Raman Gupta 2020-03-13 07:16:24 -04:00 committed by GitHub
parent 31d150794d
commit 26d7b2164e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 188 additions and 112 deletions

View File

@ -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")

View File

@ -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."""

View File

@ -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"
}
}
}

View File

@ -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,