diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index 2095c5cc209..ec6e9b54551 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -14,7 +14,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script from .const import ( + ATTR_DEVICE_INFO, ATTR_REMOTE, + ATTR_UDN, CONF_APP_ID, CONF_ENCRYPTION_KEY, CONF_ON_ACTION, @@ -86,6 +88,22 @@ async def async_setup_entry(hass, config_entry): panasonic_viera_data[config_entry.entry_id] = {ATTR_REMOTE: remote} + # Add device_info to older config entries + if ATTR_DEVICE_INFO not in config or config[ATTR_DEVICE_INFO] is None: + device_info = await remote.async_get_device_info() + unique_id = config_entry.unique_id + if device_info is None: + _LOGGER.error( + "Couldn't gather device info. Please restart Home Assistant with your TV turned on and connected to your network." + ) + else: + unique_id = device_info[ATTR_UDN] + hass.config_entries.async_update_entry( + config_entry, + unique_id=unique_id, + data={**config, ATTR_DEVICE_INFO: device_info}, + ) + for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, component) @@ -223,6 +241,12 @@ class Remote: _LOGGER.debug("Play media: %s (%s)", media_id, media_type) await self._handle_errors(self._control.open_webpage, media_id) + async def async_get_device_info(self): + """Return device info.""" + if self._control is None: + return None + return await self._handle_errors(self._control.get_device_info) + async def _handle_errors(self, func, *args): """Handle errors from func, set available and reconnect if needed.""" try: diff --git a/homeassistant/components/panasonic_viera/config_flow.py b/homeassistant/components/panasonic_viera/config_flow.py index 8489c69fe66..b39d3a1d3c8 100644 --- a/homeassistant/components/panasonic_viera/config_flow.py +++ b/homeassistant/components/panasonic_viera/config_flow.py @@ -10,6 +10,9 @@ from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PIN, CONF_PORT from .const import ( # pylint: disable=unused-import + ATTR_DEVICE_INFO, + ATTR_FRIENDLY_NAME, + ATTR_UDN, CONF_APP_ID, CONF_ENCRYPTION_KEY, CONF_ON_ACTION, @@ -35,6 +38,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_NAME: None, CONF_PORT: None, CONF_ON_ACTION: None, + ATTR_DEVICE_INFO: None, } self._remote = None @@ -49,6 +53,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._remote = await self.hass.async_add_executor_job( partial(RemoteControl, self._data[CONF_HOST], self._data[CONF_PORT]) ) + + self._data[ATTR_DEVICE_INFO] = await self.hass.async_add_executor_job( + self._remote.get_device_info + ) except (TimeoutError, URLError, SOAPError, OSError) as err: _LOGGER.error("Could not establish remote connection: %s", err) errors["base"] = "cannot_connect" @@ -57,6 +65,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="unknown") if "base" not in errors: + await self.async_set_unique_id(self._data[ATTR_DEVICE_INFO][ATTR_UDN]) + self._abort_if_unique_id_configured() + + if self._data[CONF_NAME] == DEFAULT_NAME: + self._data[CONF_NAME] = self._data[ATTR_DEVICE_INFO][ + ATTR_FRIENDLY_NAME + ].replace("_", " ") + if self._remote.type == TV_TYPE_ENCRYPTED: return await self.async_step_pairing() diff --git a/homeassistant/components/panasonic_viera/const.py b/homeassistant/components/panasonic_viera/const.py index 2fb264a6b1c..36b61360441 100644 --- a/homeassistant/components/panasonic_viera/const.py +++ b/homeassistant/components/panasonic_viera/const.py @@ -12,4 +12,10 @@ DEFAULT_PORT = 55000 ATTR_REMOTE = "remote" +ATTR_DEVICE_INFO = "device_info" +ATTR_FRIENDLY_NAME = "friendlyName" +ATTR_MANUFACTURER = "manufacturer" +ATTR_MODEL_NUMBER = "modelNumber" +ATTR_UDN = "UDN" + ERROR_INVALID_PIN_CODE = "invalid_pin_code" diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index 934067acfaf..fe80e9a770f 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -20,7 +20,14 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.const import CONF_NAME -from .const import ATTR_REMOTE, DOMAIN +from .const import ( + ATTR_DEVICE_INFO, + ATTR_MANUFACTURER, + ATTR_MODEL_NUMBER, + ATTR_REMOTE, + ATTR_UDN, + DOMAIN, +) SUPPORT_VIERATV = ( SUPPORT_PAUSE @@ -46,24 +53,39 @@ async def async_setup_entry(hass, config_entry, async_add_entities): remote = hass.data[DOMAIN][config_entry.entry_id][ATTR_REMOTE] name = config[CONF_NAME] + device_info = config[ATTR_DEVICE_INFO] - tv_device = PanasonicVieraTVEntity(remote, name) + tv_device = PanasonicVieraTVEntity(remote, name, device_info) async_add_entities([tv_device]) class PanasonicVieraTVEntity(MediaPlayerEntity): """Representation of a Panasonic Viera TV.""" - def __init__(self, remote, name, uuid=None): + def __init__(self, remote, name, device_info): """Initialize the entity.""" self._remote = remote self._name = name - self._uuid = uuid + self._device_info = device_info @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of the device.""" - return self._uuid + if self._device_info is not None: + return self._device_info[ATTR_UDN] + return None + + @property + def device_info(self): + """Return device specific attributes.""" + if self._device_info is None: + return None + return { + "name": self._name, + "identifiers": {(DOMAIN, self._device_info[ATTR_UDN])}, + "manufacturer": self._device_info[ATTR_MANUFACTURER], + "model": self._device_info[ATTR_MODEL_NUMBER], + } @property def name(self): diff --git a/tests/components/panasonic_viera/test_config_flow.py b/tests/components/panasonic_viera/test_config_flow.py index 39d6913e3a8..f3d1f1cc8f1 100644 --- a/tests/components/panasonic_viera/test_config_flow.py +++ b/tests/components/panasonic_viera/test_config_flow.py @@ -4,6 +4,11 @@ import pytest from homeassistant import config_entries from homeassistant.components.panasonic_viera.const import ( + ATTR_DEVICE_INFO, + ATTR_FRIENDLY_NAME, + ATTR_MANUFACTURER, + ATTR_MODEL_NUMBER, + ATTR_UDN, CONF_APP_ID, CONF_ENCRYPTION_KEY, CONF_ON_ACTION, @@ -36,6 +41,10 @@ def get_mock_remote( encrypted=False, app_id=None, encryption_key=None, + name=DEFAULT_NAME, + manufacturer="mock-manufacturer", + model_number="mock-model-number", + unique_id="mock-unique-id", ): """Return a mock remote.""" mock_remote = Mock() @@ -58,6 +67,16 @@ def get_mock_remote( mock_remote.authorize_pin_code = authorize_pin_code + def get_device_info(): + return { + ATTR_FRIENDLY_NAME: name, + ATTR_MANUFACTURER: manufacturer, + ATTR_MODEL_NUMBER: model_number, + ATTR_UDN: unique_id, + } + + mock_remote.get_device_info = get_device_info + return mock_remote @@ -89,6 +108,12 @@ async def test_flow_non_encrypted(hass): CONF_NAME: DEFAULT_NAME, CONF_PORT: DEFAULT_PORT, CONF_ON_ACTION: None, + ATTR_DEVICE_INFO: { + ATTR_FRIENDLY_NAME: DEFAULT_NAME, + ATTR_MANUFACTURER: "mock-manufacturer", + ATTR_MODEL_NUMBER: "mock-model-number", + ATTR_UDN: "mock-unique-id", + }, } @@ -181,6 +206,12 @@ async def test_flow_encrypted_valid_pin_code(hass): CONF_ON_ACTION: None, CONF_APP_ID: "test-app-id", CONF_ENCRYPTION_KEY: "test-encryption-key", + ATTR_DEVICE_INFO: { + ATTR_FRIENDLY_NAME: DEFAULT_NAME, + ATTR_MANUFACTURER: "mock-manufacturer", + ATTR_MODEL_NUMBER: "mock-model-number", + ATTR_UDN: "mock-unique-id", + }, } @@ -359,6 +390,12 @@ async def test_imported_flow_non_encrypted(hass): CONF_NAME: DEFAULT_NAME, CONF_PORT: DEFAULT_PORT, CONF_ON_ACTION: "test-on-action", + ATTR_DEVICE_INFO: { + ATTR_FRIENDLY_NAME: DEFAULT_NAME, + ATTR_MANUFACTURER: "mock-manufacturer", + ATTR_MODEL_NUMBER: "mock-model-number", + ATTR_UDN: "mock-unique-id", + }, } @@ -403,6 +440,12 @@ async def test_imported_flow_encrypted_valid_pin_code(hass): CONF_ON_ACTION: "test-on-action", CONF_APP_ID: "test-app-id", CONF_ENCRYPTION_KEY: "test-encryption-key", + ATTR_DEVICE_INFO: { + ATTR_FRIENDLY_NAME: DEFAULT_NAME, + ATTR_MANUFACTURER: "mock-manufacturer", + ATTR_MODEL_NUMBER: "mock-model-number", + ATTR_UDN: "mock-unique-id", + }, } diff --git a/tests/components/panasonic_viera/test_init.py b/tests/components/panasonic_viera/test_init.py index a4a1ca94fe5..70b8db656e1 100644 --- a/tests/components/panasonic_viera/test_init.py +++ b/tests/components/panasonic_viera/test_init.py @@ -1,5 +1,10 @@ """Test the Panasonic Viera setup process.""" from homeassistant.components.panasonic_viera.const import ( + ATTR_DEVICE_INFO, + ATTR_FRIENDLY_NAME, + ATTR_MANUFACTURER, + ATTR_MODEL_NUMBER, + ATTR_UDN, CONF_APP_ID, CONF_ENCRYPTION_KEY, CONF_ON_ACTION, @@ -26,8 +31,15 @@ MOCK_ENCRYPTION_DATA = { CONF_ENCRYPTION_KEY: "mock-encryption-key", } +MOCK_DEVICE_INFO = { + ATTR_FRIENDLY_NAME: DEFAULT_NAME, + ATTR_MANUFACTURER: "mock-manufacturer", + ATTR_MODEL_NUMBER: "mock-model-number", + ATTR_UDN: "mock-unique-id", +} -def get_mock_remote(): + +def get_mock_remote(device_info=MOCK_DEVICE_INFO): """Return a mock remote.""" mock_remote = Mock() @@ -36,6 +48,11 @@ def get_mock_remote(): mock_remote.async_create_remote_control = async_create_remote_control + async def async_get_device_info(): + return device_info + + mock_remote.async_get_device_info = async_get_device_info + return mock_remote @@ -43,8 +60,8 @@ async def test_setup_entry_encrypted(hass): """Test setup with encrypted config entry.""" mock_entry = MockConfigEntry( domain=DOMAIN, - unique_id=MOCK_CONFIG_DATA[CONF_HOST], - data={**MOCK_CONFIG_DATA, **MOCK_ENCRYPTION_DATA}, + unique_id=MOCK_DEVICE_INFO[ATTR_UDN], + data={**MOCK_CONFIG_DATA, **MOCK_ENCRYPTION_DATA, **MOCK_DEVICE_INFO}, ) mock_entry.add_to_hass(hass) @@ -64,8 +81,89 @@ async def test_setup_entry_encrypted(hass): assert state.name == DEFAULT_NAME +async def test_setup_entry_encrypted_missing_device_info(hass): + """Test setup with encrypted config entry and missing device info.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=MOCK_CONFIG_DATA[CONF_HOST], + data={**MOCK_CONFIG_DATA, **MOCK_ENCRYPTION_DATA}, + ) + + mock_entry.add_to_hass(hass) + + mock_remote = get_mock_remote() + + with patch( + "homeassistant.components.panasonic_viera.Remote", + return_value=mock_remote, + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert mock_entry.data[ATTR_DEVICE_INFO] == MOCK_DEVICE_INFO + assert mock_entry.unique_id == MOCK_DEVICE_INFO[ATTR_UDN] + + state = hass.states.get("media_player.panasonic_viera_tv") + + assert state + assert state.name == DEFAULT_NAME + + +async def test_setup_entry_encrypted_missing_device_info_none(hass): + """Test setup with encrypted config entry and device info set to None.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=MOCK_CONFIG_DATA[CONF_HOST], + data={**MOCK_CONFIG_DATA, **MOCK_ENCRYPTION_DATA}, + ) + + mock_entry.add_to_hass(hass) + + mock_remote = get_mock_remote(device_info=None) + + with patch( + "homeassistant.components.panasonic_viera.Remote", + return_value=mock_remote, + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert mock_entry.data[ATTR_DEVICE_INFO] is None + assert mock_entry.unique_id == MOCK_CONFIG_DATA[CONF_HOST] + + state = hass.states.get("media_player.panasonic_viera_tv") + + assert state + assert state.name == DEFAULT_NAME + + async def test_setup_entry_unencrypted(hass): """Test setup with unencrypted config entry.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=MOCK_DEVICE_INFO[ATTR_UDN], + data={**MOCK_CONFIG_DATA, **MOCK_DEVICE_INFO}, + ) + + mock_entry.add_to_hass(hass) + + mock_remote = get_mock_remote() + + with patch( + "homeassistant.components.panasonic_viera.Remote", + return_value=mock_remote, + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("media_player.panasonic_viera_tv") + + assert state + assert state.name == DEFAULT_NAME + + +async def test_setup_entry_unencrypted_missing_device_info(hass): + """Test setup with unencrypted config entry and missing device info.""" mock_entry = MockConfigEntry( domain=DOMAIN, unique_id=MOCK_CONFIG_DATA[CONF_HOST], @@ -83,6 +181,37 @@ async def test_setup_entry_unencrypted(hass): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() + assert mock_entry.data[ATTR_DEVICE_INFO] == MOCK_DEVICE_INFO + assert mock_entry.unique_id == MOCK_DEVICE_INFO[ATTR_UDN] + + state = hass.states.get("media_player.panasonic_viera_tv") + + assert state + assert state.name == DEFAULT_NAME + + +async def test_setup_entry_unencrypted_missing_device_info_none(hass): + """Test setup with unencrypted config entry and device info set to None.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=MOCK_CONFIG_DATA[CONF_HOST], + data=MOCK_CONFIG_DATA, + ) + + mock_entry.add_to_hass(hass) + + mock_remote = get_mock_remote(device_info=None) + + with patch( + "homeassistant.components.panasonic_viera.Remote", + return_value=mock_remote, + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert mock_entry.data[ATTR_DEVICE_INFO] is None + assert mock_entry.unique_id == MOCK_CONFIG_DATA[CONF_HOST] + state = hass.states.get("media_player.panasonic_viera_tv") assert state @@ -106,7 +235,7 @@ async def test_setup_config_flow_initiated(hass): async def test_setup_unload_entry(hass): """Test if config entry is unloaded.""" mock_entry = MockConfigEntry( - domain=DOMAIN, unique_id=MOCK_CONFIG_DATA[CONF_HOST], data=MOCK_CONFIG_DATA + domain=DOMAIN, unique_id=MOCK_DEVICE_INFO[ATTR_UDN], data=MOCK_CONFIG_DATA ) mock_entry.add_to_hass(hass)