mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
Add app support for TVs to Vizio integration (#32432)
* add app support * code cleanup, add additional test, add CONF_APPS storage logic for import * simplify schema defaults logic * remove unnecessary lower() and fix docstring * remove default return for popping CONF_APPS during import update because we know entry data has CONF_APPS due to if statement * further simplification * even more simplification * fix type hints * move app configuration to separate step, fix tests, and only make app updates if device_type == tv * remove errors variable from tv_apps and move tv_apps schema out of ConfigFlow for consistency * slight refactor * remove unused error from strings.json * set unique id as early as possible * correct which dictionary to use to set unique id in pair_tv step
This commit is contained in:
parent
873bf887a5
commit
a579fcf248
@ -3,14 +3,29 @@ import asyncio
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.media_player import DEVICE_CLASS_TV
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||||
|
|
||||||
from .const import DOMAIN, VIZIO_SCHEMA
|
from .const import CONF_APPS, CONF_DEVICE_CLASS, DOMAIN, VIZIO_SCHEMA
|
||||||
|
|
||||||
|
|
||||||
|
def validate_apps(config: ConfigType) -> ConfigType:
|
||||||
|
"""Validate CONF_APPS is only used when CONF_DEVICE_CLASS == DEVICE_CLASS_TV."""
|
||||||
|
if (
|
||||||
|
config.get(CONF_APPS) is not None
|
||||||
|
and config[CONF_DEVICE_CLASS] != DEVICE_CLASS_TV
|
||||||
|
):
|
||||||
|
raise vol.Invalid(
|
||||||
|
f"'{CONF_APPS}' can only be used if {CONF_DEVICE_CLASS}' is '{DEVICE_CLASS_TV}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{DOMAIN: vol.All(cv.ensure_list, [vol.Schema(VIZIO_SCHEMA)])},
|
{DOMAIN: vol.All(cv.ensure_list, [vol.All(VIZIO_SCHEMA, validate_apps)])},
|
||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -12,16 +12,22 @@ from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_ZEROCONF, ConfigE
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_ACCESS_TOKEN,
|
CONF_ACCESS_TOKEN,
|
||||||
CONF_DEVICE_CLASS,
|
CONF_DEVICE_CLASS,
|
||||||
|
CONF_EXCLUDE,
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
|
CONF_INCLUDE,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_PIN,
|
CONF_PIN,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
CONF_TYPE,
|
CONF_TYPE,
|
||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_APPS,
|
||||||
|
CONF_APPS_TO_INCLUDE_OR_EXCLUDE,
|
||||||
|
CONF_INCLUDE_OR_EXCLUDE,
|
||||||
CONF_VOLUME_STEP,
|
CONF_VOLUME_STEP,
|
||||||
DEFAULT_DEVICE_CLASS,
|
DEFAULT_DEVICE_CLASS,
|
||||||
DEFAULT_NAME,
|
DEFAULT_NAME,
|
||||||
@ -34,7 +40,11 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def _get_config_schema(input_dict: Dict[str, Any] = None) -> vol.Schema:
|
def _get_config_schema(input_dict: Dict[str, Any] = None) -> vol.Schema:
|
||||||
"""Return schema defaults for config data based on user input/config dict. Retain info already provided for future form views by setting them as defaults in 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.
|
||||||
|
"""
|
||||||
if input_dict is None:
|
if input_dict is None:
|
||||||
input_dict = {}
|
input_dict = {}
|
||||||
|
|
||||||
@ -57,13 +67,16 @@ def _get_config_schema(input_dict: Dict[str, Any] = None) -> vol.Schema:
|
|||||||
|
|
||||||
|
|
||||||
def _get_pairing_schema(input_dict: Dict[str, Any] = None) -> vol.Schema:
|
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."""
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
if input_dict is None:
|
if input_dict is None:
|
||||||
input_dict = {}
|
input_dict = {}
|
||||||
|
|
||||||
return vol.Schema(
|
return vol.Schema(
|
||||||
{vol.Required(CONF_PIN, default=input_dict.get(CONF_PIN, "")): str},
|
{vol.Required(CONF_PIN, default=input_dict.get(CONF_PIN, "")): str}
|
||||||
extra=vol.ALLOW_EXTRA,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -73,7 +86,7 @@ def _host_is_same(host1: str, host2: str) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
class VizioOptionsConfigFlow(config_entries.OptionsFlow):
|
class VizioOptionsConfigFlow(config_entries.OptionsFlow):
|
||||||
"""Handle Transmission client options."""
|
"""Handle Vizio options."""
|
||||||
|
|
||||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||||
"""Initialize vizio options flow."""
|
"""Initialize vizio options flow."""
|
||||||
@ -117,22 +130,18 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
self._ch_type = None
|
self._ch_type = None
|
||||||
self._pairing_token = None
|
self._pairing_token = None
|
||||||
self._data = None
|
self._data = None
|
||||||
|
self._apps = {}
|
||||||
|
|
||||||
async def _create_entry_if_unique(
|
async def _create_entry_if_unique(
|
||||||
self, input_dict: Dict[str, Any]
|
self, input_dict: Dict[str, Any]
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Check if unique_id doesn't already exist. If it does, abort. If it doesn't, create entry."""
|
"""Check if unique_id doesn't already exist. If it does, abort. If it doesn't, create entry."""
|
||||||
unique_id = await VizioAsync.get_unique_id(
|
# Remove extra keys that will not be used by entry setup
|
||||||
input_dict[CONF_HOST],
|
input_dict.pop(CONF_APPS_TO_INCLUDE_OR_EXCLUDE, None)
|
||||||
input_dict.get(CONF_ACCESS_TOKEN),
|
input_dict.pop(CONF_INCLUDE_OR_EXCLUDE, None)
|
||||||
input_dict[CONF_DEVICE_CLASS],
|
|
||||||
session=async_get_clientsession(self.hass, False),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set unique ID and abort if unique ID is already configured on an entry or a flow
|
if self._apps:
|
||||||
# with the unique ID is already in progress
|
input_dict[CONF_APPS] = self._apps
|
||||||
await self.async_set_unique_id(unique_id=unique_id, raise_on_progress=True)
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
|
|
||||||
return self.async_create_entry(title=input_dict[CONF_NAME], data=input_dict)
|
return self.async_create_entry(title=input_dict[CONF_NAME], data=input_dict)
|
||||||
|
|
||||||
@ -172,6 +181,27 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
errors["base"] = "cant_connect"
|
errors["base"] = "cant_connect"
|
||||||
|
|
||||||
if not errors:
|
if not errors:
|
||||||
|
unique_id = await VizioAsync.get_unique_id(
|
||||||
|
user_input[CONF_HOST],
|
||||||
|
user_input.get(CONF_ACCESS_TOKEN),
|
||||||
|
user_input[CONF_DEVICE_CLASS],
|
||||||
|
session=async_get_clientsession(self.hass, False),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set unique ID and abort if unique ID is already configured on an entry or a flow
|
||||||
|
# with the unique ID is already in progress
|
||||||
|
await self.async_set_unique_id(
|
||||||
|
unique_id=unique_id, raise_on_progress=True
|
||||||
|
)
|
||||||
|
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)
|
return await self._create_entry_if_unique(user_input)
|
||||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||||
elif self._must_show_form and self.context["source"] == SOURCE_IMPORT:
|
elif self._must_show_form and self.context["source"] == SOURCE_IMPORT:
|
||||||
@ -180,11 +210,9 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
# their configuration.yaml or to proceed with config flow pairing. We
|
# their configuration.yaml or to proceed with config flow pairing. We
|
||||||
# will also provide contextual message to user explaining why
|
# will also provide contextual message to user explaining why
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Couldn't complete configuration.yaml import: '%s' key is missing. To "
|
"Couldn't complete configuration.yaml import: '%s' key is "
|
||||||
"complete setup, '%s' can be obtained by going through pairing process "
|
"missing. Either provide '%s' key in configuration.yaml or "
|
||||||
"via frontend Integrations menu; to avoid re-pairing your device in the "
|
"finish setup by completing configuration via frontend.",
|
||||||
"future, once you have finished pairing, it is recommended to add "
|
|
||||||
"obtained value to your config ",
|
|
||||||
CONF_ACCESS_TOKEN,
|
CONF_ACCESS_TOKEN,
|
||||||
CONF_ACCESS_TOKEN,
|
CONF_ACCESS_TOKEN,
|
||||||
)
|
)
|
||||||
@ -210,20 +238,32 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
for entry in self.hass.config_entries.async_entries(DOMAIN):
|
for entry in self.hass.config_entries.async_entries(DOMAIN):
|
||||||
if _host_is_same(entry.data[CONF_HOST], import_config[CONF_HOST]):
|
if _host_is_same(entry.data[CONF_HOST], import_config[CONF_HOST]):
|
||||||
updated_options = {}
|
updated_options = {}
|
||||||
updated_name = {}
|
updated_data = {}
|
||||||
|
remove_apps = False
|
||||||
|
|
||||||
if entry.data[CONF_NAME] != import_config[CONF_NAME]:
|
if entry.data[CONF_NAME] != import_config[CONF_NAME]:
|
||||||
updated_name[CONF_NAME] = import_config[CONF_NAME]
|
updated_data[CONF_NAME] = import_config[CONF_NAME]
|
||||||
|
|
||||||
|
# Update entry.data[CONF_APPS] if import_config[CONF_APPS] differs, and
|
||||||
|
# pop entry.data[CONF_APPS] if import_config[CONF_APPS] is not specified
|
||||||
|
if entry.data.get(CONF_APPS) != import_config.get(CONF_APPS):
|
||||||
|
if not import_config.get(CONF_APPS):
|
||||||
|
remove_apps = True
|
||||||
|
else:
|
||||||
|
updated_data[CONF_APPS] = import_config[CONF_APPS]
|
||||||
|
|
||||||
if entry.data.get(CONF_VOLUME_STEP) != import_config[CONF_VOLUME_STEP]:
|
if entry.data.get(CONF_VOLUME_STEP) != import_config[CONF_VOLUME_STEP]:
|
||||||
updated_options[CONF_VOLUME_STEP] = import_config[CONF_VOLUME_STEP]
|
updated_options[CONF_VOLUME_STEP] = import_config[CONF_VOLUME_STEP]
|
||||||
|
|
||||||
if updated_options or updated_name:
|
if updated_options or updated_data or remove_apps:
|
||||||
new_data = entry.data.copy()
|
new_data = entry.data.copy()
|
||||||
new_options = entry.options.copy()
|
new_options = entry.options.copy()
|
||||||
|
|
||||||
if updated_name:
|
if remove_apps:
|
||||||
new_data.update(updated_name)
|
new_data.pop(CONF_APPS)
|
||||||
|
|
||||||
|
if updated_data:
|
||||||
|
new_data.update(updated_data)
|
||||||
|
|
||||||
if updated_options:
|
if updated_options:
|
||||||
new_data.update(updated_options)
|
new_data.update(updated_options)
|
||||||
@ -237,6 +277,10 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
return self.async_abort(reason="already_setup")
|
return self.async_abort(reason="already_setup")
|
||||||
|
|
||||||
self._must_show_form = True
|
self._must_show_form = True
|
||||||
|
# Store config key/value pairs that are not configurable in user step so they
|
||||||
|
# don't get lost on user step
|
||||||
|
if import_config.get(CONF_APPS):
|
||||||
|
self._apps = copy.deepcopy(import_config[CONF_APPS])
|
||||||
return await self.async_step_user(user_input=import_config)
|
return await self.async_step_user(user_input=import_config)
|
||||||
|
|
||||||
async def async_step_zeroconf(
|
async def async_step_zeroconf(
|
||||||
@ -319,12 +363,26 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
self._data[CONF_ACCESS_TOKEN] = pair_data.auth_token
|
self._data[CONF_ACCESS_TOKEN] = pair_data.auth_token
|
||||||
self._must_show_form = True
|
self._must_show_form = True
|
||||||
|
|
||||||
|
unique_id = await VizioAsync.get_unique_id(
|
||||||
|
self._data[CONF_HOST],
|
||||||
|
self._data[CONF_ACCESS_TOKEN],
|
||||||
|
self._data[CONF_DEVICE_CLASS],
|
||||||
|
session=async_get_clientsession(self.hass, False),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set unique ID and abort if unique ID is already configured on an entry or a flow
|
||||||
|
# with the unique ID is already in progress
|
||||||
|
await self.async_set_unique_id(
|
||||||
|
unique_id=unique_id, raise_on_progress=True
|
||||||
|
)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||||
if self.context["source"] == SOURCE_IMPORT:
|
if self.context["source"] == SOURCE_IMPORT:
|
||||||
# If user is pairing via config import, show different message
|
# If user is pairing via config import, show different message
|
||||||
return await self.async_step_pairing_complete_import()
|
return await self.async_step_pairing_complete_import()
|
||||||
|
|
||||||
return await self.async_step_pairing_complete()
|
return await self.async_step_tv_apps()
|
||||||
|
|
||||||
# If no data was retrieved, it's assumed that the pairing attempt was not
|
# If no data was retrieved, it's assumed that the pairing attempt was not
|
||||||
# successful
|
# successful
|
||||||
@ -336,26 +394,43 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _pairing_complete(self, step_id: str) -> Dict[str, Any]:
|
async def async_step_pairing_complete_import(
|
||||||
"""Handle config flow completion."""
|
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."""
|
||||||
if not self._must_show_form:
|
if not self._must_show_form:
|
||||||
return await self._create_entry_if_unique(self._data)
|
return await self._create_entry_if_unique(self._data)
|
||||||
|
|
||||||
self._must_show_form = False
|
self._must_show_form = False
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id=step_id,
|
step_id="pairing_complete_import",
|
||||||
data_schema=vol.Schema({}),
|
data_schema=vol.Schema({}),
|
||||||
description_placeholders={"access_token": self._data[CONF_ACCESS_TOKEN]},
|
description_placeholders={"access_token": self._data[CONF_ACCESS_TOKEN]},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_pairing_complete(
|
async def async_step_tv_apps(
|
||||||
self, user_input: Dict[str, Any] = None
|
self, user_input: Dict[str, Any] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Complete non-import config flow by displaying final message to confirm pairing."""
|
"""Handle app configuration to complete TV configuration."""
|
||||||
return await self._pairing_complete("pairing_complete")
|
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()
|
||||||
|
|
||||||
async def async_step_pairing_complete_import(
|
return await self._create_entry_if_unique(self._data)
|
||||||
self, user_input: Dict[str, Any] = None
|
|
||||||
) -> Dict[str, Any]:
|
return self.async_show_form(
|
||||||
"""Complete import config flow by displaying final message to show user access token and give further instructions."""
|
step_id="tv_apps",
|
||||||
return await self._pairing_complete("pairing_complete_import")
|
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()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
"""Constants used by vizio component."""
|
"""Constants used by vizio component."""
|
||||||
|
from pyvizio import VizioAsync
|
||||||
from pyvizio.const import (
|
from pyvizio.const import (
|
||||||
DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER,
|
DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER,
|
||||||
DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV,
|
DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV,
|
||||||
@ -19,11 +20,21 @@ from homeassistant.components.media_player.const import (
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_ACCESS_TOKEN,
|
CONF_ACCESS_TOKEN,
|
||||||
CONF_DEVICE_CLASS,
|
CONF_DEVICE_CLASS,
|
||||||
|
CONF_EXCLUDE,
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
|
CONF_INCLUDE,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
)
|
)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
CONF_ADDITIONAL_CONFIGS = "additional_configs"
|
||||||
|
CONF_APP_ID = "APP_ID"
|
||||||
|
CONF_APPS = "apps"
|
||||||
|
CONF_APPS_TO_INCLUDE_OR_EXCLUDE = "apps_to_include_or_exclude"
|
||||||
|
CONF_CONFIG = "config"
|
||||||
|
CONF_INCLUDE_OR_EXCLUDE = "include_or_exclude"
|
||||||
|
CONF_NAME_SPACE = "NAME_SPACE"
|
||||||
|
CONF_MESSAGE = "MESSAGE"
|
||||||
CONF_VOLUME_STEP = "volume_step"
|
CONF_VOLUME_STEP = "volume_step"
|
||||||
|
|
||||||
DEFAULT_DEVICE_CLASS = DEVICE_CLASS_TV
|
DEFAULT_DEVICE_CLASS = DEVICE_CLASS_TV
|
||||||
@ -69,4 +80,30 @@ VIZIO_SCHEMA = {
|
|||||||
vol.Optional(CONF_VOLUME_STEP, default=DEFAULT_VOLUME_STEP): vol.All(
|
vol.Optional(CONF_VOLUME_STEP, default=DEFAULT_VOLUME_STEP): vol.All(
|
||||||
vol.Coerce(int), vol.Range(min=1, max=10)
|
vol.Coerce(int), vol.Range(min=1, max=10)
|
||||||
),
|
),
|
||||||
|
vol.Optional(CONF_APPS): vol.All(
|
||||||
|
{
|
||||||
|
vol.Exclusive(CONF_INCLUDE, "apps_filter"): vol.All(
|
||||||
|
cv.ensure_list, [vol.All(cv.string, vol.In(VizioAsync.get_apps_list()))]
|
||||||
|
),
|
||||||
|
vol.Exclusive(CONF_EXCLUDE, "apps_filter"): vol.All(
|
||||||
|
cv.ensure_list, [vol.All(cv.string, vol.In(VizioAsync.get_apps_list()))]
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_ADDITIONAL_CONFIGS): vol.All(
|
||||||
|
cv.ensure_list,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
vol.Required(CONF_NAME): cv.string,
|
||||||
|
vol.Required(CONF_CONFIG): {
|
||||||
|
vol.Required(CONF_APP_ID): cv.string,
|
||||||
|
vol.Required(CONF_NAME_SPACE): vol.Coerce(int),
|
||||||
|
vol.Optional(CONF_MESSAGE, default=None): vol.Or(
|
||||||
|
cv.string, None
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
cv.has_at_least_one_key(CONF_INCLUDE, CONF_EXCLUDE, CONF_ADDITIONAL_CONFIGS),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "vizio",
|
"domain": "vizio",
|
||||||
"name": "Vizio SmartCast",
|
"name": "Vizio SmartCast",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/vizio",
|
"documentation": "https://www.home-assistant.io/integrations/vizio",
|
||||||
"requirements": ["pyvizio==0.1.26"],
|
"requirements": ["pyvizio==0.1.35"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": ["@raman325"],
|
"codeowners": ["@raman325"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
|
@ -1,16 +1,23 @@
|
|||||||
"""Vizio SmartCast Device support."""
|
"""Vizio SmartCast Device support."""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Callable, List
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
|
|
||||||
from pyvizio import VizioAsync
|
from pyvizio import VizioAsync
|
||||||
|
from pyvizio.const import INPUT_APPS, NO_APP_RUNNING, UNKNOWN_APP
|
||||||
|
from pyvizio.helpers import find_app_name
|
||||||
|
|
||||||
from homeassistant.components.media_player import MediaPlayerDevice
|
from homeassistant.components.media_player import (
|
||||||
|
DEVICE_CLASS_SPEAKER,
|
||||||
|
MediaPlayerDevice,
|
||||||
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_ACCESS_TOKEN,
|
CONF_ACCESS_TOKEN,
|
||||||
CONF_DEVICE_CLASS,
|
CONF_DEVICE_CLASS,
|
||||||
|
CONF_EXCLUDE,
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
|
CONF_INCLUDE,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
@ -25,6 +32,8 @@ from homeassistant.helpers.entity import Entity
|
|||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_ADDITIONAL_CONFIGS,
|
||||||
|
CONF_APPS,
|
||||||
CONF_VOLUME_STEP,
|
CONF_VOLUME_STEP,
|
||||||
DEFAULT_TIMEOUT,
|
DEFAULT_TIMEOUT,
|
||||||
DEFAULT_VOLUME_STEP,
|
DEFAULT_VOLUME_STEP,
|
||||||
@ -51,6 +60,7 @@ async def async_setup_entry(
|
|||||||
token = config_entry.data.get(CONF_ACCESS_TOKEN)
|
token = config_entry.data.get(CONF_ACCESS_TOKEN)
|
||||||
name = config_entry.data[CONF_NAME]
|
name = config_entry.data[CONF_NAME]
|
||||||
device_class = config_entry.data[CONF_DEVICE_CLASS]
|
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
|
# If config entry options not set up, set them up, otherwise assign values managed in options
|
||||||
volume_step = config_entry.options.get(
|
volume_step = config_entry.options.get(
|
||||||
@ -83,7 +93,9 @@ async def async_setup_entry(
|
|||||||
_LOGGER.warning("Failed to connect to %s", host)
|
_LOGGER.warning("Failed to connect to %s", host)
|
||||||
raise PlatformNotReady
|
raise PlatformNotReady
|
||||||
|
|
||||||
entity = VizioDevice(config_entry, device, name, volume_step, device_class)
|
entity = VizioDevice(
|
||||||
|
config_entry, device, name, volume_step, device_class, conf_apps,
|
||||||
|
)
|
||||||
|
|
||||||
async_add_entities([entity], update_before_add=True)
|
async_add_entities([entity], update_before_add=True)
|
||||||
|
|
||||||
@ -98,6 +110,7 @@ class VizioDevice(MediaPlayerDevice):
|
|||||||
name: str,
|
name: str,
|
||||||
volume_step: int,
|
volume_step: int,
|
||||||
device_class: str,
|
device_class: str,
|
||||||
|
conf_apps: Dict[str, List[Any]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize Vizio device."""
|
"""Initialize Vizio device."""
|
||||||
self._config_entry = config_entry
|
self._config_entry = config_entry
|
||||||
@ -109,7 +122,11 @@ class VizioDevice(MediaPlayerDevice):
|
|||||||
self._volume_step = volume_step
|
self._volume_step = volume_step
|
||||||
self._is_muted = None
|
self._is_muted = None
|
||||||
self._current_input = None
|
self._current_input = None
|
||||||
self._available_inputs = 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._device_class = device_class
|
self._device_class = device_class
|
||||||
self._supported_commands = SUPPORTED_COMMANDS[device_class]
|
self._supported_commands = SUPPORTED_COMMANDS[device_class]
|
||||||
self._device = device
|
self._device = device
|
||||||
@ -119,6 +136,30 @@ class VizioDevice(MediaPlayerDevice):
|
|||||||
self._model = None
|
self._model = None
|
||||||
self._sw_version = None
|
self._sw_version = None
|
||||||
|
|
||||||
|
def _apps_list(self, apps: List[str]) -> List[str]:
|
||||||
|
"""Return process apps list based on configured filters."""
|
||||||
|
if self._conf_apps.get(CONF_INCLUDE):
|
||||||
|
return [app for app in apps if app in self._conf_apps[CONF_INCLUDE]]
|
||||||
|
|
||||||
|
if self._conf_apps.get(CONF_EXCLUDE):
|
||||||
|
return [app for app in apps if app not in self._conf_apps[CONF_EXCLUDE]]
|
||||||
|
|
||||||
|
return apps
|
||||||
|
|
||||||
|
async def _current_app_name(self) -> Optional[str]:
|
||||||
|
"""Return name of the currently running app by parsing pyvizio output."""
|
||||||
|
app = await self._device.get_current_app(log_api_exception=False)
|
||||||
|
if app in [None, NO_APP_RUNNING]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if app == UNKNOWN_APP and self._additional_app_configs:
|
||||||
|
return find_app_name(
|
||||||
|
await self._device.get_current_app_config(log_api_exception=False),
|
||||||
|
self._additional_app_configs,
|
||||||
|
)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Retrieve latest state of the device."""
|
"""Retrieve latest state of the device."""
|
||||||
if not self._model:
|
if not self._model:
|
||||||
@ -149,6 +190,7 @@ class VizioDevice(MediaPlayerDevice):
|
|||||||
self._is_muted = None
|
self._is_muted = None
|
||||||
self._current_input = None
|
self._current_input = None
|
||||||
self._available_inputs = None
|
self._available_inputs = None
|
||||||
|
self._available_apps = None
|
||||||
return
|
return
|
||||||
|
|
||||||
self._state = STATE_ON
|
self._state = STATE_ON
|
||||||
@ -165,8 +207,33 @@ class VizioDevice(MediaPlayerDevice):
|
|||||||
self._current_input = input_
|
self._current_input = input_
|
||||||
|
|
||||||
inputs = await self._device.get_inputs_list(log_api_exception=False)
|
inputs = await self._device.get_inputs_list(log_api_exception=False)
|
||||||
if inputs is not None:
|
|
||||||
self._available_inputs = [input_.name for input_ in inputs]
|
# If no inputs returned, end update
|
||||||
|
if not inputs:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._available_inputs = [input_.name for input_ in inputs]
|
||||||
|
|
||||||
|
# Return before setting app variables if INPUT_APPS isn't in available inputs
|
||||||
|
if self._device_class == DEVICE_CLASS_SPEAKER or not any(
|
||||||
|
app for app in INPUT_APPS if app in self._available_inputs
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create list of available known apps from known app list after
|
||||||
|
# filtering by CONF_INCLUDE/CONF_EXCLUDE
|
||||||
|
if not self._available_apps:
|
||||||
|
self._available_apps = self._apps_list(self._device.get_apps_list())
|
||||||
|
|
||||||
|
# Attempt to get current app name. If app name is unknown, check list
|
||||||
|
# of additional apps specified in configuration
|
||||||
|
self._current_app = await self._current_app_name()
|
||||||
|
|
||||||
|
def _get_additional_app_names(self) -> List[Dict[str, Any]]:
|
||||||
|
"""Return list of additional apps that were included in configuration.yaml."""
|
||||||
|
return [
|
||||||
|
additional_app["name"] for additional_app in self._additional_app_configs
|
||||||
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _async_send_update_options_signal(
|
async def _async_send_update_options_signal(
|
||||||
@ -237,13 +304,39 @@ class VizioDevice(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def source(self) -> str:
|
def source(self) -> str:
|
||||||
"""Return current input of the device."""
|
"""Return current input of the device."""
|
||||||
|
if self._current_input in INPUT_APPS:
|
||||||
|
return self._current_app
|
||||||
|
|
||||||
return self._current_input
|
return self._current_input
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source_list(self) -> List:
|
def source_list(self) -> List[str]:
|
||||||
"""Return list of available inputs of the device."""
|
"""Return list of available inputs of the device."""
|
||||||
|
# If Smartcast app is in input list, and the app list has been retrieved,
|
||||||
|
# show the combination with , otherwise just return inputs
|
||||||
|
if self._available_apps:
|
||||||
|
return [
|
||||||
|
*[
|
||||||
|
_input
|
||||||
|
for _input in self._available_inputs
|
||||||
|
if _input not in INPUT_APPS
|
||||||
|
],
|
||||||
|
*self._available_apps,
|
||||||
|
*self._get_additional_app_names(),
|
||||||
|
]
|
||||||
|
|
||||||
return self._available_inputs
|
return self._available_inputs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def app_id(self) -> Optional[str]:
|
||||||
|
"""Return the current app."""
|
||||||
|
return self._current_app
|
||||||
|
|
||||||
|
@property
|
||||||
|
def app_name(self) -> Optional[str]:
|
||||||
|
"""Return the friendly name of the current app."""
|
||||||
|
return self._current_app
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self) -> int:
|
def supported_features(self) -> int:
|
||||||
"""Flag device features that are supported."""
|
"""Flag device features that are supported."""
|
||||||
@ -297,7 +390,18 @@ class VizioDevice(MediaPlayerDevice):
|
|||||||
|
|
||||||
async def async_select_source(self, source: str) -> None:
|
async def async_select_source(self, source: str) -> None:
|
||||||
"""Select input source."""
|
"""Select input source."""
|
||||||
await self._device.set_input(source)
|
if source in self._available_inputs:
|
||||||
|
await self._device.set_input(source)
|
||||||
|
elif source in self._get_additional_app_names():
|
||||||
|
await self._device.launch_app_config(
|
||||||
|
**next(
|
||||||
|
app["config"]
|
||||||
|
for app in self._additional_app_configs
|
||||||
|
if app["name"] == source
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif source in self._available_apps:
|
||||||
|
await self._device.launch_app(source)
|
||||||
|
|
||||||
async def async_volume_up(self) -> None:
|
async def async_volume_up(self) -> None:
|
||||||
"""Increase volume of the device."""
|
"""Increase volume of the device."""
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Setup Vizio SmartCast Device",
|
"title": "Setup Vizio SmartCast Device",
|
||||||
"description": "All fields are required except Access Token. If you choose not to provide an Access Token, and your Device Type is 'tv', you will go through a pairing process with your device so an Access Token can be retrieved.\n\nTo go through the pairing process, before clicking Submit, ensure your TV is powered on and connected to the network. You also need to be able to see the screen.",
|
"description": "An Access Token is only needed for TVs. If you are configuring a TV and do not have an Access Token yet, leave it blank to go through a pairing process.",
|
||||||
"data": {
|
"data": {
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"host": "<Host/IP>:<Port>",
|
"host": "<Host/IP>:<Port>",
|
||||||
@ -19,13 +19,17 @@
|
|||||||
"pin": "PIN"
|
"pin": "PIN"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pairing_complete": {
|
|
||||||
"title": "Pairing Complete",
|
|
||||||
"description": "Your Vizio SmartCast device is now connected to Home Assistant."
|
|
||||||
},
|
|
||||||
"pairing_complete_import": {
|
"pairing_complete_import": {
|
||||||
"title": "Pairing Complete",
|
"title": "Pairing Complete",
|
||||||
"description": "Your Vizio SmartCast device is now connected to Home Assistant.\n\nYour Access Token is '**{access_token}**'."
|
"description": "Your Vizio SmartCast TV is now connected to Home Assistant.\n\nYour Access Token is '**{access_token}**'."
|
||||||
|
},
|
||||||
|
"user_tv": {
|
||||||
|
"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": {
|
"error": {
|
||||||
@ -36,7 +40,7 @@
|
|||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_setup": "This entry has already been setup.",
|
"already_setup": "This entry has already been setup.",
|
||||||
"updated_entry": "This entry has already been setup but the name and/or options defined in the configuration do not match the previously imported configuration, so the configuration entry has been updated accordingly."
|
"updated_entry": "This entry has already been setup but the name, apps, and/or options defined in the configuration do not match the previously imported configuration, so the configuration entry has been updated accordingly."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
|
@ -1717,7 +1717,7 @@ pyversasense==0.0.6
|
|||||||
pyvesync==1.1.0
|
pyvesync==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.vizio
|
# homeassistant.components.vizio
|
||||||
pyvizio==0.1.26
|
pyvizio==0.1.35
|
||||||
|
|
||||||
# homeassistant.components.velux
|
# homeassistant.components.velux
|
||||||
pyvlx==0.2.12
|
pyvlx==0.2.12
|
||||||
|
@ -602,7 +602,7 @@ pyvera==0.3.7
|
|||||||
pyvesync==1.1.0
|
pyvesync==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.vizio
|
# homeassistant.components.vizio
|
||||||
pyvizio==0.1.26
|
pyvizio==0.1.35
|
||||||
|
|
||||||
# homeassistant.components.html5
|
# homeassistant.components.html5
|
||||||
pywebpush==1.9.2
|
pywebpush==1.9.2
|
||||||
|
@ -5,9 +5,12 @@ from pyvizio.const import DEVICE_CLASS_SPEAKER, MAX_VOLUME
|
|||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ACCESS_TOKEN,
|
ACCESS_TOKEN,
|
||||||
|
APP_LIST,
|
||||||
CH_TYPE,
|
CH_TYPE,
|
||||||
|
CURRENT_APP,
|
||||||
CURRENT_INPUT,
|
CURRENT_INPUT,
|
||||||
INPUT_LIST,
|
INPUT_LIST,
|
||||||
|
INPUT_LIST_WITH_APPS,
|
||||||
MODEL,
|
MODEL,
|
||||||
RESPONSE_TOKEN,
|
RESPONSE_TOKEN,
|
||||||
UNIQUE_ID,
|
UNIQUE_ID,
|
||||||
@ -154,3 +157,22 @@ def vizio_update_fixture():
|
|||||||
return_value=VERSION,
|
return_value=VERSION,
|
||||||
):
|
):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="vizio_update_with_apps")
|
||||||
|
def vizio_update_with_apps_fixture(vizio_update: pytest.fixture):
|
||||||
|
"""Mock valid updates to vizio device that supports apps."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.vizio.media_player.VizioAsync.get_inputs_list",
|
||||||
|
return_value=get_mock_inputs(INPUT_LIST_WITH_APPS),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.vizio.media_player.VizioAsync.get_apps_list",
|
||||||
|
return_value=APP_LIST,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.vizio.media_player.VizioAsync.get_current_input",
|
||||||
|
return_value="CAST",
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.vizio.media_player.VizioAsync.get_current_app",
|
||||||
|
return_value=CURRENT_APP,
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
@ -6,11 +6,23 @@ from homeassistant.components.media_player import (
|
|||||||
DEVICE_CLASS_TV,
|
DEVICE_CLASS_TV,
|
||||||
DOMAIN as MP_DOMAIN,
|
DOMAIN as MP_DOMAIN,
|
||||||
)
|
)
|
||||||
from homeassistant.components.vizio.const import CONF_VOLUME_STEP
|
from homeassistant.components.vizio.const import (
|
||||||
|
CONF_ADDITIONAL_CONFIGS,
|
||||||
|
CONF_APP_ID,
|
||||||
|
CONF_APPS,
|
||||||
|
CONF_APPS_TO_INCLUDE_OR_EXCLUDE,
|
||||||
|
CONF_CONFIG,
|
||||||
|
CONF_INCLUDE_OR_EXCLUDE,
|
||||||
|
CONF_MESSAGE,
|
||||||
|
CONF_NAME_SPACE,
|
||||||
|
CONF_VOLUME_STEP,
|
||||||
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_ACCESS_TOKEN,
|
CONF_ACCESS_TOKEN,
|
||||||
CONF_DEVICE_CLASS,
|
CONF_DEVICE_CLASS,
|
||||||
|
CONF_EXCLUDE,
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
|
CONF_INCLUDE,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_PIN,
|
CONF_PIN,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
@ -52,6 +64,22 @@ class MockCompletePairingResponse(object):
|
|||||||
self.auth_token = auth_token
|
self.auth_token = auth_token
|
||||||
|
|
||||||
|
|
||||||
|
CURRENT_INPUT = "HDMI"
|
||||||
|
INPUT_LIST = ["HDMI", "USB", "Bluetooth", "AUX"]
|
||||||
|
|
||||||
|
CURRENT_APP = "Hulu"
|
||||||
|
APP_LIST = ["Hulu", "Netflix"]
|
||||||
|
INPUT_LIST_WITH_APPS = INPUT_LIST + ["CAST"]
|
||||||
|
CUSTOM_APP_NAME = "APP3"
|
||||||
|
CUSTOM_CONFIG = {CONF_APP_ID: "test", CONF_MESSAGE: None, CONF_NAME_SPACE: 10}
|
||||||
|
ADDITIONAL_APP_CONFIG = {
|
||||||
|
"name": CUSTOM_APP_NAME,
|
||||||
|
CONF_CONFIG: CUSTOM_CONFIG,
|
||||||
|
}
|
||||||
|
|
||||||
|
ENTITY_ID = f"{MP_DOMAIN}.{slugify(NAME)}"
|
||||||
|
|
||||||
|
|
||||||
MOCK_PIN_CONFIG = {CONF_PIN: PIN}
|
MOCK_PIN_CONFIG = {CONF_PIN: PIN}
|
||||||
|
|
||||||
MOCK_USER_VALID_TV_CONFIG = {
|
MOCK_USER_VALID_TV_CONFIG = {
|
||||||
@ -73,6 +101,58 @@ MOCK_IMPORT_VALID_TV_CONFIG = {
|
|||||||
CONF_VOLUME_STEP: VOLUME_STEP,
|
CONF_VOLUME_STEP: VOLUME_STEP,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MOCK_TV_WITH_INCLUDE_CONFIG = {
|
||||||
|
CONF_NAME: NAME,
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_DEVICE_CLASS: DEVICE_CLASS_TV,
|
||||||
|
CONF_ACCESS_TOKEN: ACCESS_TOKEN,
|
||||||
|
CONF_VOLUME_STEP: VOLUME_STEP,
|
||||||
|
CONF_APPS: {CONF_INCLUDE: [CURRENT_APP]},
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_TV_WITH_EXCLUDE_CONFIG = {
|
||||||
|
CONF_NAME: NAME,
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_DEVICE_CLASS: DEVICE_CLASS_TV,
|
||||||
|
CONF_ACCESS_TOKEN: ACCESS_TOKEN,
|
||||||
|
CONF_VOLUME_STEP: VOLUME_STEP,
|
||||||
|
CONF_APPS: {CONF_EXCLUDE: ["Netflix"]},
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_TV_WITH_ADDITIONAL_APPS_CONFIG = {
|
||||||
|
CONF_NAME: NAME,
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_DEVICE_CLASS: DEVICE_CLASS_TV,
|
||||||
|
CONF_ACCESS_TOKEN: ACCESS_TOKEN,
|
||||||
|
CONF_VOLUME_STEP: VOLUME_STEP,
|
||||||
|
CONF_APPS: {CONF_ADDITIONAL_CONFIGS: [ADDITIONAL_APP_CONFIG]},
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_SPEAKER_APPS_FAILURE = {
|
||||||
|
CONF_NAME: NAME,
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER,
|
||||||
|
CONF_ACCESS_TOKEN: ACCESS_TOKEN,
|
||||||
|
CONF_VOLUME_STEP: VOLUME_STEP,
|
||||||
|
CONF_APPS: {CONF_ADDITIONAL_CONFIGS: [ADDITIONAL_APP_CONFIG]},
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_TV_APPS_FAILURE = {
|
||||||
|
CONF_NAME: NAME,
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_DEVICE_CLASS: DEVICE_CLASS_TV,
|
||||||
|
CONF_ACCESS_TOKEN: ACCESS_TOKEN,
|
||||||
|
CONF_VOLUME_STEP: VOLUME_STEP,
|
||||||
|
CONF_APPS: None,
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_TV_APPS_WITH_VALID_APPS_CONFIG = {
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_DEVICE_CLASS: DEVICE_CLASS_TV,
|
||||||
|
CONF_ACCESS_TOKEN: ACCESS_TOKEN,
|
||||||
|
CONF_APPS: {CONF_INCLUDE: [CURRENT_APP]},
|
||||||
|
}
|
||||||
|
|
||||||
MOCK_TV_CONFIG_NO_TOKEN = {
|
MOCK_TV_CONFIG_NO_TOKEN = {
|
||||||
CONF_NAME: NAME,
|
CONF_NAME: NAME,
|
||||||
CONF_HOST: HOST,
|
CONF_HOST: HOST,
|
||||||
@ -85,6 +165,15 @@ MOCK_SPEAKER_CONFIG = {
|
|||||||
CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER,
|
CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MOCK_INCLUDE_APPS = {
|
||||||
|
CONF_INCLUDE_OR_EXCLUDE: CONF_INCLUDE.title(),
|
||||||
|
CONF_APPS_TO_INCLUDE_OR_EXCLUDE: [CURRENT_APP],
|
||||||
|
}
|
||||||
|
MOCK_INCLUDE_NO_APPS = {
|
||||||
|
CONF_INCLUDE_OR_EXCLUDE: CONF_INCLUDE.title(),
|
||||||
|
CONF_APPS_TO_INCLUDE_OR_EXCLUDE: [],
|
||||||
|
}
|
||||||
|
|
||||||
VIZIO_ZEROCONF_SERVICE_TYPE = "_viziocast._tcp.local."
|
VIZIO_ZEROCONF_SERVICE_TYPE = "_viziocast._tcp.local."
|
||||||
ZEROCONF_NAME = f"{NAME}.{VIZIO_ZEROCONF_SERVICE_TYPE}"
|
ZEROCONF_NAME = f"{NAME}.{VIZIO_ZEROCONF_SERVICE_TYPE}"
|
||||||
ZEROCONF_HOST = HOST.split(":")[0]
|
ZEROCONF_HOST = HOST.split(":")[0]
|
||||||
@ -97,8 +186,3 @@ MOCK_ZEROCONF_SERVICE_INFO = {
|
|||||||
CONF_PORT: ZEROCONF_PORT,
|
CONF_PORT: ZEROCONF_PORT,
|
||||||
"properties": {"name": "SB4031-D5"},
|
"properties": {"name": "SB4031-D5"},
|
||||||
}
|
}
|
||||||
|
|
||||||
CURRENT_INPUT = "HDMI"
|
|
||||||
INPUT_LIST = ["HDMI", "USB", "Bluetooth", "AUX"]
|
|
||||||
|
|
||||||
ENTITY_ID = f"{MP_DOMAIN}.{slugify(NAME)}"
|
|
||||||
|
@ -6,7 +6,12 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant import data_entry_flow
|
from homeassistant import data_entry_flow
|
||||||
from homeassistant.components.media_player import DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV
|
from homeassistant.components.media_player import DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV
|
||||||
|
from homeassistant.components.vizio.config_flow import _get_config_schema
|
||||||
from homeassistant.components.vizio.const import (
|
from homeassistant.components.vizio.const import (
|
||||||
|
CONF_APPS,
|
||||||
|
CONF_APPS_TO_INCLUDE_OR_EXCLUDE,
|
||||||
|
CONF_INCLUDE,
|
||||||
|
CONF_INCLUDE_OR_EXCLUDE,
|
||||||
CONF_VOLUME_STEP,
|
CONF_VOLUME_STEP,
|
||||||
DEFAULT_NAME,
|
DEFAULT_NAME,
|
||||||
DEFAULT_VOLUME_STEP,
|
DEFAULT_VOLUME_STEP,
|
||||||
@ -25,12 +30,16 @@ from homeassistant.helpers.typing import HomeAssistantType
|
|||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ACCESS_TOKEN,
|
ACCESS_TOKEN,
|
||||||
|
CURRENT_APP,
|
||||||
HOST,
|
HOST,
|
||||||
HOST2,
|
HOST2,
|
||||||
MOCK_IMPORT_VALID_TV_CONFIG,
|
MOCK_IMPORT_VALID_TV_CONFIG,
|
||||||
|
MOCK_INCLUDE_APPS,
|
||||||
|
MOCK_INCLUDE_NO_APPS,
|
||||||
MOCK_PIN_CONFIG,
|
MOCK_PIN_CONFIG,
|
||||||
MOCK_SPEAKER_CONFIG,
|
MOCK_SPEAKER_CONFIG,
|
||||||
MOCK_TV_CONFIG_NO_TOKEN,
|
MOCK_TV_CONFIG_NO_TOKEN,
|
||||||
|
MOCK_TV_WITH_EXCLUDE_CONFIG,
|
||||||
MOCK_USER_VALID_TV_CONFIG,
|
MOCK_USER_VALID_TV_CONFIG,
|
||||||
MOCK_ZEROCONF_SERVICE_INFO,
|
MOCK_ZEROCONF_SERVICE_INFO,
|
||||||
NAME,
|
NAME,
|
||||||
@ -86,12 +95,48 @@ async def test_user_flow_all_fields(
|
|||||||
result["flow_id"], user_input=MOCK_USER_VALID_TV_CONFIG
|
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["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
assert result["title"] == NAME
|
assert result["title"] == NAME
|
||||||
assert result["data"][CONF_NAME] == NAME
|
assert result["data"][CONF_NAME] == NAME
|
||||||
assert result["data"][CONF_HOST] == HOST
|
assert result["data"][CONF_HOST] == HOST
|
||||||
assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_TV
|
assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_TV
|
||||||
assert result["data"][CONF_ACCESS_TOKEN] == ACCESS_TOKEN
|
assert result["data"][CONF_ACCESS_TOKEN] == ACCESS_TOKEN
|
||||||
|
assert result["data"][CONF_APPS][CONF_INCLUDE] == [CURRENT_APP]
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
async def test_options_flow(hass: HomeAssistantType) -> None:
|
||||||
@ -218,13 +263,13 @@ async def test_user_error_on_could_not_connect(
|
|||||||
assert result["errors"] == {"base": "cant_connect"}
|
assert result["errors"] == {"base": "cant_connect"}
|
||||||
|
|
||||||
|
|
||||||
async def test_user_tv_pairing(
|
async def test_user_tv_pairing_no_apps(
|
||||||
hass: HomeAssistantType,
|
hass: HomeAssistantType,
|
||||||
vizio_connect: pytest.fixture,
|
vizio_connect: pytest.fixture,
|
||||||
vizio_bypass_setup: pytest.fixture,
|
vizio_bypass_setup: pytest.fixture,
|
||||||
vizio_complete_pairing: pytest.fixture,
|
vizio_complete_pairing: pytest.fixture,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test pairing config flow when access token not provided for tv during user entry."""
|
"""Test pairing config flow when access token not provided for tv during user entry and no apps configured."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN
|
DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN
|
||||||
)
|
)
|
||||||
@ -237,15 +282,18 @@ async def test_user_tv_pairing(
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["step_id"] == "pairing_complete"
|
assert result["step_id"] == "tv_apps"
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=MOCK_INCLUDE_NO_APPS
|
||||||
|
)
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
assert result["title"] == NAME
|
assert result["title"] == NAME
|
||||||
assert result["data"][CONF_NAME] == NAME
|
assert result["data"][CONF_NAME] == NAME
|
||||||
assert result["data"][CONF_HOST] == HOST
|
assert result["data"][CONF_HOST] == HOST
|
||||||
assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_TV
|
assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_TV
|
||||||
|
assert CONF_APPS not in result["data"]
|
||||||
|
|
||||||
|
|
||||||
async def test_user_start_pairing_failure(
|
async def test_user_start_pairing_failure(
|
||||||
@ -385,12 +433,12 @@ async def test_import_flow_update_options(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_import_flow_update_name(
|
async def test_import_flow_update_name_and_apps(
|
||||||
hass: HomeAssistantType,
|
hass: HomeAssistantType,
|
||||||
vizio_connect: pytest.fixture,
|
vizio_connect: pytest.fixture,
|
||||||
vizio_bypass_update: pytest.fixture,
|
vizio_bypass_update: pytest.fixture,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test import config flow with updated name."""
|
"""Test import config flow with updated name and apps."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
context={"source": SOURCE_IMPORT},
|
context={"source": SOURCE_IMPORT},
|
||||||
@ -404,6 +452,7 @@ async def test_import_flow_update_name(
|
|||||||
|
|
||||||
updated_config = MOCK_IMPORT_VALID_TV_CONFIG.copy()
|
updated_config = MOCK_IMPORT_VALID_TV_CONFIG.copy()
|
||||||
updated_config[CONF_NAME] = NAME2
|
updated_config[CONF_NAME] = NAME2
|
||||||
|
updated_config[CONF_APPS] = {CONF_INCLUDE: [CURRENT_APP]}
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
context={"source": SOURCE_IMPORT},
|
context={"source": SOURCE_IMPORT},
|
||||||
@ -413,6 +462,39 @@ async def test_import_flow_update_name(
|
|||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
assert result["reason"] == "updated_entry"
|
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_NAME] == NAME2
|
||||||
|
assert hass.config_entries.async_get_entry(entry_id).data[CONF_APPS] == {
|
||||||
|
CONF_INCLUDE: [CURRENT_APP]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import_flow_update_remove_apps(
|
||||||
|
hass: HomeAssistantType,
|
||||||
|
vizio_connect: pytest.fixture,
|
||||||
|
vizio_bypass_update: pytest.fixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test import config flow with removed apps."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_IMPORT},
|
||||||
|
data=vol.Schema(VIZIO_SCHEMA)(MOCK_TV_WITH_EXCLUDE_CONFIG),
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["result"].data[CONF_NAME] == NAME
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
entry_id = result["result"].entry_id
|
||||||
|
|
||||||
|
updated_config = MOCK_TV_WITH_EXCLUDE_CONFIG.copy()
|
||||||
|
updated_config.pop(CONF_APPS)
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_IMPORT},
|
||||||
|
data=vol.Schema(VIZIO_SCHEMA)(updated_config),
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
async def test_import_needs_pairing(
|
async def test_import_needs_pairing(
|
||||||
@ -452,6 +534,49 @@ async def test_import_needs_pairing(
|
|||||||
assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_TV
|
assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_TV
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import_with_apps_needs_pairing(
|
||||||
|
hass: HomeAssistantType,
|
||||||
|
vizio_connect: pytest.fixture,
|
||||||
|
vizio_bypass_setup: pytest.fixture,
|
||||||
|
vizio_complete_pairing: pytest.fixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test pairing config flow when access token not provided for tv but apps are included during import."""
|
||||||
|
import_config = MOCK_TV_CONFIG_NO_TOKEN.copy()
|
||||||
|
import_config[CONF_APPS] = {CONF_INCLUDE: [CURRENT_APP]}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=import_config
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
# Mock inputting info without apps to make sure apps get stored
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=_get_config_schema(MOCK_TV_CONFIG_NO_TOKEN)(MOCK_TV_CONFIG_NO_TOKEN),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "pair_tv"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=MOCK_PIN_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "pairing_complete_import"
|
||||||
|
|
||||||
|
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
|
||||||
|
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_APPS][CONF_INCLUDE] == [CURRENT_APP]
|
||||||
|
|
||||||
|
|
||||||
async def test_import_error(
|
async def test_import_error(
|
||||||
hass: HomeAssistantType,
|
hass: HomeAssistantType,
|
||||||
vizio_connect: pytest.fixture,
|
vizio_connect: pytest.fixture,
|
||||||
|
@ -1,14 +1,21 @@
|
|||||||
"""Tests for Vizio config flow."""
|
"""Tests for Vizio config flow."""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict
|
||||||
from unittest.mock import call
|
from unittest.mock import call
|
||||||
|
|
||||||
from asynctest import patch
|
from asynctest import patch
|
||||||
import pytest
|
import pytest
|
||||||
|
from pytest import raises
|
||||||
|
from pyvizio._api.apps import AppConfig
|
||||||
from pyvizio.const import (
|
from pyvizio.const import (
|
||||||
DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER,
|
DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER,
|
||||||
DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV,
|
DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV,
|
||||||
|
INPUT_APPS,
|
||||||
MAX_VOLUME,
|
MAX_VOLUME,
|
||||||
|
UNKNOWN_APP,
|
||||||
)
|
)
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
ATTR_INPUT_SOURCE,
|
ATTR_INPUT_SOURCE,
|
||||||
@ -27,16 +34,41 @@ from homeassistant.components.media_player import (
|
|||||||
SERVICE_VOLUME_SET,
|
SERVICE_VOLUME_SET,
|
||||||
SERVICE_VOLUME_UP,
|
SERVICE_VOLUME_UP,
|
||||||
)
|
)
|
||||||
from homeassistant.components.vizio.const import CONF_VOLUME_STEP, DOMAIN
|
from homeassistant.components.vizio import validate_apps
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE
|
from homeassistant.components.vizio.const import (
|
||||||
|
CONF_ADDITIONAL_CONFIGS,
|
||||||
|
CONF_APPS,
|
||||||
|
CONF_VOLUME_STEP,
|
||||||
|
DOMAIN,
|
||||||
|
VIZIO_SCHEMA,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
CONF_EXCLUDE,
|
||||||
|
CONF_INCLUDE,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_ON,
|
||||||
|
STATE_UNAVAILABLE,
|
||||||
|
)
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
ADDITIONAL_APP_CONFIG,
|
||||||
|
APP_LIST,
|
||||||
|
CURRENT_APP,
|
||||||
CURRENT_INPUT,
|
CURRENT_INPUT,
|
||||||
|
CUSTOM_APP_NAME,
|
||||||
|
CUSTOM_CONFIG,
|
||||||
ENTITY_ID,
|
ENTITY_ID,
|
||||||
INPUT_LIST,
|
INPUT_LIST,
|
||||||
|
INPUT_LIST_WITH_APPS,
|
||||||
|
MOCK_SPEAKER_APPS_FAILURE,
|
||||||
MOCK_SPEAKER_CONFIG,
|
MOCK_SPEAKER_CONFIG,
|
||||||
|
MOCK_TV_APPS_FAILURE,
|
||||||
|
MOCK_TV_WITH_ADDITIONAL_APPS_CONFIG,
|
||||||
|
MOCK_TV_WITH_EXCLUDE_CONFIG,
|
||||||
|
MOCK_TV_WITH_INCLUDE_CONFIG,
|
||||||
MOCK_USER_VALID_TV_CONFIG,
|
MOCK_USER_VALID_TV_CONFIG,
|
||||||
NAME,
|
NAME,
|
||||||
UNIQUE_ID,
|
UNIQUE_ID,
|
||||||
@ -45,6 +77,8 @@ from .const import (
|
|||||||
|
|
||||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def _test_setup(
|
async def _test_setup(
|
||||||
hass: HomeAssistantType, ha_device_class: str, vizio_power_state: bool
|
hass: HomeAssistantType, ha_device_class: str, vizio_power_state: bool
|
||||||
@ -60,12 +94,16 @@ async def _test_setup(
|
|||||||
if ha_device_class == DEVICE_CLASS_SPEAKER:
|
if ha_device_class == DEVICE_CLASS_SPEAKER:
|
||||||
vizio_device_class = VIZIO_DEVICE_CLASS_SPEAKER
|
vizio_device_class = VIZIO_DEVICE_CLASS_SPEAKER
|
||||||
config_entry = MockConfigEntry(
|
config_entry = MockConfigEntry(
|
||||||
domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, unique_id=UNIQUE_ID
|
domain=DOMAIN,
|
||||||
|
data=vol.Schema(VIZIO_SCHEMA)(MOCK_SPEAKER_CONFIG),
|
||||||
|
unique_id=UNIQUE_ID,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
vizio_device_class = VIZIO_DEVICE_CLASS_TV
|
vizio_device_class = VIZIO_DEVICE_CLASS_TV
|
||||||
config_entry = MockConfigEntry(
|
config_entry = MockConfigEntry(
|
||||||
domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG, unique_id=UNIQUE_ID
|
domain=DOMAIN,
|
||||||
|
data=vol.Schema(VIZIO_SCHEMA)(MOCK_USER_VALID_TV_CONFIG),
|
||||||
|
unique_id=UNIQUE_ID,
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
@ -94,6 +132,72 @@ async def _test_setup(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _test_setup_with_apps(
|
||||||
|
hass: HomeAssistantType, device_config: Dict[str, Any], app: str
|
||||||
|
) -> None:
|
||||||
|
"""Test Vizio Device with apps entity setup."""
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN, data=vol.Schema(VIZIO_SCHEMA)(device_config), unique_id=UNIQUE_ID
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.vizio.media_player.VizioAsync.get_all_audio_settings",
|
||||||
|
return_value={
|
||||||
|
"volume": int(MAX_VOLUME[VIZIO_DEVICE_CLASS_TV] / 2),
|
||||||
|
"mute": "Off",
|
||||||
|
},
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.vizio.media_player.VizioAsync.get_power_state",
|
||||||
|
return_value=True,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.vizio.media_player.VizioAsync.get_current_app",
|
||||||
|
return_value=app,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.vizio.media_player.VizioAsync.get_current_app_config",
|
||||||
|
return_value=AppConfig(**ADDITIONAL_APP_CONFIG["config"]),
|
||||||
|
):
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
attr = hass.states.get(ENTITY_ID).attributes
|
||||||
|
assert attr["friendly_name"] == NAME
|
||||||
|
assert attr["device_class"] == DEVICE_CLASS_TV
|
||||||
|
assert hass.states.get(ENTITY_ID).state == STATE_ON
|
||||||
|
|
||||||
|
if device_config.get(CONF_APPS, {}).get(CONF_INCLUDE) or device_config.get(
|
||||||
|
CONF_APPS, {}
|
||||||
|
).get(CONF_EXCLUDE):
|
||||||
|
list_to_test = list(INPUT_LIST_WITH_APPS + [CURRENT_APP])
|
||||||
|
elif device_config.get(CONF_APPS, {}).get(CONF_ADDITIONAL_CONFIGS):
|
||||||
|
list_to_test = list(
|
||||||
|
INPUT_LIST_WITH_APPS
|
||||||
|
+ APP_LIST
|
||||||
|
+ [
|
||||||
|
app["name"]
|
||||||
|
for app in device_config[CONF_APPS][CONF_ADDITIONAL_CONFIGS]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
list_to_test = list(INPUT_LIST_WITH_APPS + APP_LIST)
|
||||||
|
|
||||||
|
for app_to_remove in INPUT_APPS:
|
||||||
|
if app_to_remove in list_to_test:
|
||||||
|
list_to_test.remove(app_to_remove)
|
||||||
|
|
||||||
|
assert attr["source_list"] == list_to_test
|
||||||
|
assert app in attr["source_list"] or app == UNKNOWN_APP
|
||||||
|
if app == UNKNOWN_APP:
|
||||||
|
assert attr["source"] == ADDITIONAL_APP_CONFIG["name"]
|
||||||
|
else:
|
||||||
|
assert attr["source"] == app
|
||||||
|
assert (
|
||||||
|
attr["volume_level"]
|
||||||
|
== float(int(MAX_VOLUME[VIZIO_DEVICE_CLASS_TV] / 2))
|
||||||
|
/ MAX_VOLUME[VIZIO_DEVICE_CLASS_TV]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _test_setup_failure(hass: HomeAssistantType, config: str) -> None:
|
async def _test_setup_failure(hass: HomeAssistantType, config: str) -> None:
|
||||||
"""Test generic Vizio entity setup failure."""
|
"""Test generic Vizio entity setup failure."""
|
||||||
with patch(
|
with patch(
|
||||||
@ -311,3 +415,89 @@ async def test_update_available_to_unavailable(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test device becomes unavailable after being available."""
|
"""Test device becomes unavailable after being available."""
|
||||||
await _test_update_availability_switch(hass, True, None, caplog)
|
await _test_update_availability_switch(hass, True, None, caplog)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_with_apps(
|
||||||
|
hass: HomeAssistantType,
|
||||||
|
vizio_connect: pytest.fixture,
|
||||||
|
vizio_update_with_apps: pytest.fixture,
|
||||||
|
caplog: pytest.fixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test device setup with apps."""
|
||||||
|
await _test_setup_with_apps(hass, MOCK_USER_VALID_TV_CONFIG, CURRENT_APP)
|
||||||
|
await _test_service(
|
||||||
|
hass,
|
||||||
|
"launch_app",
|
||||||
|
SERVICE_SELECT_SOURCE,
|
||||||
|
{ATTR_INPUT_SOURCE: CURRENT_APP},
|
||||||
|
CURRENT_APP,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_with_apps_include(
|
||||||
|
hass: HomeAssistantType,
|
||||||
|
vizio_connect: pytest.fixture,
|
||||||
|
vizio_update_with_apps: pytest.fixture,
|
||||||
|
caplog: pytest.fixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test device setup with apps and apps["include"] in config."""
|
||||||
|
await _test_setup_with_apps(hass, MOCK_TV_WITH_INCLUDE_CONFIG, CURRENT_APP)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_with_apps_exclude(
|
||||||
|
hass: HomeAssistantType,
|
||||||
|
vizio_connect: pytest.fixture,
|
||||||
|
vizio_update_with_apps: pytest.fixture,
|
||||||
|
caplog: pytest.fixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test device setup with apps and apps["exclude"] in config."""
|
||||||
|
await _test_setup_with_apps(hass, MOCK_TV_WITH_EXCLUDE_CONFIG, CURRENT_APP)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_with_apps_additional_apps_config(
|
||||||
|
hass: HomeAssistantType,
|
||||||
|
vizio_connect: pytest.fixture,
|
||||||
|
vizio_update_with_apps: pytest.fixture,
|
||||||
|
caplog: pytest.fixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test device setup with apps and apps["additional_configs"] in config."""
|
||||||
|
await _test_setup_with_apps(hass, MOCK_TV_WITH_ADDITIONAL_APPS_CONFIG, UNKNOWN_APP)
|
||||||
|
|
||||||
|
await _test_service(
|
||||||
|
hass,
|
||||||
|
"launch_app",
|
||||||
|
SERVICE_SELECT_SOURCE,
|
||||||
|
{ATTR_INPUT_SOURCE: CURRENT_APP},
|
||||||
|
CURRENT_APP,
|
||||||
|
)
|
||||||
|
await _test_service(
|
||||||
|
hass,
|
||||||
|
"launch_app_config",
|
||||||
|
SERVICE_SELECT_SOURCE,
|
||||||
|
{ATTR_INPUT_SOURCE: CUSTOM_APP_NAME},
|
||||||
|
**CUSTOM_CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test that invalid app does nothing
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.vizio.media_player.VizioAsync.launch_app"
|
||||||
|
) as service_call1, patch(
|
||||||
|
"homeassistant.components.vizio.media_player.VizioAsync.launch_app_config"
|
||||||
|
) as service_call2:
|
||||||
|
await hass.services.async_call(
|
||||||
|
MP_DOMAIN,
|
||||||
|
SERVICE_SELECT_SOURCE,
|
||||||
|
service_data={ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "_"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert not service_call1.called
|
||||||
|
assert not service_call2.called
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_apps_config(hass: HomeAssistantType):
|
||||||
|
"""Test that schema validation fails on certain conditions."""
|
||||||
|
with raises(vol.Invalid):
|
||||||
|
vol.Schema(vol.All(VIZIO_SCHEMA, validate_apps))(MOCK_TV_APPS_FAILURE)
|
||||||
|
|
||||||
|
with raises(vol.Invalid):
|
||||||
|
vol.Schema(vol.All(VIZIO_SCHEMA, validate_apps))(MOCK_SPEAKER_APPS_FAILURE)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user