mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 08:47:10 +00:00
Support vizio pairing through config flow (#31520)
* support pairing through config flow * simplify import failure log messages * remove unnecessary list comprehension * bump pyvizio to add passing ClientSession in where it was missed * show different message if user completes pairing through import * remove dupe failure message since reasons for failure are the same in both instances * remove extra constant * add host reachability check during pairing workflow * revert redundant connection check since check is implicitly done during pairing process * fix rebase errors * fix string * updates based on review * update docstring * missed commit * update import confirmation message to be less wordy * use ConfigFlow _abort_if_unique_id_configured * fix test
This commit is contained in:
parent
03d8abe1ba
commit
b9fa32444a
@ -3,34 +3,14 @@ import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import DEVICE_CLASS_TV
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DEVICE_CLASS
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||
|
||||
from .const import DOMAIN, VIZIO_SCHEMA
|
||||
|
||||
|
||||
def validate_auth(config: ConfigType) -> ConfigType:
|
||||
"""Validate presence of CONF_ACCESS_TOKEN when CONF_DEVICE_CLASS == DEVICE_CLASS_TV."""
|
||||
token = config.get(CONF_ACCESS_TOKEN)
|
||||
if config[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV and not token:
|
||||
raise vol.Invalid(
|
||||
f"When '{CONF_DEVICE_CLASS}' is '{DEVICE_CLASS_TV}' then "
|
||||
f"'{CONF_ACCESS_TOKEN}' is required.",
|
||||
path=[CONF_ACCESS_TOKEN],
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.All(
|
||||
cv.ensure_list, [vol.All(vol.Schema(VIZIO_SCHEMA), validate_auth)]
|
||||
)
|
||||
},
|
||||
{DOMAIN: vol.All(cv.ensure_list, [vol.Schema(VIZIO_SCHEMA)])},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Config flow for Vizio."""
|
||||
import copy
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
@ -7,24 +8,25 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.media_player import DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_ZEROCONF, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_ACCESS_TOKEN,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PIN,
|
||||
CONF_PORT,
|
||||
CONF_TYPE,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from . import validate_auth
|
||||
from .const import (
|
||||
CONF_VOLUME_STEP,
|
||||
DEFAULT_DEVICE_CLASS,
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_VOLUME_STEP,
|
||||
DEVICE_ID,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
@ -42,7 +44,7 @@ def _get_config_schema(input_dict: Dict[str, Any] = None) -> vol.Schema:
|
||||
CONF_NAME, default=input_dict.get(CONF_NAME, DEFAULT_NAME)
|
||||
): str,
|
||||
vol.Required(CONF_HOST, default=input_dict.get(CONF_HOST)): str,
|
||||
vol.Optional(
|
||||
vol.Required(
|
||||
CONF_DEVICE_CLASS,
|
||||
default=input_dict.get(CONF_DEVICE_CLASS, DEFAULT_DEVICE_CLASS),
|
||||
): vol.All(str, vol.Lower, vol.In([DEVICE_CLASS_TV, DEVICE_CLASS_SPEAKER])),
|
||||
@ -54,6 +56,17 @@ def _get_config_schema(input_dict: Dict[str, Any] = None) -> vol.Schema:
|
||||
)
|
||||
|
||||
|
||||
def _get_pairing_schema(input_dict: Dict[str, Any] = None) -> vol.Schema:
|
||||
"""Return schema defaults for pairing data based on user input. Retain info already provided for future form views by setting them as defaults in schema."""
|
||||
if input_dict is None:
|
||||
input_dict = {}
|
||||
|
||||
return vol.Schema(
|
||||
{vol.Required(CONF_PIN, default=input_dict.get(CONF_PIN, "")): str},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
def _host_is_same(host1: str, host2: str) -> bool:
|
||||
"""Check if host1 and host2 are the same."""
|
||||
return host1.split(":")[0] == host2.split(":")[0]
|
||||
@ -101,6 +114,27 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Initialize config flow."""
|
||||
self._user_schema = None
|
||||
self._must_show_form = None
|
||||
self._ch_type = None
|
||||
self._pairing_token = None
|
||||
self._data = None
|
||||
|
||||
async def _create_entry_if_unique(
|
||||
self, input_dict: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Check if unique_id doesn't already exist. If it does, abort. If it doesn't, create entry."""
|
||||
unique_id = await VizioAsync.get_unique_id(
|
||||
input_dict[CONF_HOST],
|
||||
input_dict.get(CONF_ACCESS_TOKEN),
|
||||
input_dict[CONF_DEVICE_CLASS],
|
||||
session=async_get_clientsession(self.hass, False),
|
||||
)
|
||||
|
||||
# Set unique ID and abort if unique ID is already configured on an entry or a flow
|
||||
# with the unique ID is already in progress
|
||||
await self.async_set_unique_id(unique_id=unique_id, raise_on_progress=True)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(title=input_dict[CONF_NAME], data=input_dict)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: Dict[str, Any] = None
|
||||
@ -116,17 +150,18 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
for entry in self.hass.config_entries.async_entries(DOMAIN):
|
||||
if _host_is_same(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)
|
||||
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
if self._must_show_form and self.context["source"] == SOURCE_ZEROCONF:
|
||||
# Discovery should always display the config form before trying to
|
||||
# create entry so that user can update default config options
|
||||
self._must_show_form = False
|
||||
elif user_input[
|
||||
CONF_DEVICE_CLASS
|
||||
] == DEVICE_CLASS_SPEAKER or user_input.get(CONF_ACCESS_TOKEN):
|
||||
# Ensure config is valid for a device
|
||||
if not await VizioAsync.validate_ha_config(
|
||||
user_input[CONF_HOST],
|
||||
@ -135,38 +170,38 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
session=async_get_clientsession(self.hass, False),
|
||||
):
|
||||
errors["base"] = "cant_connect"
|
||||
except vol.Invalid:
|
||||
errors["base"] = "tv_needs_token"
|
||||
|
||||
if not errors:
|
||||
# Skip validating config and creating entry if form must be shown
|
||||
if self._must_show_form:
|
||||
if not errors:
|
||||
return await self._create_entry_if_unique(user_input)
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
elif self._must_show_form and self.context["source"] == SOURCE_IMPORT:
|
||||
# Import should always display the config form if CONF_ACCESS_TOKEN
|
||||
# wasn't included but is needed so that the user can choose to update
|
||||
# their configuration.yaml or to proceed with config flow pairing. We
|
||||
# will also provide contextual message to user explaining why
|
||||
_LOGGER.warning(
|
||||
"Couldn't complete configuration.yaml import: '%s' key is missing. To "
|
||||
"complete setup, '%s' can be obtained by going through pairing process "
|
||||
"via frontend Integrations menu; to avoid re-pairing your device in the "
|
||||
"future, once you have finished pairing, it is recommended to add "
|
||||
"obtained value to your config ",
|
||||
CONF_ACCESS_TOKEN,
|
||||
CONF_ACCESS_TOKEN,
|
||||
)
|
||||
self._must_show_form = False
|
||||
else:
|
||||
# Abort flow if existing entry with same unique ID matches new config entry.
|
||||
# Since name and host check have already passed, if an entry already exists,
|
||||
# It is likely a reconfigured device.
|
||||
unique_id = await VizioAsync.get_unique_id(
|
||||
user_input[CONF_HOST],
|
||||
user_input.get(CONF_ACCESS_TOKEN),
|
||||
user_input[CONF_DEVICE_CLASS],
|
||||
session=async_get_clientsession(self.hass, False),
|
||||
)
|
||||
self._data = copy.deepcopy(user_input)
|
||||
return await self.async_step_pair_tv()
|
||||
|
||||
if await self.async_set_unique_id(
|
||||
unique_id=unique_id, raise_on_progress=True
|
||||
):
|
||||
return self.async_abort(
|
||||
reason="already_setup_with_diff_host_and_name"
|
||||
)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_NAME], data=user_input
|
||||
)
|
||||
|
||||
# Use user_input params as default values for schema if user_input is non-empty, otherwise use default schema
|
||||
schema = self._user_schema or _get_config_schema()
|
||||
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
if errors and self.context["source"] == SOURCE_IMPORT:
|
||||
# Log an error message if import config flow fails since otherwise failure is silent
|
||||
_LOGGER.error(
|
||||
"configuration.yaml import failure: %s", ", ".join(errors.values())
|
||||
)
|
||||
|
||||
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]:
|
||||
@ -201,6 +236,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return self.async_abort(reason="already_setup")
|
||||
|
||||
self._must_show_form = True
|
||||
return await self.async_step_user(user_input=import_config)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
@ -231,7 +267,95 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
discovery_info[CONF_HOST]
|
||||
)
|
||||
|
||||
# Form must be shown after discovery so user can confirm/update configuration before ConfigEntry creation.
|
||||
# Form must be shown after discovery so user can confirm/update configuration
|
||||
# before ConfigEntry creation.
|
||||
self._must_show_form = True
|
||||
|
||||
return await self.async_step_user(user_input=discovery_info)
|
||||
|
||||
async def async_step_pair_tv(
|
||||
self, user_input: Dict[str, Any] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Start pairing process and ask user for PIN to complete pairing process."""
|
||||
errors = {}
|
||||
|
||||
# Start pairing process if it hasn't already started
|
||||
if not self._ch_type and not self._pairing_token:
|
||||
dev = VizioAsync(
|
||||
DEVICE_ID,
|
||||
self._data[CONF_HOST],
|
||||
self._data[CONF_NAME],
|
||||
None,
|
||||
self._data[CONF_DEVICE_CLASS],
|
||||
session=async_get_clientsession(self.hass, False),
|
||||
)
|
||||
pair_data = await dev.start_pair()
|
||||
|
||||
if pair_data:
|
||||
self._ch_type = pair_data.ch_type
|
||||
self._pairing_token = pair_data.token
|
||||
return await self.async_step_pair_tv()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=_get_config_schema(self._data),
|
||||
errors={"base": "cant_connect"},
|
||||
)
|
||||
|
||||
# Complete pairing process if PIN has been provided
|
||||
if user_input and user_input.get(CONF_PIN):
|
||||
dev = VizioAsync(
|
||||
DEVICE_ID,
|
||||
self._data[CONF_HOST],
|
||||
self._data[CONF_NAME],
|
||||
None,
|
||||
self._data[CONF_DEVICE_CLASS],
|
||||
session=async_get_clientsession(self.hass, False),
|
||||
)
|
||||
pair_data = await dev.pair(
|
||||
self._ch_type, self._pairing_token, user_input[CONF_PIN]
|
||||
)
|
||||
|
||||
if pair_data:
|
||||
self._data[CONF_ACCESS_TOKEN] = pair_data.auth_token
|
||||
self._must_show_form = True
|
||||
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
if self.context["source"] == SOURCE_IMPORT:
|
||||
# If user is pairing via config import, show different message
|
||||
return await self.async_step_pairing_complete_import()
|
||||
|
||||
return await self.async_step_pairing_complete()
|
||||
|
||||
# If no data was retrieved, it's assumed that the pairing attempt was not
|
||||
# successful
|
||||
errors[CONF_PIN] = "complete_pairing_failed"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="pair_tv",
|
||||
data_schema=_get_pairing_schema(user_input),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def _pairing_complete(self, step_id: str) -> Dict[str, Any]:
|
||||
"""Handle config flow completion."""
|
||||
if not self._must_show_form:
|
||||
return await self._create_entry_if_unique(self._data)
|
||||
|
||||
self._must_show_form = False
|
||||
return self.async_show_form(
|
||||
step_id=step_id,
|
||||
data_schema=vol.Schema({}),
|
||||
description_placeholders={"access_token": self._data[CONF_ACCESS_TOKEN]},
|
||||
)
|
||||
|
||||
async def async_step_pairing_complete(
|
||||
self, user_input: Dict[str, Any] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Complete non-import config flow by displaying final message to confirm pairing."""
|
||||
return await self._pairing_complete("pairing_complete")
|
||||
|
||||
async def async_step_pairing_complete_import(
|
||||
self, user_input: Dict[str, Any] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Complete import config flow by displaying final message to show user access token and give further instructions."""
|
||||
return await self._pairing_complete("pairing_complete_import")
|
||||
|
@ -4,23 +4,38 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Setup Vizio SmartCast Device",
|
||||
"description": "All fields are required except Access Token. If you choose not to provide an Access Token, and your Device Type is 'tv', you will go through a pairing process with your device so an Access Token can be retrieved.\n\nTo go through the pairing process, before clicking Submit, ensure your TV is powered on and connected to the network. You also need to be able to see the screen.",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"host": "<Host/IP>:<Port>",
|
||||
"device_class": "Device Type",
|
||||
"access_token": "Access Token"
|
||||
}
|
||||
},
|
||||
"pair_tv": {
|
||||
"title": "Complete Pairing Process",
|
||||
"description": "Your TV should be displaying a code. Enter that code into the form and then continue to the next step to complete the pairing.",
|
||||
"data": {
|
||||
"pin": "PIN"
|
||||
}
|
||||
},
|
||||
"pairing_complete": {
|
||||
"title": "Pairing Complete",
|
||||
"description": "Your Vizio SmartCast device is now connected to Home Assistant."
|
||||
},
|
||||
"pairing_complete_import": {
|
||||
"title": "Pairing Complete",
|
||||
"description": "Your Vizio SmartCast device is now connected to Home Assistant.\n\nYour Access Token is '**{access_token}**'."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"host_exists": "Vizio device with specified host already configured.",
|
||||
"name_exists": "Vizio device with specified 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."
|
||||
"complete_pairing failed": "Unable to complete pairing. Ensure the PIN you provided is correct and the TV is still powered and connected to the network before resubmitting.",
|
||||
"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."
|
||||
},
|
||||
"abort": {
|
||||
"already_setup": "This entry has already been setup.",
|
||||
"already_setup_with_diff_host_and_name": "This entry appears to have already been setup with a different host and name based on its serial number. Please remove any old entries from your configuration.yaml and from the Integrations menu before reattempting to add this device.",
|
||||
"updated_entry": "This entry has already been setup but the name and/or options defined in the configuration do not match the previously imported configuration, so the configuration entry has been updated accordingly."
|
||||
}
|
||||
},
|
||||
|
@ -3,7 +3,18 @@ from asynctest import patch
|
||||
import pytest
|
||||
from pyvizio.const import DEVICE_CLASS_SPEAKER, MAX_VOLUME
|
||||
|
||||
from .const import CURRENT_INPUT, INPUT_LIST, MODEL, UNIQUE_ID, VERSION
|
||||
from .const import (
|
||||
ACCESS_TOKEN,
|
||||
CH_TYPE,
|
||||
CURRENT_INPUT,
|
||||
INPUT_LIST,
|
||||
MODEL,
|
||||
RESPONSE_TOKEN,
|
||||
UNIQUE_ID,
|
||||
VERSION,
|
||||
MockCompletePairingResponse,
|
||||
MockStartPairingResponse,
|
||||
)
|
||||
|
||||
|
||||
class MockInput:
|
||||
@ -42,6 +53,41 @@ def vizio_connect_fixture():
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="vizio_complete_pairing")
|
||||
def vizio_complete_pairing_fixture():
|
||||
"""Mock complete vizio pairing workflow."""
|
||||
with patch(
|
||||
"homeassistant.components.vizio.config_flow.VizioAsync.start_pair",
|
||||
return_value=MockStartPairingResponse(CH_TYPE, RESPONSE_TOKEN),
|
||||
), patch(
|
||||
"homeassistant.components.vizio.config_flow.VizioAsync.pair",
|
||||
return_value=MockCompletePairingResponse(ACCESS_TOKEN),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="vizio_start_pairing_failure")
|
||||
def vizio_start_pairing_failure_fixture():
|
||||
"""Mock vizio start pairing failure."""
|
||||
with patch(
|
||||
"homeassistant.components.vizio.config_flow.VizioAsync.start_pair",
|
||||
return_value=None,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="vizio_invalid_pin_failure")
|
||||
def vizio_invalid_pin_failure_fixture():
|
||||
"""Mock vizio failure due to invalid pin."""
|
||||
with patch(
|
||||
"homeassistant.components.vizio.config_flow.VizioAsync.start_pair",
|
||||
return_value=MockStartPairingResponse(CH_TYPE, RESPONSE_TOKEN),
|
||||
), patch(
|
||||
"homeassistant.components.vizio.config_flow.VizioAsync.pair", return_value=None,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="vizio_bypass_setup")
|
||||
def vizio_bypass_setup_fixture():
|
||||
"""Mock component setup."""
|
||||
|
@ -12,6 +12,7 @@ from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PIN,
|
||||
CONF_PORT,
|
||||
CONF_TYPE,
|
||||
)
|
||||
@ -29,6 +30,30 @@ UNIQUE_ID = "testid"
|
||||
MODEL = "model"
|
||||
VERSION = "version"
|
||||
|
||||
CH_TYPE = 1
|
||||
RESPONSE_TOKEN = 1234
|
||||
PIN = "abcd"
|
||||
|
||||
|
||||
class MockStartPairingResponse(object):
|
||||
"""Mock Vizio start pairing response."""
|
||||
|
||||
def __init__(self, ch_type: int, token: int) -> None:
|
||||
"""Initialize mock start pairing response."""
|
||||
self.ch_type = ch_type
|
||||
self.token = token
|
||||
|
||||
|
||||
class MockCompletePairingResponse(object):
|
||||
"""Mock Vizio complete pairing response."""
|
||||
|
||||
def __init__(self, auth_token: str) -> None:
|
||||
"""Initialize mock complete pairing response."""
|
||||
self.auth_token = auth_token
|
||||
|
||||
|
||||
MOCK_PIN_CONFIG = {CONF_PIN: PIN}
|
||||
|
||||
MOCK_USER_VALID_TV_CONFIG = {
|
||||
CONF_NAME: NAME,
|
||||
CONF_HOST: HOST,
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""Tests for Vizio config flow."""
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
@ -17,6 +19,7 @@ from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PIN,
|
||||
)
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
@ -25,6 +28,7 @@ from .const import (
|
||||
HOST,
|
||||
HOST2,
|
||||
MOCK_IMPORT_VALID_TV_CONFIG,
|
||||
MOCK_PIN_CONFIG,
|
||||
MOCK_SPEAKER_CONFIG,
|
||||
MOCK_TV_CONFIG_NO_TOKEN,
|
||||
MOCK_USER_VALID_TV_CONFIG,
|
||||
@ -37,6 +41,8 @@ from .const import (
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def test_user_flow_minimum_fields(
|
||||
hass: HomeAssistantType,
|
||||
@ -197,7 +203,7 @@ async def test_user_esn_already_exists(
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_setup_with_diff_host_and_name"
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_user_error_on_could_not_connect(
|
||||
@ -212,18 +218,73 @@ async def test_user_error_on_could_not_connect(
|
||||
assert result["errors"] == {"base": "cant_connect"}
|
||||
|
||||
|
||||
async def test_user_error_on_tv_needs_token(
|
||||
async def test_user_tv_pairing(
|
||||
hass: HomeAssistantType,
|
||||
vizio_connect: pytest.fixture,
|
||||
vizio_bypass_setup: pytest.fixture,
|
||||
vizio_complete_pairing: pytest.fixture,
|
||||
) -> None:
|
||||
"""Test when config fails custom validation for non null access token when device_class = tv during user setup."""
|
||||
"""Test pairing config flow when access token not provided for tv during user entry."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"base": "tv_needs_token"}
|
||||
assert result["step_id"] == "pair_tv"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_PIN_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "pairing_complete"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == NAME
|
||||
assert result["data"][CONF_NAME] == NAME
|
||||
assert result["data"][CONF_HOST] == HOST
|
||||
assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_TV
|
||||
|
||||
|
||||
async def test_user_start_pairing_failure(
|
||||
hass: HomeAssistantType,
|
||||
vizio_connect: pytest.fixture,
|
||||
vizio_bypass_setup: pytest.fixture,
|
||||
vizio_start_pairing_failure: pytest.fixture,
|
||||
) -> None:
|
||||
"""Test failure to start pairing from user config flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "cant_connect"}
|
||||
|
||||
|
||||
async def test_user_invalid_pin(
|
||||
hass: HomeAssistantType,
|
||||
vizio_connect: pytest.fixture,
|
||||
vizio_bypass_setup: pytest.fixture,
|
||||
vizio_invalid_pin_failure: pytest.fixture,
|
||||
) -> None:
|
||||
"""Test failure to complete pairing from user config flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "pair_tv"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_PIN_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "pair_tv"
|
||||
assert result["errors"] == {CONF_PIN: "complete_pairing_failed"}
|
||||
|
||||
|
||||
async def test_import_flow_minimum_fields(
|
||||
@ -354,6 +415,76 @@ async def test_import_flow_update_name(
|
||||
assert hass.config_entries.async_get_entry(entry_id).data[CONF_NAME] == NAME2
|
||||
|
||||
|
||||
async def test_import_needs_pairing(
|
||||
hass: HomeAssistantType,
|
||||
vizio_connect: pytest.fixture,
|
||||
vizio_bypass_setup: pytest.fixture,
|
||||
vizio_complete_pairing: pytest.fixture,
|
||||
) -> None:
|
||||
"""Test pairing config flow when access token not provided for tv during import."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_TV_CONFIG_NO_TOKEN
|
||||
)
|
||||
|
||||
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=MOCK_TV_CONFIG_NO_TOKEN
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "pair_tv"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_PIN_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "pairing_complete_import"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == NAME
|
||||
assert result["data"][CONF_NAME] == NAME
|
||||
assert result["data"][CONF_HOST] == HOST
|
||||
assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_TV
|
||||
|
||||
|
||||
async def test_import_error(
|
||||
hass: HomeAssistantType,
|
||||
vizio_connect: pytest.fixture,
|
||||
vizio_bypass_setup: pytest.fixture,
|
||||
caplog: pytest.fixture,
|
||||
) -> None:
|
||||
"""Test that error is logged when import config has an error."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=vol.Schema(VIZIO_SCHEMA)(MOCK_SPEAKER_CONFIG),
|
||||
options={CONF_VOLUME_STEP: VOLUME_STEP},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
fail_entry = MOCK_SPEAKER_CONFIG.copy()
|
||||
fail_entry[CONF_HOST] = "0.0.0.0"
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=vol.Schema(VIZIO_SCHEMA)(fail_entry),
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
# Ensure error gets logged
|
||||
vizio_log_list = [
|
||||
log
|
||||
for log in caplog.records
|
||||
if log.name == "homeassistant.components.vizio.config_flow"
|
||||
]
|
||||
assert len(vizio_log_list) == 1
|
||||
|
||||
|
||||
async def test_zeroconf_flow(
|
||||
hass: HomeAssistantType,
|
||||
vizio_connect: pytest.fixture,
|
||||
|
Loading…
x
Reference in New Issue
Block a user