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_configuredpull/30783/head^2
parent
5fa7d6f22a
commit
ac771addc1
|
@ -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
|
||||
|
|
|
@ -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."""
|
||||
|
||||
CONF_VOLUME_STEP = "volume_step"
|
||||
|
||||
DEFAULT_NAME = "Vizio SmartCast"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
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 = False
|
||||
return
|
||||
|
||||
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))
|
||||
|
|
|
@ -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",
|
||||
"velbus",
|
||||
"vesync",
|
||||
"vizio",
|
||||
"wemo",
|
||||
"withings",
|
||||
"wled",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Tests for the Vizio integration."""
|
|
@ -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…
Reference in New Issue