diff --git a/homeassistant/components/insteon/config_flow.py b/homeassistant/components/insteon/config_flow.py index d9081c5b45e..5bf8769f321 100644 --- a/homeassistant/components/insteon/config_flow.py +++ b/homeassistant/components/insteon/config_flow.py @@ -1,19 +1,24 @@ """Test config flow for Insteon.""" +from __future__ import annotations + import logging from pyinsteon import async_connect import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import usb from homeassistant.const import ( CONF_ADDRESS, CONF_DEVICE, CONF_HOST, + CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( @@ -107,6 +112,9 @@ def _remove_x10(device, options): class InsteonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Insteon config flow handler.""" + _device_path: str | None = None + _device_name: str | None = None + @staticmethod @callback def async_get_options_flow(config_entry): @@ -177,6 +185,38 @@ class InsteonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="cannot_connect") return self.async_create_entry(title="", data=import_info) + async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: + """Handle USB discovery.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + dev_path = await self.hass.async_add_executor_job( + usb.get_serial_by_id, discovery_info.device + ) + self._device_path = dev_path + self._device_name = usb.human_readable_device_name( + dev_path, + discovery_info.serial_number, + discovery_info.manufacturer, + discovery_info.description, + discovery_info.vid, + discovery_info.pid, + ) + self._set_confirm_only() + self.context["title_placeholders"] = {CONF_NAME: self._device_name} + await self.async_set_unique_id(config_entries.DEFAULT_DISCOVERY_UNIQUE_ID) + return await self.async_step_confirm_usb() + + async def async_step_confirm_usb(self, user_input=None): + """Confirm a discovery.""" + if user_input is not None: + return await self.async_step_plm({CONF_DEVICE: self._device_path}) + + return self.async_show_form( + step_id="confirm_usb", + description_placeholders={CONF_NAME: self._device_name}, + ) + class InsteonOptionsFlowHandler(config_entries.OptionsFlow): """Handle an Insteon options flow.""" diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index e9f5e60f9f8..63eb24ee453 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -6,5 +6,11 @@ "codeowners": ["@teharris1"], "config_flow": true, "iot_class": "local_push", - "loggers": ["pyinsteon", "pypubsub"] + "loggers": ["pyinsteon", "pypubsub"], + "after_dependencies": ["usb"], + "usb": [ + { + "vid": "10BF" + } + ] } diff --git a/homeassistant/components/insteon/strings.json b/homeassistant/components/insteon/strings.json index ca88b43956f..793a38a2694 100644 --- a/homeassistant/components/insteon/strings.json +++ b/homeassistant/components/insteon/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "user": { "description": "Select the Insteon modem type.", @@ -31,6 +32,9 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "confirm_usb": { + "description": "Do you want to setup {name}?" } }, "error": { diff --git a/homeassistant/components/insteon/translations/en.json b/homeassistant/components/insteon/translations/en.json index 18217bb2842..4c4a439b938 100644 --- a/homeassistant/components/insteon/translations/en.json +++ b/homeassistant/components/insteon/translations/en.json @@ -8,7 +8,11 @@ "cannot_connect": "Failed to connect", "select_single": "Select one option." }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "Do you want to setup {name}?" + }, "hubv1": { "data": { "host": "IP Address", diff --git a/homeassistant/generated/usb.py b/homeassistant/generated/usb.py index 1ba9b235f85..2e5104ce66d 100644 --- a/homeassistant/generated/usb.py +++ b/homeassistant/generated/usb.py @@ -6,6 +6,10 @@ To update, run python3 -m script.hassfest # fmt: off USB = [ + { + "domain": "insteon", + "vid": "10BF" + }, { "domain": "modem_callerid", "vid": "0572", diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 9ca54ea8d8f..878b540b721 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -3,6 +3,7 @@ from unittest.mock import patch from homeassistant import config_entries, data_entry_flow +from homeassistant.components import usb from homeassistant.components.insteon.config_flow import ( HUB1, HUB2, @@ -594,3 +595,56 @@ async def test_options_override_bad_data(hass: HomeAssistant): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "input_error"} + + +async def test_discovery_via_usb(hass): + """Test usb flow.""" + discovery_info = usb.UsbServiceInfo( + device="/dev/ttyINSTEON", + pid="AAAA", + vid="AAAA", + serial_number="1234", + description="insteon radio", + manufacturer="test", + ) + result = await hass.config_entries.flow.async_init( + "insteon", context={"source": config_entries.SOURCE_USB}, data=discovery_info + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "confirm_usb" + + with patch("homeassistant.components.insteon.config_flow.async_connect"), patch( + "homeassistant.components.insteon.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["data"] == {"device": "/dev/ttyINSTEON"} + + +async def test_discovery_via_usb_already_setup(hass): + """Test usb flow -- already setup.""" + + MockConfigEntry( + domain=DOMAIN, data={CONF_DEVICE: {CONF_DEVICE: "/dev/ttyUSB1"}} + ).add_to_hass(hass) + + discovery_info = usb.UsbServiceInfo( + device="/dev/ttyINSTEON", + pid="AAAA", + vid="AAAA", + serial_number="1234", + description="insteon radio", + manufacturer="test", + ) + result = await hass.config_entries.flow.async_init( + "insteon", context={"source": config_entries.SOURCE_USB}, data=discovery_info + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed"