diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 66cb91f9330..60807471e03 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -14,6 +14,7 @@ from zwave_js_server.version import VersionInfo, get_server_version from homeassistant import config_entries, exceptions from homeassistant.components import usb from homeassistant.components.hassio import HassioServiceInfo, is_hassio +from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.const import CONF_NAME, CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import ( @@ -337,6 +338,33 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_manual() + async def async_step_zeroconf( + self, discovery_info: ZeroconfServiceInfo + ) -> FlowResult: + """Handle zeroconf discovery.""" + home_id = str(discovery_info.properties["homeId"]) + await self.async_set_unique_id(home_id) + self._abort_if_unique_id_configured() + self.ws_address = f"ws://{discovery_info.host}:{discovery_info.port}" + self.context.update({"title_placeholders": {CONF_NAME: home_id}}) + return await self.async_step_zeroconf_confirm() + + async def async_step_zeroconf_confirm( + self, user_input: dict | None = None + ) -> FlowResult: + """Confirm the setup.""" + if user_input is not None: + return await self.async_step_manual({CONF_URL: self.ws_address}) + + assert self.ws_address + return self.async_show_form( + step_id="zeroconf_confirm", + description_placeholders={ + "home_id": self.unique_id, + CONF_URL: self.ws_address[5:], + }, + ) + async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: """Handle USB Discovery.""" if not is_hassio(self.hass): diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index c3968181563..cdc81b9478b 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -29,5 +29,6 @@ ] } ], + "zeroconf": ["_zwave-js-server._tcp.local."], "loggers": ["zwave_js_server"] } diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 13f65921cdb..91a6eae6ab6 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -36,6 +36,10 @@ }, "hassio_confirm": { "title": "Set up Z-Wave JS integration with the Z-Wave JS add-on" + }, + "zeroconf_confirm": { + "description": "Do you want to add the Z-Wave JS Server with home ID {home_id} found at {url} to Home Assistant?", + "title": "Discovered Z-Wave JS Server" } }, "error": { diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json index 3962674ff9c..843f2aaf284 100644 --- a/homeassistant/components/zwave_js/translations/en.json +++ b/homeassistant/components/zwave_js/translations/en.json @@ -26,7 +26,6 @@ "step": { "configure_addon": { "data": { - "network_key": "Network Key", "s0_legacy_key": "S0 Key (Legacy)", "s2_access_control_key": "S2 Access Control Key", "s2_authenticated_key": "S2 Authenticated Key", @@ -59,6 +58,10 @@ }, "usb_confirm": { "description": "Do you want to setup {name} with the Z-Wave JS add-on?" + }, + "zeroconf_confirm": { + "description": "Do you want to add the Z-Wave JS Server with home ID {home_id} found at {url} to Home Assistant?", + "title": "Discovered Z-Wave JS Server" } } }, @@ -113,7 +116,6 @@ "data": { "emulate_hardware": "Emulate Hardware", "log_level": "Log level", - "network_key": "Network Key", "s0_legacy_key": "S0 Key (Legacy)", "s2_access_control_key": "S2 Access Control Key", "s2_authenticated_key": "S2 Authenticated Key", @@ -142,6 +144,5 @@ "title": "The Z-Wave JS add-on is starting." } } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 9c3776155e1..2a0a4d767a9 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -377,6 +377,11 @@ ZEROCONF = { { "domain": "kodi" } + ], + "_zwave-js-server._tcp.local.": [ + { + "domain": "zwave_js" + } ] } diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 07a3f1305a5..f107a5fd8e2 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -10,6 +10,7 @@ from homeassistant import config_entries from homeassistant.components import usb from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.hassio.handler import HassioAPIError +from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.components.zwave_js.config_flow import SERVER_VERSION_TIMEOUT, TITLE from homeassistant.components.zwave_js.const import DOMAIN @@ -2459,3 +2460,48 @@ async def test_import_addon_installed( } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_zeroconf(hass): + """Test zeroconf discovery.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + host="localhost", + addresses=["127.0.0.1"], + hostname="mock_hostname", + name="mock_name", + port=3000, + type="_zwave-js-server._tcp.local.", + properties={"homeId": "1234"}, + ), + ) + + assert result["type"] == "form" + assert result["step_id"] == "zeroconf_confirm" + + with patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == TITLE + assert result["data"] == { + "url": "ws://localhost:3000", + "usb_path": None, + "s0_legacy_key": None, + "s2_access_control_key": None, + "s2_authenticated_key": None, + "s2_unauthenticated_key": None, + "use_addon": False, + "integration_created_addon": False, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1