Add Config Flow support, Device Registry support, available property to vizio component (#30653)
* add config flow support, device registry support, available property * raise PlatformNotReady if HA cant connect to device * remove test logging statement and fix integration title * store import and last user input values so user can see errors next to value that caused error * add PARALLEL_UPDATES * add missing type hints * add missing type hints to tests * fix options config flow title * changes based on review * better key name for message when cant connect * fix missed update to key name * fix comments * remove logger from test which was used to debug and update test function names and docstrings to be more accurate * add __init__.py to vizio tests module * readded options flow and updated main component to handle options updates, set unique ID to serial, fixes based on review * pop hass.data in media_player unload instead of in __init__ since it is set in media_player * update requirements_all and requirements_test_all * make unique_id key name a constant * remove additional line breaks after docstrings * unload entries during test_user_flow and test_import_flow tests to hopefully reduce teardown time * try to speed up tests * remove unnecessary code, use event bus to track options updates, move patches to pytest fixtures and fix patch scoping * fix comment * remove translations from commit * suppress API error logging when checking for device availability as it can spam logs * update requirements_all and requirements_test_all * dont pass hass to entity since it is passed to entity anyway, remove entity unload from tests, other misc changes from review * fix clearing listeners * use config_entry unique ID for unique ID and use config_entry entry ID as update signal * update config flow based on suggested changes * update volume step on config import if it doesn't match config_entry volume step * update config_entry data and options with new volume step value * copy entry.data and entry.options before updating when updating config_entry * fix test_import_entity_already_configured
This commit is contained in:
parent
5fa7d6f22a
commit
ac771addc1
11 changed files with 669 additions and 72 deletions
|
@ -1,7 +1,7 @@
|
||||||
"""The vizio component."""
|
"""The vizio component."""
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_ACCESS_TOKEN,
|
CONF_ACCESS_TOKEN,
|
||||||
CONF_DEVICE_CLASS,
|
CONF_DEVICE_CLASS,
|
||||||
|
@ -9,13 +9,14 @@ from homeassistant.const import (
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_VOLUME_STEP,
|
CONF_VOLUME_STEP,
|
||||||
DEFAULT_DEVICE_CLASS,
|
DEFAULT_DEVICE_CLASS,
|
||||||
DEFAULT_NAME,
|
DEFAULT_NAME,
|
||||||
DEFAULT_VOLUME_STEP,
|
DEFAULT_VOLUME_STEP,
|
||||||
|
DOMAIN,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,3 +43,41 @@ VIZIO_SCHEMA = {
|
||||||
vol.Coerce(int), vol.Range(min=1, max=10)
|
vol.Coerce(int), vol.Range(min=1, max=10)
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
DOMAIN: vol.All(
|
||||||
|
cv.ensure_list, [vol.All(vol.Schema(VIZIO_SCHEMA), validate_auth)]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
||||||
|
"""Component setup, run import config flow for each entry in config."""
|
||||||
|
if DOMAIN in config:
|
||||||
|
for entry in config[DOMAIN]:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=entry
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
|
||||||
|
"""Load the saved entities."""
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(entry, "media_player")
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
await hass.config_entries.async_forward_entry_unload(entry, "media_player")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
171
homeassistant/components/vizio/config_flow.py
Normal file
171
homeassistant/components/vizio/config_flow.py
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
"""Config flow for Vizio."""
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from pyvizio import VizioAsync
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_ACCESS_TOKEN,
|
||||||
|
CONF_DEVICE_CLASS,
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_NAME,
|
||||||
|
)
|
||||||
|
from homeassistant.core import callback
|
||||||
|
|
||||||
|
from . import validate_auth
|
||||||
|
from .const import (
|
||||||
|
CONF_VOLUME_STEP,
|
||||||
|
DEFAULT_DEVICE_CLASS,
|
||||||
|
DEFAULT_NAME,
|
||||||
|
DEFAULT_VOLUME_STEP,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def update_schema_defaults(input_dict: Dict[str, Any]) -> vol.Schema:
|
||||||
|
"""Update schema defaults based on user input/config dict. Retains info already provided for future form views."""
|
||||||
|
return vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(
|
||||||
|
CONF_NAME, default=input_dict.get(CONF_NAME, DEFAULT_NAME)
|
||||||
|
): str,
|
||||||
|
vol.Required(CONF_HOST, default=input_dict.get(CONF_HOST)): str,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_DEVICE_CLASS,
|
||||||
|
default=input_dict.get(CONF_DEVICE_CLASS, DEFAULT_DEVICE_CLASS),
|
||||||
|
): vol.All(str, vol.Lower, vol.In(["tv", "soundbar"])),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_ACCESS_TOKEN, default=input_dict.get(CONF_ACCESS_TOKEN, "")
|
||||||
|
): str,
|
||||||
|
},
|
||||||
|
extra=vol.REMOVE_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a Vizio config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@callback
|
||||||
|
def async_get_options_flow(config_entry):
|
||||||
|
"""Get the options flow for this handler."""
|
||||||
|
return VizioOptionsConfigFlow(config_entry)
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Initialize config flow."""
|
||||||
|
self.import_schema = None
|
||||||
|
self.user_schema = None
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: Dict[str, Any] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
# Store current values in case setup fails and user needs to edit
|
||||||
|
self.user_schema = update_schema_defaults(user_input)
|
||||||
|
|
||||||
|
# Check if new config entry matches any existing config entries
|
||||||
|
for entry in self.hass.config_entries.async_entries(DOMAIN):
|
||||||
|
if entry.data[CONF_HOST] == user_input[CONF_HOST]:
|
||||||
|
errors[CONF_HOST] = "host_exists"
|
||||||
|
break
|
||||||
|
|
||||||
|
if entry.data[CONF_NAME] == user_input[CONF_NAME]:
|
||||||
|
errors[CONF_NAME] = "name_exists"
|
||||||
|
break
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
try:
|
||||||
|
# Ensure schema passes custom validation, otherwise catch exception and add error
|
||||||
|
validate_auth(user_input)
|
||||||
|
|
||||||
|
# Ensure config is valid for a device
|
||||||
|
if not await VizioAsync.validate_ha_config(
|
||||||
|
user_input[CONF_HOST],
|
||||||
|
user_input.get(CONF_ACCESS_TOKEN),
|
||||||
|
user_input[CONF_DEVICE_CLASS],
|
||||||
|
):
|
||||||
|
errors["base"] = "cant_connect"
|
||||||
|
except vol.Invalid:
|
||||||
|
errors["base"] = "tv_needs_token"
|
||||||
|
|
||||||
|
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],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Abort flow if existing component with same unique ID matches new config entry
|
||||||
|
if await self.async_set_unique_id(
|
||||||
|
unique_id=unique_id, raise_on_progress=True
|
||||||
|
):
|
||||||
|
return self.async_abort(reason="already_setup")
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=user_input[CONF_NAME], data=user_input
|
||||||
|
)
|
||||||
|
|
||||||
|
schema = self.user_schema or self.import_schema or update_schema_defaults({})
|
||||||
|
|
||||||
|
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
||||||
|
|
||||||
|
async def async_step_import(self, import_config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Import a config entry from configuration.yaml."""
|
||||||
|
# Check if new config entry matches any existing config entries
|
||||||
|
for entry in self.hass.config_entries.async_entries(DOMAIN):
|
||||||
|
if entry.data[CONF_HOST] == import_config[CONF_HOST] and entry.data[
|
||||||
|
CONF_NAME
|
||||||
|
] == import_config.get(CONF_NAME):
|
||||||
|
if entry.data[CONF_VOLUME_STEP] != import_config[CONF_VOLUME_STEP]:
|
||||||
|
new_volume_step = {
|
||||||
|
CONF_VOLUME_STEP: import_config[CONF_VOLUME_STEP]
|
||||||
|
}
|
||||||
|
self.hass.config_entries.async_update_entry(
|
||||||
|
entry=entry,
|
||||||
|
data=entry.data.copy().update(new_volume_step),
|
||||||
|
options=entry.options.copy().update(new_volume_step),
|
||||||
|
)
|
||||||
|
return self.async_abort(reason="updated_volume_step")
|
||||||
|
return self.async_abort(reason="already_setup")
|
||||||
|
|
||||||
|
# Store import values in case setup fails so user can see error
|
||||||
|
self.import_schema = update_schema_defaults(import_config)
|
||||||
|
|
||||||
|
return await self.async_step_user(user_input=import_config)
|
||||||
|
|
||||||
|
|
||||||
|
class VizioOptionsConfigFlow(config_entries.OptionsFlow):
|
||||||
|
"""Handle Transmission client options."""
|
||||||
|
|
||||||
|
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||||
|
"""Initialize vizio options flow."""
|
||||||
|
self.config_entry = config_entry
|
||||||
|
|
||||||
|
async def async_step_init(
|
||||||
|
self, user_input: Dict[str, Any] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Manage the vizio options."""
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
|
options = {
|
||||||
|
vol.Optional(
|
||||||
|
CONF_VOLUME_STEP,
|
||||||
|
default=self.config_entry.options.get(
|
||||||
|
CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP
|
||||||
|
),
|
||||||
|
): vol.All(vol.Coerce(int), vol.Range(min=1, max=10))
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.async_show_form(step_id="init", data_schema=vol.Schema(options))
|
|
@ -1,5 +1,4 @@
|
||||||
"""Constants used by vizio component."""
|
"""Constants used by vizio component."""
|
||||||
|
|
||||||
CONF_VOLUME_STEP = "volume_step"
|
CONF_VOLUME_STEP = "volume_step"
|
||||||
|
|
||||||
DEFAULT_NAME = "Vizio SmartCast"
|
DEFAULT_NAME = "Vizio SmartCast"
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
"domain": "vizio",
|
"domain": "vizio",
|
||||||
"name": "Vizio SmartCast TV",
|
"name": "Vizio SmartCast TV",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/vizio",
|
"documentation": "https://www.home-assistant.io/integrations/vizio",
|
||||||
"requirements": ["pyvizio==0.0.15"],
|
"requirements": ["pyvizio==0.0.20"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": ["@raman325"]
|
"codeowners": ["@raman325"],
|
||||||
|
"config_flow": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
"""Vizio SmartCast Device support."""
|
"""Vizio SmartCast Device support."""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Callable, Dict, List
|
from typing import Callable, List
|
||||||
|
|
||||||
from pyvizio import VizioAsync
|
from pyvizio import VizioAsync
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant import util
|
from homeassistant import util
|
||||||
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
|
from homeassistant.components.media_player import MediaPlayerDevice
|
||||||
from homeassistant.components.media_player.const import (
|
from homeassistant.components.media_player.const import (
|
||||||
SUPPORT_NEXT_TRACK,
|
SUPPORT_NEXT_TRACK,
|
||||||
SUPPORT_PREVIOUS_TRACK,
|
SUPPORT_PREVIOUS_TRACK,
|
||||||
|
@ -19,6 +17,7 @@ from homeassistant.components.media_player.const import (
|
||||||
SUPPORT_VOLUME_SET,
|
SUPPORT_VOLUME_SET,
|
||||||
SUPPORT_VOLUME_STEP,
|
SUPPORT_VOLUME_STEP,
|
||||||
)
|
)
|
||||||
|
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,
|
||||||
|
@ -27,18 +26,24 @@ from homeassistant.const import (
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
)
|
)
|
||||||
|
from homeassistant.exceptions import PlatformNotReady
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.dispatcher import (
|
||||||
|
async_dispatcher_connect,
|
||||||
|
async_dispatcher_send,
|
||||||
|
)
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
from . import VIZIO_SCHEMA, validate_auth
|
from .const import CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP, DEVICE_ID, DOMAIN, ICON
|
||||||
from .const import CONF_VOLUME_STEP, DEVICE_ID, ICON
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
COMMON_SUPPORTED_COMMANDS = (
|
COMMON_SUPPORTED_COMMANDS = (
|
||||||
SUPPORT_SELECT_SOURCE
|
SUPPORT_SELECT_SOURCE
|
||||||
| SUPPORT_TURN_ON
|
| SUPPORT_TURN_ON
|
||||||
|
@ -54,26 +59,35 @@ SUPPORTED_COMMANDS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = vol.All(PLATFORM_SCHEMA.extend(VIZIO_SCHEMA), validate_auth)
|
async def async_setup_entry(
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(
|
|
||||||
hass: HomeAssistantType,
|
hass: HomeAssistantType,
|
||||||
config: ConfigType,
|
config_entry: ConfigEntry,
|
||||||
async_add_entities: Callable[[List[Entity], bool], None],
|
async_add_entities: Callable[[List[Entity], bool], None],
|
||||||
discovery_info: Dict[str, Any] = None,
|
) -> bool:
|
||||||
):
|
"""Set up a Vizio media player entry."""
|
||||||
"""Set up the Vizio media player platform."""
|
host = config_entry.data[CONF_HOST]
|
||||||
|
token = config_entry.data.get(CONF_ACCESS_TOKEN)
|
||||||
|
name = config_entry.data[CONF_NAME]
|
||||||
|
device_type = config_entry.data[CONF_DEVICE_CLASS]
|
||||||
|
|
||||||
host = config[CONF_HOST]
|
# If config entry options not set up, set them up, otherwise assign values managed in options
|
||||||
token = config.get(CONF_ACCESS_TOKEN)
|
if CONF_VOLUME_STEP not in config_entry.options:
|
||||||
name = config[CONF_NAME]
|
volume_step = config_entry.data.get(CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP)
|
||||||
volume_step = config[CONF_VOLUME_STEP]
|
hass.config_entries.async_update_entry(
|
||||||
device_type = config[CONF_DEVICE_CLASS]
|
config_entry, options={CONF_VOLUME_STEP: volume_step}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
volume_step = config_entry.options[CONF_VOLUME_STEP]
|
||||||
|
|
||||||
device = VizioAsync(
|
device = VizioAsync(
|
||||||
DEVICE_ID, host, name, token, device_type, async_get_clientsession(hass, False)
|
DEVICE_ID,
|
||||||
|
host,
|
||||||
|
name,
|
||||||
|
token,
|
||||||
|
device_type,
|
||||||
|
session=async_get_clientsession(hass, False),
|
||||||
)
|
)
|
||||||
|
|
||||||
if not await device.can_connect():
|
if not await device.can_connect():
|
||||||
fail_auth_msg = ""
|
fail_auth_msg = ""
|
||||||
if token:
|
if token:
|
||||||
|
@ -83,18 +97,27 @@ async def async_setup_platform(
|
||||||
"is valid and available, device type is correct%s",
|
"is valid and available, device type is correct%s",
|
||||||
fail_auth_msg,
|
fail_auth_msg,
|
||||||
)
|
)
|
||||||
return
|
raise PlatformNotReady
|
||||||
|
|
||||||
async_add_entities([VizioDevice(device, name, volume_step, device_type)], True)
|
entity = VizioDevice(config_entry, device, name, volume_step, device_type)
|
||||||
|
|
||||||
|
async_add_entities([entity], True)
|
||||||
|
|
||||||
|
|
||||||
class VizioDevice(MediaPlayerDevice):
|
class VizioDevice(MediaPlayerDevice):
|
||||||
"""Media Player implementation which performs REST requests to device."""
|
"""Media Player implementation which performs REST requests to device."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, device: VizioAsync, name: str, volume_step: int, device_type: str
|
self,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
device: VizioAsync,
|
||||||
|
name: str,
|
||||||
|
volume_step: int,
|
||||||
|
device_type: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize Vizio device."""
|
"""Initialize Vizio device."""
|
||||||
|
self._config_entry = config_entry
|
||||||
|
self._async_unsub_listeners = []
|
||||||
|
|
||||||
self._name = name
|
self._name = name
|
||||||
self._state = None
|
self._state = None
|
||||||
|
@ -106,104 +129,140 @@ class VizioDevice(MediaPlayerDevice):
|
||||||
self._supported_commands = SUPPORTED_COMMANDS[device_type]
|
self._supported_commands = SUPPORTED_COMMANDS[device_type]
|
||||||
self._device = device
|
self._device = device
|
||||||
self._max_volume = float(self._device.get_max_volume())
|
self._max_volume = float(self._device.get_max_volume())
|
||||||
self._unique_id = None
|
|
||||||
self._icon = ICON[device_type]
|
self._icon = ICON[device_type]
|
||||||
|
self._available = True
|
||||||
|
|
||||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Retrieve latest state of the device."""
|
"""Retrieve latest state of the device."""
|
||||||
|
is_on = await self._device.get_power_state(False)
|
||||||
|
|
||||||
if not self._unique_id:
|
if is_on is None:
|
||||||
self._unique_id = await self._device.get_esn()
|
self._available = False
|
||||||
|
return
|
||||||
|
|
||||||
is_on = await self._device.get_power_state()
|
self._available = True
|
||||||
|
|
||||||
if is_on:
|
|
||||||
self._state = STATE_ON
|
|
||||||
|
|
||||||
volume = await self._device.get_current_volume()
|
|
||||||
if volume is not None:
|
|
||||||
self._volume_level = float(volume) / self._max_volume
|
|
||||||
|
|
||||||
input_ = await self._device.get_current_input()
|
|
||||||
if input_ is not None:
|
|
||||||
self._current_input = input_.meta_name
|
|
||||||
|
|
||||||
inputs = await self._device.get_inputs()
|
|
||||||
if inputs is not None:
|
|
||||||
self._available_inputs = [input_.name for input_ in inputs]
|
|
||||||
|
|
||||||
else:
|
|
||||||
if is_on is None:
|
|
||||||
self._state = None
|
|
||||||
else:
|
|
||||||
self._state = STATE_OFF
|
|
||||||
|
|
||||||
|
if not is_on:
|
||||||
|
self._state = STATE_OFF
|
||||||
self._volume_level = None
|
self._volume_level = None
|
||||||
self._current_input = None
|
self._current_input = None
|
||||||
self._available_inputs = None
|
self._available_inputs = None
|
||||||
|
return
|
||||||
|
|
||||||
|
self._state = STATE_ON
|
||||||
|
|
||||||
|
volume = await self._device.get_current_volume(False)
|
||||||
|
if volume is not None:
|
||||||
|
self._volume_level = float(volume) / self._max_volume
|
||||||
|
|
||||||
|
input_ = await self._device.get_current_input(False)
|
||||||
|
if input_ is not None:
|
||||||
|
self._current_input = input_.meta_name
|
||||||
|
|
||||||
|
inputs = await self._device.get_inputs(False)
|
||||||
|
if inputs is not None:
|
||||||
|
self._available_inputs = [input_.name for input_ in inputs]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _async_send_update_options_signal(
|
||||||
|
hass: HomeAssistantType, config_entry: ConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Send update event when when Vizio config entry is updated."""
|
||||||
|
# Move this method to component level if another entity ever gets added for a single config entry. See here: https://github.com/home-assistant/home-assistant/pull/30653#discussion_r366426121
|
||||||
|
async_dispatcher_send(hass, config_entry.entry_id, config_entry)
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Register callbacks when entity is added."""
|
||||||
|
# Register callback for when config entry is updated.
|
||||||
|
self._async_unsub_listeners.append(
|
||||||
|
self._config_entry.add_update_listener(
|
||||||
|
self._async_send_update_options_signal
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Register callback for update event
|
||||||
|
self._async_unsub_listeners.append(
|
||||||
|
async_dispatcher_connect(
|
||||||
|
self.hass, self._config_entry.entry_id, self._async_update_options
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self):
|
||||||
|
"""Disconnect callbacks when entity is removed."""
|
||||||
|
for listener in self._async_unsub_listeners:
|
||||||
|
listener()
|
||||||
|
|
||||||
|
self._async_unsub_listeners.clear()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return the availabiliity of the device."""
|
||||||
|
return self._available
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> str:
|
def state(self) -> str:
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
|
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""Return the name of the device."""
|
"""Return the name of the device."""
|
||||||
|
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self) -> str:
|
def icon(self) -> str:
|
||||||
"""Return the icon of the device."""
|
"""Return the icon of the device."""
|
||||||
|
|
||||||
return self._icon
|
return self._icon
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def volume_level(self) -> float:
|
def volume_level(self) -> float:
|
||||||
"""Return the volume level of the device."""
|
"""Return the volume level of the device."""
|
||||||
|
|
||||||
return self._volume_level
|
return self._volume_level
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source(self) -> str:
|
def source(self) -> str:
|
||||||
"""Return current input of the device."""
|
"""Return current input of the device."""
|
||||||
|
|
||||||
return self._current_input
|
return self._current_input
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source_list(self) -> List:
|
def source_list(self) -> List:
|
||||||
"""Return list of available inputs of the device."""
|
"""Return list of available inputs of the device."""
|
||||||
|
|
||||||
return self._available_inputs
|
return self._available_inputs
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self) -> int:
|
def supported_features(self) -> int:
|
||||||
"""Flag device features that are supported."""
|
"""Flag device features that are supported."""
|
||||||
|
|
||||||
return self._supported_commands
|
return self._supported_commands
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self) -> str:
|
def unique_id(self) -> str:
|
||||||
"""Return the unique id of the device."""
|
"""Return the unique id of the device."""
|
||||||
|
return self._config_entry.unique_id
|
||||||
|
|
||||||
return self._unique_id
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return device registry information."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self._config_entry.unique_id)},
|
||||||
|
"name": self.name,
|
||||||
|
"manufacturer": "VIZIO",
|
||||||
|
}
|
||||||
|
|
||||||
async def async_turn_on(self) -> None:
|
async def async_turn_on(self) -> None:
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
|
|
||||||
await self._device.pow_on()
|
await self._device.pow_on()
|
||||||
|
|
||||||
async def async_turn_off(self) -> None:
|
async def async_turn_off(self) -> None:
|
||||||
"""Turn the device off."""
|
"""Turn the device off."""
|
||||||
|
|
||||||
await self._device.pow_off()
|
await self._device.pow_off()
|
||||||
|
|
||||||
async def async_mute_volume(self, mute: bool) -> None:
|
async def async_mute_volume(self, mute: bool) -> None:
|
||||||
"""Mute the volume."""
|
"""Mute the volume."""
|
||||||
|
|
||||||
if mute:
|
if mute:
|
||||||
await self._device.mute_on()
|
await self._device.mute_on()
|
||||||
else:
|
else:
|
||||||
|
@ -211,22 +270,18 @@ class VizioDevice(MediaPlayerDevice):
|
||||||
|
|
||||||
async def async_media_previous_track(self) -> None:
|
async def async_media_previous_track(self) -> None:
|
||||||
"""Send previous channel command."""
|
"""Send previous channel command."""
|
||||||
|
|
||||||
await self._device.ch_down()
|
await self._device.ch_down()
|
||||||
|
|
||||||
async def async_media_next_track(self) -> None:
|
async def async_media_next_track(self) -> None:
|
||||||
"""Send next channel command."""
|
"""Send next channel command."""
|
||||||
|
|
||||||
await self._device.ch_up()
|
await self._device.ch_up()
|
||||||
|
|
||||||
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.input_switch(source)
|
await self._device.input_switch(source)
|
||||||
|
|
||||||
async def async_volume_up(self) -> None:
|
async def async_volume_up(self) -> None:
|
||||||
"""Increasing volume of the device."""
|
"""Increasing volume of the device."""
|
||||||
|
|
||||||
await self._device.vol_up(self._volume_step)
|
await self._device.vol_up(self._volume_step)
|
||||||
|
|
||||||
if self._volume_level is not None:
|
if self._volume_level is not None:
|
||||||
|
@ -236,7 +291,6 @@ class VizioDevice(MediaPlayerDevice):
|
||||||
|
|
||||||
async def async_volume_down(self) -> None:
|
async def async_volume_down(self) -> None:
|
||||||
"""Decreasing volume of the device."""
|
"""Decreasing volume of the device."""
|
||||||
|
|
||||||
await self._device.vol_down(self._volume_step)
|
await self._device.vol_down(self._volume_step)
|
||||||
|
|
||||||
if self._volume_level is not None:
|
if self._volume_level is not None:
|
||||||
|
@ -246,7 +300,6 @@ class VizioDevice(MediaPlayerDevice):
|
||||||
|
|
||||||
async def async_set_volume_level(self, volume: float) -> None:
|
async def async_set_volume_level(self, volume: float) -> None:
|
||||||
"""Set volume level."""
|
"""Set volume level."""
|
||||||
|
|
||||||
if self._volume_level is not None:
|
if self._volume_level is not None:
|
||||||
if volume > self._volume_level:
|
if volume > self._volume_level:
|
||||||
num = int(self._max_volume * (volume - self._volume_level))
|
num = int(self._max_volume * (volume - self._volume_level))
|
||||||
|
|
40
homeassistant/components/vizio/strings.json
Normal file
40
homeassistant/components/vizio/strings.json
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"title": "Vizio SmartCast",
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Setup Vizio SmartCast Client",
|
||||||
|
"data": {
|
||||||
|
"name": "Name",
|
||||||
|
"host": "<Host/IP>:<Port>",
|
||||||
|
"device_class": "Device Type",
|
||||||
|
"access_token": "Access Token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"host_exists": "Host already configured.",
|
||||||
|
"name_exists": "Name already configured.",
|
||||||
|
"cant_connect": "Could not connect to the device. [Review the docs](https://www.home-assistant.io/integrations/vizio/) and re-verify that:\n- The device is powered on\n- The device is connected to the network\n- The values you filled in are accurate\nbefore attempting to resubmit.",
|
||||||
|
"tv_needs_token": "When Device Type is `tv` then a valid Access Token is needed."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_in_progress": "Config flow for vizio component already in progress.",
|
||||||
|
"already_setup": "This entry has already been setup.",
|
||||||
|
"host_exists": "Vizio component with host already configured.",
|
||||||
|
"name_exists": "Vizio component with name already configured.",
|
||||||
|
"updated_volume_step": "This entry has already been setup but the volume step size in the config does not match the config entry so the config entry has been updated accordingly."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"title": "Update Vizo SmartCast Options",
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Update Vizo SmartCast Options",
|
||||||
|
"data": {
|
||||||
|
"volume_step": "Volume Step Size"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -92,6 +92,7 @@ FLOWS = [
|
||||||
"upnp",
|
"upnp",
|
||||||
"velbus",
|
"velbus",
|
||||||
"vesync",
|
"vesync",
|
||||||
|
"vizio",
|
||||||
"wemo",
|
"wemo",
|
||||||
"withings",
|
"withings",
|
||||||
"wled",
|
"wled",
|
||||||
|
|
|
@ -1693,7 +1693,7 @@ pyversasense==0.0.6
|
||||||
pyvesync==1.1.0
|
pyvesync==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.vizio
|
# homeassistant.components.vizio
|
||||||
pyvizio==0.0.15
|
pyvizio==0.0.20
|
||||||
|
|
||||||
# homeassistant.components.velux
|
# homeassistant.components.velux
|
||||||
pyvlx==0.2.12
|
pyvlx==0.2.12
|
||||||
|
|
|
@ -557,6 +557,9 @@ pyvera==0.3.7
|
||||||
# homeassistant.components.vesync
|
# homeassistant.components.vesync
|
||||||
pyvesync==1.1.0
|
pyvesync==1.1.0
|
||||||
|
|
||||||
|
# homeassistant.components.vizio
|
||||||
|
pyvizio==0.0.20
|
||||||
|
|
||||||
# homeassistant.components.html5
|
# homeassistant.components.html5
|
||||||
pywebpush==1.9.2
|
pywebpush==1.9.2
|
||||||
|
|
||||||
|
|
1
tests/components/vizio/__init__.py
Normal file
1
tests/components/vizio/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for the Vizio integration."""
|
289
tests/components/vizio/test_config_flow.py
Normal file
289
tests/components/vizio/test_config_flow.py
Normal file
|
@ -0,0 +1,289 @@
|
||||||
|
"""Tests for Vizio config flow."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from asynctest import patch
|
||||||
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import data_entry_flow
|
||||||
|
from homeassistant.components.vizio import VIZIO_SCHEMA
|
||||||
|
from homeassistant.components.vizio.const import (
|
||||||
|
CONF_VOLUME_STEP,
|
||||||
|
DEFAULT_NAME,
|
||||||
|
DEFAULT_VOLUME_STEP,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_ACCESS_TOKEN,
|
||||||
|
CONF_DEVICE_CLASS,
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_NAME,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
NAME = "Vizio"
|
||||||
|
HOST = "192.168.1.1:9000"
|
||||||
|
DEVICE_CLASS_TV = "tv"
|
||||||
|
DEVICE_CLASS_SOUNDBAR = "soundbar"
|
||||||
|
ACCESS_TOKEN = "deadbeef"
|
||||||
|
VOLUME_STEP = 2
|
||||||
|
UNIQUE_ID = "testid"
|
||||||
|
|
||||||
|
MOCK_USER_VALID_TV_ENTRY = {
|
||||||
|
CONF_NAME: NAME,
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_DEVICE_CLASS: DEVICE_CLASS_TV,
|
||||||
|
CONF_ACCESS_TOKEN: ACCESS_TOKEN,
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_IMPORT_VALID_TV_ENTRY = {
|
||||||
|
CONF_NAME: NAME,
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_DEVICE_CLASS: DEVICE_CLASS_TV,
|
||||||
|
CONF_ACCESS_TOKEN: ACCESS_TOKEN,
|
||||||
|
CONF_VOLUME_STEP: VOLUME_STEP,
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_INVALID_TV_ENTRY = {
|
||||||
|
CONF_NAME: NAME,
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_DEVICE_CLASS: DEVICE_CLASS_TV,
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_SOUNDBAR_ENTRY = {
|
||||||
|
CONF_NAME: NAME,
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_DEVICE_CLASS: DEVICE_CLASS_SOUNDBAR,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="vizio_connect")
|
||||||
|
def vizio_connect_fixture():
|
||||||
|
"""Mock valid vizio device and entry setup."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config",
|
||||||
|
return_value=True,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.vizio.config_flow.VizioAsync.get_unique_id",
|
||||||
|
return_value=UNIQUE_ID,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.vizio.async_setup_entry", return_value=True
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="vizio_cant_connect")
|
||||||
|
def vizio_cant_connect_fixture():
|
||||||
|
"""Mock vizio device cant connect."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config",
|
||||||
|
return_value=False,
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_flow_minimum_fields(hass: HomeAssistantType, vizio_connect) -> None:
|
||||||
|
"""Test user config flow with minimum fields."""
|
||||||
|
# test form shows
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={
|
||||||
|
CONF_NAME: NAME,
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_DEVICE_CLASS: DEVICE_CLASS_SOUNDBAR,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
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_SOUNDBAR
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_flow_all_fields(hass: HomeAssistantType, vizio_connect) -> None:
|
||||||
|
"""Test user config flow with all fields."""
|
||||||
|
# test form shows
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={
|
||||||
|
CONF_NAME: NAME,
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_DEVICE_CLASS: DEVICE_CLASS_TV,
|
||||||
|
CONF_ACCESS_TOKEN: ACCESS_TOKEN,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_host_already_configured(
|
||||||
|
hass: HomeAssistantType, vizio_connect
|
||||||
|
) -> None:
|
||||||
|
"""Test host is already configured during user setup."""
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=MOCK_SOUNDBAR_ENTRY,
|
||||||
|
options={CONF_VOLUME_STEP: VOLUME_STEP},
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
fail_entry = MOCK_SOUNDBAR_ENTRY.copy()
|
||||||
|
fail_entry[CONF_NAME] = "newtestname"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=fail_entry,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {CONF_HOST: "host_exists"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_name_already_configured(
|
||||||
|
hass: HomeAssistantType, vizio_connect
|
||||||
|
) -> None:
|
||||||
|
"""Test name is already configured during user setup."""
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=MOCK_SOUNDBAR_ENTRY,
|
||||||
|
options={CONF_VOLUME_STEP: VOLUME_STEP},
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
fail_entry = MOCK_SOUNDBAR_ENTRY.copy()
|
||||||
|
fail_entry[CONF_HOST] = "0.0.0.0"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], fail_entry
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {CONF_NAME: "name_exists"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_error_on_could_not_connect(
|
||||||
|
hass: HomeAssistantType, vizio_cant_connect
|
||||||
|
) -> None:
|
||||||
|
"""Test with could_not_connect during user_setup."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], MOCK_USER_VALID_TV_ENTRY
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {"base": "cant_connect"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_error_on_tv_needs_token(
|
||||||
|
hass: HomeAssistantType, vizio_connect
|
||||||
|
) -> None:
|
||||||
|
"""Test when config fails custom validation for non null access token when device_class = tv during user setup."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], MOCK_INVALID_TV_ENTRY
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {"base": "tv_needs_token"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import_flow_minimum_fields(
|
||||||
|
hass: HomeAssistantType, vizio_connect
|
||||||
|
) -> None:
|
||||||
|
"""Test import config flow with minimum fields."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": "import"},
|
||||||
|
data=vol.Schema(VIZIO_SCHEMA)(
|
||||||
|
{CONF_HOST: HOST, CONF_DEVICE_CLASS: DEVICE_CLASS_SOUNDBAR}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["title"] == DEFAULT_NAME
|
||||||
|
assert result["data"][CONF_NAME] == DEFAULT_NAME
|
||||||
|
assert result["data"][CONF_HOST] == HOST
|
||||||
|
assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_SOUNDBAR
|
||||||
|
assert result["data"][CONF_VOLUME_STEP] == DEFAULT_VOLUME_STEP
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import_flow_all_fields(hass: HomeAssistantType, vizio_connect) -> None:
|
||||||
|
"""Test import config flow with all fields."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": "import"},
|
||||||
|
data=vol.Schema(VIZIO_SCHEMA)(MOCK_IMPORT_VALID_TV_ENTRY),
|
||||||
|
)
|
||||||
|
|
||||||
|
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_VOLUME_STEP] == VOLUME_STEP
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import_entity_already_configured(
|
||||||
|
hass: HomeAssistantType, vizio_connect
|
||||||
|
) -> None:
|
||||||
|
"""Test entity is already configured during import setup."""
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=vol.Schema(VIZIO_SCHEMA)(MOCK_SOUNDBAR_ENTRY),
|
||||||
|
options={CONF_VOLUME_STEP: VOLUME_STEP},
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
fail_entry = vol.Schema(VIZIO_SCHEMA)(MOCK_SOUNDBAR_ENTRY.copy())
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "import"}, data=fail_entry
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "already_setup"
|
Loading…
Add table
Add a link
Reference in a new issue