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
pull/30783/head^2
Raman Gupta 2020-01-15 05:43:55 -05:00 committed by Martin Hjelmare
parent 5fa7d6f22a
commit ac771addc1
11 changed files with 669 additions and 72 deletions

View File

@ -1,7 +1,7 @@
"""The vizio component."""
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_ACCESS_TOKEN,
CONF_DEVICE_CLASS,
@ -9,13 +9,14 @@ from homeassistant.const import (
CONF_NAME,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from .const import (
CONF_VOLUME_STEP,
DEFAULT_DEVICE_CLASS,
DEFAULT_NAME,
DEFAULT_VOLUME_STEP,
DOMAIN,
)
@ -42,3 +43,41 @@ VIZIO_SCHEMA = {
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

View 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))

View File

@ -1,5 +1,4 @@
"""Constants used by vizio component."""
CONF_VOLUME_STEP = "volume_step"
DEFAULT_NAME = "Vizio SmartCast"

View File

@ -2,7 +2,8 @@
"domain": "vizio",
"name": "Vizio SmartCast TV",
"documentation": "https://www.home-assistant.io/integrations/vizio",
"requirements": ["pyvizio==0.0.15"],
"requirements": ["pyvizio==0.0.20"],
"dependencies": [],
"codeowners": ["@raman325"]
"codeowners": ["@raman325"],
"config_flow": true
}

View File

@ -1,14 +1,12 @@
"""Vizio SmartCast Device support."""
from datetime import timedelta
import logging
from typing import Any, Callable, Dict, List
from typing import Callable, List
from pyvizio import VizioAsync
import voluptuous as vol
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 (
SUPPORT_NEXT_TRACK,
SUPPORT_PREVIOUS_TRACK,
@ -19,6 +17,7 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_ACCESS_TOKEN,
CONF_DEVICE_CLASS,
@ -27,18 +26,24 @@ from homeassistant.const import (
STATE_OFF,
STATE_ON,
)
from homeassistant.exceptions import PlatformNotReady
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.typing import ConfigType, HomeAssistantType
from homeassistant.helpers.typing import HomeAssistantType
from . import VIZIO_SCHEMA, validate_auth
from .const import CONF_VOLUME_STEP, DEVICE_ID, ICON
from .const import CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP, DEVICE_ID, DOMAIN, ICON
_LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
PARALLEL_UPDATES = 0
COMMON_SUPPORTED_COMMANDS = (
SUPPORT_SELECT_SOURCE
| SUPPORT_TURN_ON
@ -54,26 +59,35 @@ SUPPORTED_COMMANDS = {
}
PLATFORM_SCHEMA = vol.All(PLATFORM_SCHEMA.extend(VIZIO_SCHEMA), validate_auth)
async def async_setup_platform(
async def async_setup_entry(
hass: HomeAssistantType,
config: ConfigType,
config_entry: ConfigEntry,
async_add_entities: Callable[[List[Entity], bool], None],
discovery_info: Dict[str, Any] = None,
):
"""Set up the Vizio media player platform."""
) -> bool:
"""Set up a Vizio media player entry."""
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]
token = config.get(CONF_ACCESS_TOKEN)
name = config[CONF_NAME]
volume_step = config[CONF_VOLUME_STEP]
device_type = config[CONF_DEVICE_CLASS]
# If config entry options not set up, set them up, otherwise assign values managed in options
if CONF_VOLUME_STEP not in config_entry.options:
volume_step = config_entry.data.get(CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP)
hass.config_entries.async_update_entry(
config_entry, options={CONF_VOLUME_STEP: volume_step}
)
else:
volume_step = config_entry.options[CONF_VOLUME_STEP]
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():
fail_auth_msg = ""
if token:
@ -83,18 +97,27 @@ async def async_setup_platform(
"is valid and available, device type is correct%s",
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):
"""Media Player implementation which performs REST requests to device."""
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:
"""Initialize Vizio device."""
self._config_entry = config_entry
self._async_unsub_listeners = []
self._name = name
self._state = None
@ -106,104 +129,140 @@ class VizioDevice(MediaPlayerDevice):
self._supported_commands = SUPPORTED_COMMANDS[device_type]
self._device = device
self._max_volume = float(self._device.get_max_volume())
self._unique_id = None
self._icon = ICON[device_type]
self._available = True
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
async def async_update(self) -> None:
"""Retrieve latest state of the device."""
is_on = await self._device.get_power_state(False)
if not self._unique_id:
self._unique_id = await self._device.get_esn()
if is_on is None:
self._available = False
return
is_on = await self._device.get_power_state()
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
self._available = True
if not is_on:
self._state = STATE_OFF
self._volume_level = None
self._current_input = 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
def state(self) -> str:
"""Return the state of the device."""
return self._state
@property
def name(self) -> str:
"""Return the name of the device."""
return self._name
@property
def icon(self) -> str:
"""Return the icon of the device."""
return self._icon
@property
def volume_level(self) -> float:
"""Return the volume level of the device."""
return self._volume_level
@property
def source(self) -> str:
"""Return current input of the device."""
return self._current_input
@property
def source_list(self) -> List:
"""Return list of available inputs of the device."""
return self._available_inputs
@property
def supported_features(self) -> int:
"""Flag device features that are supported."""
return self._supported_commands
@property
def unique_id(self) -> str:
"""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:
"""Turn the device on."""
await self._device.pow_on()
async def async_turn_off(self) -> None:
"""Turn the device off."""
await self._device.pow_off()
async def async_mute_volume(self, mute: bool) -> None:
"""Mute the volume."""
if mute:
await self._device.mute_on()
else:
@ -211,22 +270,18 @@ class VizioDevice(MediaPlayerDevice):
async def async_media_previous_track(self) -> None:
"""Send previous channel command."""
await self._device.ch_down()
async def async_media_next_track(self) -> None:
"""Send next channel command."""
await self._device.ch_up()
async def async_select_source(self, source: str) -> None:
"""Select input source."""
await self._device.input_switch(source)
async def async_volume_up(self) -> None:
"""Increasing volume of the device."""
await self._device.vol_up(self._volume_step)
if self._volume_level is not None:
@ -236,7 +291,6 @@ class VizioDevice(MediaPlayerDevice):
async def async_volume_down(self) -> None:
"""Decreasing volume of the device."""
await self._device.vol_down(self._volume_step)
if self._volume_level is not None:
@ -246,7 +300,6 @@ class VizioDevice(MediaPlayerDevice):
async def async_set_volume_level(self, volume: float) -> None:
"""Set volume level."""
if self._volume_level is not None:
if volume > self._volume_level:
num = int(self._max_volume * (volume - self._volume_level))

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

View File

@ -92,6 +92,7 @@ FLOWS = [
"upnp",
"velbus",
"vesync",
"vizio",
"wemo",
"withings",
"wled",

View File

@ -1693,7 +1693,7 @@ pyversasense==0.0.6
pyvesync==1.1.0
# homeassistant.components.vizio
pyvizio==0.0.15
pyvizio==0.0.20
# homeassistant.components.velux
pyvlx==0.2.12

View File

@ -557,6 +557,9 @@ pyvera==0.3.7
# homeassistant.components.vesync
pyvesync==1.1.0
# homeassistant.components.vizio
pyvizio==0.0.20
# homeassistant.components.html5
pywebpush==1.9.2

View File

@ -0,0 +1 @@
"""Tests for the Vizio integration."""

View 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"