Simplify vizio unique ID check since only IP and device class are needed (#37692)

This commit is contained in:
Raman Gupta 2020-08-12 10:50:36 -04:00 committed by GitHub
parent 444df4a7d2
commit fbf44b37a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 88 additions and 140 deletions

View File

@ -180,14 +180,8 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._data = None
self._apps = {}
async def _create_entry_if_unique(
self, input_dict: Dict[str, Any]
) -> Dict[str, Any]:
"""
Create entry if ID is unique.
If it is, create entry. If it isn't, abort config flow.
"""
async def _create_entry(self, input_dict: Dict[str, Any]) -> Dict[str, Any]:
"""Create vizio config entry."""
# Remove extra keys that will not be used by entry setup
input_dict.pop(CONF_APPS_TO_INCLUDE_OR_EXCLUDE, None)
input_dict.pop(CONF_INCLUDE_OR_EXCLUDE, None)
@ -206,19 +200,25 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None:
# Store current values in case setup fails and user needs to edit
self._user_schema = _get_config_schema(user_input)
unique_id = await VizioAsync.get_unique_id(
user_input[CONF_HOST],
user_input[CONF_DEVICE_CLASS],
session=async_get_clientsession(self.hass, False),
)
# Check if new config entry matches any existing config entries
for entry in self.hass.config_entries.async_entries(DOMAIN):
# If source is ignore bypass host and name check and continue through loop
if entry.source == SOURCE_IGNORE:
continue
if await self.hass.async_add_executor_job(
_host_is_same, entry.data[CONF_HOST], user_input[CONF_HOST]
):
errors[CONF_HOST] = "host_exists"
if entry.data[CONF_NAME] == user_input[CONF_NAME]:
errors[CONF_NAME] = "name_exists"
if not unique_id:
errors[CONF_HOST] = "cannot_connect"
else:
# Set unique ID and abort if a flow with the same unique ID is already in progress
existing_entry = await self.async_set_unique_id(
unique_id=unique_id, raise_on_progress=True
)
# If device was discovered, abort if existing entry found, otherwise display an error
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
if self.context["source"] == SOURCE_ZEROCONF:
self._abort_if_unique_id_configured()
elif existing_entry:
errors[CONF_HOST] = "existing_config_entry_found"
if not errors:
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
@ -239,21 +239,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect"
if not errors:
unique_id = await VizioAsync.get_unique_id(
user_input[CONF_HOST],
user_input.get(CONF_ACCESS_TOKEN),
user_input[CONF_DEVICE_CLASS],
session=async_get_clientsession(self.hass, False),
)
# Set unique ID and abort if unique ID is already configured on an entry or a flow
# with the unique ID is already in progress
await self.async_set_unique_id(
unique_id=unique_id, raise_on_progress=True
)
self._abort_if_unique_id_configured()
return await self._create_entry_if_unique(user_input)
return await self._create_entry(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
@ -350,27 +336,10 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) -> Dict[str, Any]:
"""Handle zeroconf discovery."""
# Set unique ID early to prevent device from getting rediscovered multiple times
await self.async_set_unique_id(
unique_id=discovery_info[CONF_HOST].split(":")[0], raise_on_progress=True
)
self._abort_if_unique_id_configured()
discovery_info[
CONF_HOST
] = f"{discovery_info[CONF_HOST]}:{discovery_info[CONF_PORT]}"
# Check if new config entry matches any existing config entries and abort if so
for entry in self.hass.config_entries.async_entries(DOMAIN):
# If source is ignore bypass host check and continue through loop
if entry.source == SOURCE_IGNORE:
continue
if await self.hass.async_add_executor_job(
_host_is_same, entry.data[CONF_HOST], discovery_info[CONF_HOST]
):
return self.async_abort(reason="already_configured_device")
# Set default name to discovered device name by stripping zeroconf service
# (`type`) from `name`
num_chars_to_strip = len(discovery_info[CONF_TYPE]) + 1
@ -436,20 +405,6 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._data[CONF_ACCESS_TOKEN] = pair_data.auth_token
self._must_show_form = True
unique_id = await VizioAsync.get_unique_id(
self._data[CONF_HOST],
self._data[CONF_ACCESS_TOKEN],
self._data[CONF_DEVICE_CLASS],
session=async_get_clientsession(self.hass, False),
)
# Set unique ID and abort if unique ID is already configured on an entry or a flow
# with the unique ID is already in progress
await self.async_set_unique_id(
unique_id=unique_id, raise_on_progress=True
)
self._abort_if_unique_id_configured()
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
if self.context["source"] == SOURCE_IMPORT:
# If user is pairing via config import, show different message
@ -470,7 +425,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
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)
return await self._create_entry(self._data)
self._must_show_form = False
return self.async_show_form(

View File

@ -2,7 +2,7 @@
"domain": "vizio",
"name": "VIZIO SmartCast",
"documentation": "https://www.home-assistant.io/integrations/vizio",
"requirements": ["pyvizio==0.1.49"],
"requirements": ["pyvizio==0.1.51"],
"codeowners": ["@raman325"],
"config_flow": true,
"zeroconf": ["_viziocast._tcp.local."],

View File

@ -294,7 +294,7 @@ class VizioDevice(MediaPlayerEntity):
setting_type, setting_name, new_value,
)
async def async_added_to_hass(self):
async def async_added_to_hass(self) -> None:
"""Register callbacks when entity is added."""
# Register callback for when config entry is updated.
self._async_unsub_listeners.append(
@ -310,7 +310,7 @@ class VizioDevice(MediaPlayerEntity):
)
)
async def async_will_remove_from_hass(self):
async def async_will_remove_from_hass(self) -> None:
"""Disconnect callbacks when entity is removed."""
for listener in self._async_unsub_listeners:
listener()
@ -323,7 +323,7 @@ class VizioDevice(MediaPlayerEntity):
return self._available
@property
def state(self) -> str:
def state(self) -> Optional[str]:
"""Return the state of the device."""
return self._state
@ -338,7 +338,7 @@ class VizioDevice(MediaPlayerEntity):
return self._icon
@property
def volume_level(self) -> float:
def volume_level(self) -> Optional[float]:
"""Return the volume level of the device."""
return self._volume_level
@ -348,7 +348,7 @@ class VizioDevice(MediaPlayerEntity):
return self._is_volume_muted
@property
def source(self) -> str:
def source(self) -> Optional[str]:
"""Return current input of the device."""
if self._current_app is not None and self._current_input in INPUT_APPS:
return self._current_app

View File

@ -28,10 +28,9 @@
}
},
"error": {
"host_exists": "[%key:component::vizio::config::step::user::title%] with specified host already configured.",
"name_exists": "[%key:component::vizio::config::step::user::title%] with specified name already configured.",
"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.",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"existing_config_entry_found": "An existing [%key:component::vizio::config::step::user::title%] config entry with the same serial number has already been configured. You must delete the existing entry in order to configure this one."
},
"abort": {
"already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",

View File

@ -1834,7 +1834,7 @@ pyversasense==0.0.6
pyvesync==1.1.0
# homeassistant.components.vizio
pyvizio==0.1.49
pyvizio==0.1.51
# homeassistant.components.velux
pyvlx==0.2.16

View File

@ -839,7 +839,7 @@ pyvera==0.3.9
pyvesync==1.1.0
# homeassistant.components.vizio
pyvizio==0.1.49
pyvizio==0.1.51
# homeassistant.components.volumio
pyvolumio==0.1.1

View File

@ -47,15 +47,32 @@ def skip_notifications_fixture():
yield
@pytest.fixture(name="vizio_get_unique_id", autouse=True)
def vizio_get_unique_id_fixture():
"""Mock get vizio unique ID."""
with patch(
"homeassistant.components.vizio.config_flow.VizioAsync.get_unique_id",
return_value=UNIQUE_ID,
):
yield
@pytest.fixture(name="vizio_no_unique_id")
def vizio_no_unique_id_fixture():
"""Mock no vizio unique ID returrned."""
with patch(
"homeassistant.components.vizio.config_flow.VizioAsync.get_unique_id",
return_value=None,
):
yield
@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,
):
yield

View File

@ -51,7 +51,6 @@ from .const import (
NAME2,
UNIQUE_ID,
VOLUME_STEP,
ZEROCONF_HOST,
)
from tests.common import MockConfigEntry
@ -223,7 +222,10 @@ async def test_user_host_already_configured(
) -> None:
"""Test host is already configured during user setup."""
entry = MockConfigEntry(
domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, options={CONF_VOLUME_STEP: VOLUME_STEP}
domain=DOMAIN,
data=MOCK_SPEAKER_CONFIG,
options={CONF_VOLUME_STEP: VOLUME_STEP},
unique_id=UNIQUE_ID,
)
entry.add_to_hass(hass)
fail_entry = MOCK_SPEAKER_CONFIG.copy()
@ -234,61 +236,15 @@ async def test_user_host_already_configured(
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {CONF_HOST: "host_exists"}
assert result["errors"] == {CONF_HOST: "existing_config_entry_found"}
async def test_user_host_already_configured_no_port(
async def test_user_serial_number_already_exists(
hass: HomeAssistantType,
vizio_connect: pytest.fixture,
vizio_bypass_setup: pytest.fixture,
) -> None:
"""Test host is already configured during user setup when existing entry has no port."""
# Mock entry without port so we can test that the same entry WITH a port will fail
no_port_entry = MOCK_SPEAKER_CONFIG.copy()
no_port_entry[CONF_HOST] = no_port_entry[CONF_HOST].split(":")[0]
entry = MockConfigEntry(
domain=DOMAIN, data=no_port_entry, options={CONF_VOLUME_STEP: VOLUME_STEP}
)
entry.add_to_hass(hass)
fail_entry = MOCK_SPEAKER_CONFIG.copy()
fail_entry[CONF_NAME] = "newtestname"
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=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: pytest.fixture,
vizio_bypass_setup: pytest.fixture,
) -> None:
"""Test name is already configured during user setup."""
entry = MockConfigEntry(
domain=DOMAIN, data=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_USER}, data=fail_entry
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {CONF_NAME: "name_exists"}
async def test_user_esn_already_exists(
hass: HomeAssistantType,
vizio_connect: pytest.fixture,
vizio_bypass_setup: pytest.fixture,
) -> None:
"""Test ESN is already configured with different host and name during user setup."""
"""Test serial_number is already configured with different host and name during user setup."""
# Set up new entry
MockConfigEntry(
domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, unique_id=UNIQUE_ID
@ -303,14 +259,26 @@ async def test_user_esn_already_exists(
DOMAIN, context={"source": SOURCE_USER}, data=fail_entry
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {CONF_HOST: "existing_config_entry_found"}
async def test_user_error_on_could_not_connect(
hass: HomeAssistantType, vizio_no_unique_id: pytest.fixture
) -> None:
"""Test with could_not_connect during user setup due to no connectivity."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {CONF_HOST: "cannot_connect"}
async def test_user_error_on_could_not_connect_invalid_token(
hass: HomeAssistantType, vizio_cant_connect: pytest.fixture
) -> None:
"""Test with could_not_connect during user_setup."""
"""Test with could_not_connect during user setup due to invalid token."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG
)
@ -683,6 +651,7 @@ async def test_import_error(
domain=DOMAIN,
data=vol.Schema(VIZIO_SCHEMA)(MOCK_SPEAKER_CONFIG),
options={CONF_VOLUME_STEP: VOLUME_STEP},
unique_id=UNIQUE_ID,
)
entry.add_to_hass(hass)
fail_entry = MOCK_SPEAKER_CONFIG.copy()
@ -763,10 +732,14 @@ async def test_zeroconf_flow_already_configured(
hass: HomeAssistantType,
vizio_connect: pytest.fixture,
vizio_bypass_setup: pytest.fixture,
vizio_guess_device_type: pytest.fixture,
) -> None:
"""Test entity is already configured during zeroconf setup."""
entry = MockConfigEntry(
domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, options={CONF_VOLUME_STEP: VOLUME_STEP}
domain=DOMAIN,
data=MOCK_SPEAKER_CONFIG,
options={CONF_VOLUME_STEP: VOLUME_STEP},
unique_id=UNIQUE_ID,
)
entry.add_to_hass(hass)
@ -778,7 +751,7 @@ async def test_zeroconf_flow_already_configured(
# Flow should abort because device is already setup
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured_device"
assert result["reason"] == "already_configured"
async def test_zeroconf_dupe_fail(
@ -842,7 +815,7 @@ async def test_zeroconf_abort_when_ignored(
data=MOCK_SPEAKER_CONFIG,
options={CONF_VOLUME_STEP: VOLUME_STEP},
source=SOURCE_IGNORE,
unique_id=ZEROCONF_HOST,
unique_id=UNIQUE_ID,
)
entry.add_to_hass(hass)
@ -860,12 +833,16 @@ async def test_zeroconf_flow_already_configured_hostname(
vizio_connect: pytest.fixture,
vizio_bypass_setup: pytest.fixture,
vizio_hostname_check: pytest.fixture,
vizio_guess_device_type: pytest.fixture,
) -> None:
"""Test entity is already configured during zeroconf setup when existing entry uses hostname."""
config = MOCK_SPEAKER_CONFIG.copy()
config[CONF_HOST] = "hostname"
entry = MockConfigEntry(
domain=DOMAIN, data=config, options={CONF_VOLUME_STEP: VOLUME_STEP}
domain=DOMAIN,
data=config,
options={CONF_VOLUME_STEP: VOLUME_STEP},
unique_id=UNIQUE_ID,
)
entry.add_to_hass(hass)
@ -877,7 +854,7 @@ async def test_zeroconf_flow_already_configured_hostname(
# Flow should abort because device is already setup
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured_device"
assert result["reason"] == "already_configured"
async def test_import_flow_already_configured_hostname(