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

pull/38730/head
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._data = None
self._apps = {} self._apps = {}
async def _create_entry_if_unique( async def _create_entry(self, input_dict: Dict[str, Any]) -> Dict[str, Any]:
self, input_dict: Dict[str, Any] """Create vizio config entry."""
) -> Dict[str, Any]:
"""
Create entry if ID is unique.
If it is, create entry. If it isn't, abort config flow.
"""
# Remove extra keys that will not be used by entry setup # 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_APPS_TO_INCLUDE_OR_EXCLUDE, None)
input_dict.pop(CONF_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: if user_input is not None:
# Store current values in case setup fails and user needs to edit # Store current values in case setup fails and user needs to edit
self._user_schema = _get_config_schema(user_input) 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 if not unique_id:
for entry in self.hass.config_entries.async_entries(DOMAIN): errors[CONF_HOST] = "cannot_connect"
# If source is ignore bypass host and name check and continue through loop else:
if entry.source == SOURCE_IGNORE: # Set unique ID and abort if a flow with the same unique ID is already in progress
continue existing_entry = await self.async_set_unique_id(
if await self.hass.async_add_executor_job( unique_id=unique_id, raise_on_progress=True
_host_is_same, entry.data[CONF_HOST], user_input[CONF_HOST] )
): # If device was discovered, abort if existing entry found, otherwise display an error
errors[CONF_HOST] = "host_exists" # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
if self.context["source"] == SOURCE_ZEROCONF:
if entry.data[CONF_NAME] == user_input[CONF_NAME]: self._abort_if_unique_id_configured()
errors[CONF_NAME] = "name_exists" elif existing_entry:
errors[CONF_HOST] = "existing_config_entry_found"
if not errors: if not errors:
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 # 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" errors["base"] = "cannot_connect"
if not errors: if not errors:
unique_id = await VizioAsync.get_unique_id( return await self._create_entry(user_input)
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)
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
elif self._must_show_form and self.context["source"] == SOURCE_IMPORT: elif self._must_show_form and self.context["source"] == SOURCE_IMPORT:
# Import should always display the config form if CONF_ACCESS_TOKEN # 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]: ) -> Dict[str, Any]:
"""Handle zeroconf discovery.""" """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[ discovery_info[
CONF_HOST CONF_HOST
] = f"{discovery_info[CONF_HOST]}:{discovery_info[CONF_PORT]}" ] = 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 # Set default name to discovered device name by stripping zeroconf service
# (`type`) from `name` # (`type`) from `name`
num_chars_to_strip = len(discovery_info[CONF_TYPE]) + 1 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._data[CONF_ACCESS_TOKEN] = pair_data.auth_token
self._must_show_form = True 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 # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
if self.context["source"] == SOURCE_IMPORT: if self.context["source"] == SOURCE_IMPORT:
# If user is pairing via config import, show different message # 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]: async def _pairing_complete(self, step_id: str) -> Dict[str, Any]:
"""Handle config flow completion.""" """Handle config flow completion."""
if not self._must_show_form: 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 self._must_show_form = False
return self.async_show_form( return self.async_show_form(

View File

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

View File

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

View File

@ -28,10 +28,9 @@
} }
}, },
"error": { "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.", "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": { "abort": {
"already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", "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 pyvesync==1.1.0
# homeassistant.components.vizio # homeassistant.components.vizio
pyvizio==0.1.49 pyvizio==0.1.51
# homeassistant.components.velux # homeassistant.components.velux
pyvlx==0.2.16 pyvlx==0.2.16

View File

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

View File

@ -47,15 +47,32 @@ def skip_notifications_fixture():
yield 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") @pytest.fixture(name="vizio_connect")
def vizio_connect_fixture(): def vizio_connect_fixture():
"""Mock valid vizio device and entry setup.""" """Mock valid vizio device and entry setup."""
with patch( with patch(
"homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config", "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config",
return_value=True, return_value=True,
), patch(
"homeassistant.components.vizio.config_flow.VizioAsync.get_unique_id",
return_value=UNIQUE_ID,
): ):
yield yield

View File

@ -51,7 +51,6 @@ from .const import (
NAME2, NAME2,
UNIQUE_ID, UNIQUE_ID,
VOLUME_STEP, VOLUME_STEP,
ZEROCONF_HOST,
) )
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -223,7 +222,10 @@ async def test_user_host_already_configured(
) -> None: ) -> None:
"""Test host is already configured during user setup.""" """Test host is already configured during user setup."""
entry = MockConfigEntry( 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) entry.add_to_hass(hass)
fail_entry = MOCK_SPEAKER_CONFIG.copy() 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["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, hass: HomeAssistantType,
vizio_connect: pytest.fixture, vizio_connect: pytest.fixture,
vizio_bypass_setup: pytest.fixture, vizio_bypass_setup: pytest.fixture,
) -> None: ) -> None:
"""Test host is already configured during user setup when existing entry has no port.""" """Test serial_number is already configured with different host and name during user setup."""
# 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."""
# Set up new entry # Set up new entry
MockConfigEntry( MockConfigEntry(
domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, unique_id=UNIQUE_ID 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 DOMAIN, context={"source": SOURCE_USER}, data=fail_entry
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["reason"] == "already_configured" assert result["errors"] == {CONF_HOST: "existing_config_entry_found"}
async def test_user_error_on_could_not_connect( 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 hass: HomeAssistantType, vizio_cant_connect: pytest.fixture
) -> None: ) -> 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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG
) )
@ -683,6 +651,7 @@ async def test_import_error(
domain=DOMAIN, domain=DOMAIN,
data=vol.Schema(VIZIO_SCHEMA)(MOCK_SPEAKER_CONFIG), data=vol.Schema(VIZIO_SCHEMA)(MOCK_SPEAKER_CONFIG),
options={CONF_VOLUME_STEP: VOLUME_STEP}, options={CONF_VOLUME_STEP: VOLUME_STEP},
unique_id=UNIQUE_ID,
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
fail_entry = MOCK_SPEAKER_CONFIG.copy() fail_entry = MOCK_SPEAKER_CONFIG.copy()
@ -763,10 +732,14 @@ async def test_zeroconf_flow_already_configured(
hass: HomeAssistantType, hass: HomeAssistantType,
vizio_connect: pytest.fixture, vizio_connect: pytest.fixture,
vizio_bypass_setup: pytest.fixture, vizio_bypass_setup: pytest.fixture,
vizio_guess_device_type: pytest.fixture,
) -> None: ) -> None:
"""Test entity is already configured during zeroconf setup.""" """Test entity is already configured during zeroconf setup."""
entry = MockConfigEntry( 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) entry.add_to_hass(hass)
@ -778,7 +751,7 @@ async def test_zeroconf_flow_already_configured(
# Flow should abort because device is already setup # Flow should abort because device is already setup
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT 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( async def test_zeroconf_dupe_fail(
@ -842,7 +815,7 @@ async def test_zeroconf_abort_when_ignored(
data=MOCK_SPEAKER_CONFIG, data=MOCK_SPEAKER_CONFIG,
options={CONF_VOLUME_STEP: VOLUME_STEP}, options={CONF_VOLUME_STEP: VOLUME_STEP},
source=SOURCE_IGNORE, source=SOURCE_IGNORE,
unique_id=ZEROCONF_HOST, unique_id=UNIQUE_ID,
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
@ -860,12 +833,16 @@ async def test_zeroconf_flow_already_configured_hostname(
vizio_connect: pytest.fixture, vizio_connect: pytest.fixture,
vizio_bypass_setup: pytest.fixture, vizio_bypass_setup: pytest.fixture,
vizio_hostname_check: pytest.fixture, vizio_hostname_check: pytest.fixture,
vizio_guess_device_type: pytest.fixture,
) -> None: ) -> None:
"""Test entity is already configured during zeroconf setup when existing entry uses hostname.""" """Test entity is already configured during zeroconf setup when existing entry uses hostname."""
config = MOCK_SPEAKER_CONFIG.copy() config = MOCK_SPEAKER_CONFIG.copy()
config[CONF_HOST] = "hostname" config[CONF_HOST] = "hostname"
entry = MockConfigEntry( 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) 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 # Flow should abort because device is already setup
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT 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( async def test_import_flow_already_configured_hostname(