diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index 81d77b27a3e..5979caa37db 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -134,13 +134,18 @@ class IPPEntity(Entity): enabled_default: bool = True, ) -> None: """Initialize the IPP entity.""" + self._device_id = None self._enabled_default = enabled_default self._entry_id = entry_id self._icon = icon self._name = name - self._unsub_dispatcher = None self.coordinator = coordinator + if coordinator.data.info.uuid is not None: + self._device_id = coordinator.data.info.uuid + elif coordinator.data.info.serial is not None: + self._device_id = coordinator.data.info.serial + @property def name(self) -> str: """Return the name of the entity.""" @@ -179,8 +184,11 @@ class IPPEntity(Entity): @property def device_info(self) -> Dict[str, Any]: """Return device information about this IPP device.""" + if self._device_id is None: + return None + return { - ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator.data.info.uuid)}, + ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, ATTR_NAME: self.coordinator.data.info.name, ATTR_MANUFACTURER: self.coordinator.data.info.manufacturer, ATTR_MODEL: self.coordinator.data.info.model, diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index 32474881a87..7d1c3d1b1b8 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -24,7 +24,7 @@ from homeassistant.const import ( from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from .const import CONF_BASE_PATH, CONF_UUID +from .const import CONF_BASE_PATH, CONF_SERIAL, CONF_UUID from .const import DOMAIN # pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -47,7 +47,7 @@ async def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]: printer = await ipp.printer() - return {CONF_UUID: printer.info.uuid} + return {CONF_SERIAL: printer.info.serial, CONF_UUID: printer.info.uuid} class IPPFlowHandler(ConfigFlow, domain=DOMAIN): @@ -83,20 +83,28 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): _LOGGER.debug("IPP Error", exc_info=True) return self.async_abort(reason="ipp_error") - user_input[CONF_UUID] = info[CONF_UUID] + unique_id = user_input[CONF_UUID] = info[CONF_UUID] - await self.async_set_unique_id(user_input[CONF_UUID]) + if unique_id is None and info[CONF_SERIAL] is not None: + _LOGGER.debug( + "Printer UUID is missing from IPP response. Falling back to IPP serial number" + ) + unique_id = info[CONF_SERIAL] + elif unique_id is None: + _LOGGER.debug("Unable to determine unique id from IPP response") + + await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]}) return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) async def async_step_zeroconf(self, discovery_info: ConfigType) -> Dict[str, Any]: """Handle zeroconf discovery.""" - # Hostname is format: EPSON123456.local. - host = discovery_info["hostname"].rstrip(".") - port = discovery_info["port"] - name, _ = host.rsplit(".") - tls = discovery_info["type"] == "_ipps._tcp.local." + port = discovery_info[CONF_PORT] + zctype = discovery_info["type"] + name = discovery_info[CONF_NAME].replace(f".{zctype}", "") + tls = zctype == "_ipps._tcp.local." + base_path = discovery_info["properties"].get("rp", "ipp/print") # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context.update({"title_placeholders": {"name": name}}) @@ -107,8 +115,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): CONF_PORT: port, CONF_SSL: tls, CONF_VERIFY_SSL: False, - CONF_BASE_PATH: "/" - + discovery_info["properties"].get("rp", "ipp/print"), + CONF_BASE_PATH: f"/{base_path}", CONF_NAME: name, CONF_UUID: discovery_info["properties"].get("UUID"), } @@ -130,12 +137,28 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): _LOGGER.debug("IPP Error", exc_info=True) return self.async_abort(reason="ipp_error") - if info[CONF_UUID] is not None: - self.discovery_info[CONF_UUID] = info[CONF_UUID] + unique_id = self.discovery_info[CONF_UUID] + if unique_id is None and info[CONF_UUID] is not None: + _LOGGER.debug( + "Printer UUID is missing from discovery info. Falling back to IPP UUID" + ) + unique_id = self.discovery_info[CONF_UUID] = info[CONF_UUID] + elif unique_id is None and info[CONF_SERIAL] is not None: + _LOGGER.debug( + "Printer UUID is missing from discovery info and IPP response. Falling back to IPP serial number" + ) + unique_id = info[CONF_SERIAL] + elif unique_id is None: + _LOGGER.debug( + "Unable to determine unique id from discovery info and IPP response" + ) - await self.async_set_unique_id(self.discovery_info[CONF_UUID]) + await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured( - updates={CONF_HOST: self.discovery_info[CONF_HOST]} + updates={ + CONF_HOST: self.discovery_info[CONF_HOST], + CONF_NAME: self.discovery_info[CONF_NAME], + }, ) return await self.async_step_zeroconf_confirm() diff --git a/homeassistant/components/ipp/const.py b/homeassistant/components/ipp/const.py index 7caf60b7edd..a5345f4145e 100644 --- a/homeassistant/components/ipp/const.py +++ b/homeassistant/components/ipp/const.py @@ -21,5 +21,6 @@ ATTR_URI_SUPPORTED = "uri_supported" # Config Keys CONF_BASE_PATH = "base_path" +CONF_SERIAL = "serial" CONF_TLS = "tls" CONF_UUID = "uuid" diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index fd278d3df2e..5c29be09d94 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -60,6 +60,12 @@ class IPPSensor(IPPEntity): """Initialize IPP sensor.""" self._unit_of_measurement = unit_of_measurement self._key = key + self._unique_id = None + + if coordinator.data.info.uuid is not None: + self._unique_id = f"{coordinator.data.info.uuid}_{key}" + elif coordinator.data.info.serial is not None: + self._unique_id = f"{coordinator.data.info.serial}_{key}" super().__init__( entry_id=entry_id, @@ -72,7 +78,7 @@ class IPPSensor(IPPEntity): @property def unique_id(self) -> str: """Return the unique ID for this sensor.""" - return f"{self.coordinator.data.info.uuid}_{self._key}" + return self._unique_id @property def unit_of_measurement(self) -> str: diff --git a/tests/components/ipp/__init__.py b/tests/components/ipp/__init__.py index 1c52c557024..a8c79324494 100644 --- a/tests/components/ipp/__init__.py +++ b/tests/components/ipp/__init__.py @@ -21,7 +21,7 @@ ATTR_PROPERTIES = "properties" IPP_ZEROCONF_SERVICE_TYPE = "_ipp._tcp.local." IPPS_ZEROCONF_SERVICE_TYPE = "_ipps._tcp.local." -ZEROCONF_NAME = "EPSON123456" +ZEROCONF_NAME = "EPSON XP-6000 Series" ZEROCONF_HOST = "192.168.1.31" ZEROCONF_HOSTNAME = "EPSON123456.local." ZEROCONF_PORT = 631 diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 5229881fbf4..fb75ba9caef 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -50,7 +50,7 @@ async def test_show_zeroconf_form( assert result["step_id"] == "zeroconf_confirm" assert result["type"] == RESULT_TYPE_FORM - assert result["description_placeholders"] == {CONF_NAME: "EPSON123456"} + assert result["description_placeholders"] == {CONF_NAME: "EPSON XP-6000 Series"} async def test_connection_error( @@ -276,8 +276,13 @@ async def test_zeroconf_with_uuid_device_exists_abort( """Test we abort zeroconf flow if printer already configured.""" await init_integration(hass, aioclient_mock) - discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() - discovery_info["properties"]["UUID"] = "cfe92100-67c4-11d4-a45f-f8d027761251" + discovery_info = { + **MOCK_ZEROCONF_IPP_SERVICE_INFO, + "properties": { + **MOCK_ZEROCONF_IPP_SERVICE_INFO["properties"], + "UUID": "cfe92100-67c4-11d4-a45f-f8d027761251", + }, + } result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, ) @@ -315,6 +320,9 @@ async def test_full_user_flow_implementation( assert result["data"][CONF_HOST] == "192.168.1.31" assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251" + assert result["result"] + assert result["result"].unique_id == "cfe92100-67c4-11d4-a45f-f8d027761251" + async def test_full_zeroconf_flow_implementation( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker @@ -339,13 +347,17 @@ async def test_full_zeroconf_flow_implementation( ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "EPSON123456" + assert result["title"] == "EPSON XP-6000 Series" assert result["data"] assert result["data"][CONF_HOST] == "192.168.1.31" + assert result["data"][CONF_NAME] == "EPSON XP-6000 Series" assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251" assert not result["data"][CONF_SSL] + assert result["result"] + assert result["result"].unique_id == "cfe92100-67c4-11d4-a45f-f8d027761251" + async def test_full_zeroconf_tls_flow_implementation( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker @@ -364,17 +376,20 @@ async def test_full_zeroconf_tls_flow_implementation( assert result["step_id"] == "zeroconf_confirm" assert result["type"] == RESULT_TYPE_FORM - assert result["description_placeholders"] == {CONF_NAME: "EPSON123456"} + assert result["description_placeholders"] == {CONF_NAME: "EPSON XP-6000 Series"} result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "EPSON123456" + assert result["title"] == "EPSON XP-6000 Series" assert result["data"] assert result["data"][CONF_HOST] == "192.168.1.31" - assert result["data"][CONF_NAME] == "EPSON123456" + assert result["data"][CONF_NAME] == "EPSON XP-6000 Series" assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251" assert result["data"][CONF_SSL] + + assert result["result"] + assert result["result"].unique_id == "cfe92100-67c4-11d4-a45f-f8d027761251"