From 4dc885dcc371d6e4ca231263281641d92ac5395c Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 30 Mar 2021 11:13:26 -0400 Subject: [PATCH] Add discovery for Tube's Zigbee coordinators to ZHA (#48420) * add discovery for tube zigbee gateways * update discovery * add test * another test * develop translations * review comments --- homeassistant/components/zha/config_flow.py | 37 ++++++++++++++ homeassistant/components/zha/manifest.json | 4 +- homeassistant/components/zha/strings.json | 5 +- .../components/zha/translations/en.json | 1 + homeassistant/generated/zeroconf.py | 4 ++ tests/components/zha/test_config_flow.py | 51 +++++++++++++++++++ 6 files changed, 100 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 7994ebb5f25..9c440c29cd3 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -9,6 +9,8 @@ import voluptuous as vol from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.helpers.typing import DiscoveryInfoType from .core.const import ( CONF_BAUDRATE, @@ -91,6 +93,37 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema=vol.Schema(schema), ) + async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): + """Handle zeroconf discovery.""" + # Hostname is format: livingroom.local. + local_name = discovery_info["hostname"][:-1] + node_name = local_name[: -len(".local")] + host = discovery_info[CONF_HOST] + device_path = f"socket://{host}:6638" + + await self.async_set_unique_id(node_name) + self._abort_if_unique_id_configured( + updates={ + CONF_DEVICE: {CONF_DEVICE_PATH: device_path}, + } + ) + + # Check if already configured + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = { + CONF_NAME: node_name, + } + + self._device_path = device_path + self._radio_type = ( + RadioType.ezsp.name if "efr32" in local_name else RadioType.znp.name + ) + + return await self.async_step_port_config() + async def async_step_port_config(self, user_input=None): """Enter port settings specific for this type of radio.""" errors = {} @@ -118,9 +151,13 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if isinstance(radio_schema, vol.Schema): radio_schema = radio_schema.schema + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + source = self.context.get("source") for param, value in radio_schema.items(): if param in SUPPORTED_PORT_SETTINGS: schema[param] = value + if source == config_entries.SOURCE_ZEROCONF and param == CONF_BAUDRATE: + schema[param] = 115200 return self.async_show_form( step_id="port_config", diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index fbe66741bcf..5825bdcda0f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -15,5 +15,7 @@ "zigpy-zigate==0.7.3", "zigpy-znp==0.4.0" ], - "codeowners": ["@dmulcahey", "@adminiuga"] + "codeowners": ["@dmulcahey", "@adminiuga"], + "zeroconf": [{ "type": "_esphomelib._tcp.local.", "name": "tube*" }], + "after_dependencies": ["zeroconf"] } diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 93b5cd7ccf5..550fad3c2c5 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "ZHA: {name}", "step": { "user": { "title": "ZHA", @@ -21,7 +22,9 @@ } } }, - "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index d9c78171d94..d3ed2ddfce4 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Failed to connect" }, + "flow_title": "ZHA: {name}", "step": { "pick_radio": { "data": { diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index d3a976e78ea..a6af4d93fb8 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -57,6 +57,10 @@ ZEROCONF = { "_esphomelib._tcp.local.": [ { "domain": "esphome" + }, + { + "domain": "zha", + "name": "tube*" } ], "_fbx-api._tcp.local.": [ diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index f41c734537b..127c5518a41 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -28,6 +28,57 @@ def com_port(): return port +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) +async def test_discovery(detect_mock, hass): + """Test zeroconf flow -- radio detected.""" + service_info = { + "host": "192.168.1.200", + "port": 6053, + "hostname": "_tube_zb_gw._tcp.local.", + "properties": {"name": "tube_123456"}, + } + flow = await hass.config_entries.flow.async_init( + "zha", context={"source": "zeroconf"}, data=service_info + ) + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], user_input={} + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "socket://192.168.1.200:6638" + assert result["data"] == { + "device": { + "baudrate": 115200, + "flow_control": None, + "path": "socket://192.168.1.200:6638", + }, + CONF_RADIO_TYPE: "znp", + } + + +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) +async def test_discovery_already_setup(detect_mock, hass): + """Test zeroconf flow -- radio detected.""" + service_info = { + "host": "192.168.1.200", + "port": 6053, + "hostname": "_tube_zb_gw._tcp.local.", + "properties": {"name": "tube_123456"}, + } + await setup.async_setup_component(hass, "persistent_notification", {}) + MockConfigEntry(domain=DOMAIN, data={"usb_path": "/dev/ttyUSB1"}).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + "zha", context={"source": "zeroconf"}, data=service_info + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "single_instance_allowed" + + @patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) @patch( "homeassistant.components.zha.config_flow.detect_radios",