From 388815b81aad32f38f61172ed77626440d72c9b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Mar 2021 08:40:19 -1000 Subject: [PATCH] Add broadlink dhcp discovery (#48408) --- .../components/broadlink/config_flow.py | 26 +++- .../components/broadlink/manifest.json | 8 +- homeassistant/generated/dhcp.py | 16 ++ .../components/broadlink/test_config_flow.py | 143 +++++++++++++++++- 4 files changed, 190 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index 3e21765dbed..b8999fcd5f5 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -13,6 +13,7 @@ from broadlink.exceptions import ( import voluptuous as vol from homeassistant import config_entries, data_entry_flow +from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT, CONF_TYPE from homeassistant.helpers import config_validation as cv @@ -22,7 +23,7 @@ from .const import ( # pylint: disable=unused-import DOMAIN, DOMAINS_AND_TYPES, ) -from .helpers import format_mac +from .helpers import format_mac, mac_address _LOGGER = logging.getLogger(__name__) @@ -59,6 +60,29 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): "host": device.host[0], } + async def async_step_dhcp(self, dhcp_discovery): + """Handle dhcp discovery.""" + host = dhcp_discovery[IP_ADDRESS] + await self.async_set_unique_id( + format_mac(mac_address(dhcp_discovery[MAC_ADDRESS])) + ) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + try: + hello = partial(blk.discover, discover_ip_address=host) + device = (await self.hass.async_add_executor_job(hello))[0] + except IndexError: + return self.async_abort(reason="cannot_connect") + except OSError as err: + if err.errno == errno.ENETUNREACH: + return self.async_abort(reason="cannot_connect") + return self.async_abort(reason="invalid_host") + except Exception as ex: # pylint: disable=broad-except + _LOGGER.error("Failed to connect to the device at %s", host, exc_info=ex) + return self.async_abort(reason="unknown") + + await self.async_set_device(device) + return await self.async_step_auth() + async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" errors = {} diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index 8d3b16b4582..a1437521cb6 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -4,5 +4,11 @@ "documentation": "https://www.home-assistant.io/integrations/broadlink", "requirements": ["broadlink==0.17.0"], "codeowners": ["@danielhiversen", "@felipediel"], - "config_flow": true + "config_flow": true, + "dhcp": [ + {"macaddress": "34EA34*"}, + {"macaddress": "24DFA7*"}, + {"macaddress": "A043B0*"}, + {"macaddress": "B4430D*"} + ] } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index b10893230db..83622545551 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -41,6 +41,22 @@ DHCP = [ "hostname": "blink*", "macaddress": "B85F98*" }, + { + "domain": "broadlink", + "macaddress": "34EA34*" + }, + { + "domain": "broadlink", + "macaddress": "24DFA7*" + }, + { + "domain": "broadlink", + "macaddress": "A043B0*" + }, + { + "domain": "broadlink", + "macaddress": "B4430D*" + }, { "domain": "flume", "hostname": "flume-gw-*", diff --git a/tests/components/broadlink/test_config_flow.py b/tests/components/broadlink/test_config_flow.py index 30f19c178b7..6291c5d258a 100644 --- a/tests/components/broadlink/test_config_flow.py +++ b/tests/components/broadlink/test_config_flow.py @@ -6,8 +6,9 @@ from unittest.mock import call, patch import broadlink.exceptions as blke import pytest -from homeassistant import config_entries +from homeassistant import config_entries, setup from homeassistant.components.broadlink.const import DOMAIN +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from . import get_device @@ -819,3 +820,143 @@ async def test_flow_reauth_valid_host(hass): assert mock_entry.data["host"] == device.host assert mock_discover.call_count == 1 assert mock_api.auth.call_count == 1 + + +async def test_dhcp_can_finish(hass): + """Test DHCP discovery flow can finish right away.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + device = get_device("Living Room") + device.host = "1.2.3.4" + mock_api = device.get_mock_api() + + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "dhcp"}, + data={ + HOSTNAME: "broadlink", + IP_ADDRESS: "1.2.3.4", + MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["step_id"] == "finish" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Living Room" + assert result2["data"] == { + "host": "1.2.3.4", + "mac": "34ea34b43b5a", + "timeout": 10, + "type": 24374, + } + + +async def test_dhcp_fails_to_connect(hass): + """Test DHCP discovery flow that fails to connect.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch(DEVICE_DISCOVERY, side_effect=IndexError()): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "dhcp"}, + data={ + HOSTNAME: "broadlink", + IP_ADDRESS: "1.2.3.4", + MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + + +async def test_dhcp_unreachable(hass): + """Test DHCP discovery flow that fails to connect.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.ENETUNREACH, None)): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "dhcp"}, + data={ + HOSTNAME: "broadlink", + IP_ADDRESS: "1.2.3.4", + MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + + +async def test_dhcp_connect_einval(hass): + """Test DHCP discovery flow that fails to connect with EINVAL.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.EINVAL, None)): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "dhcp"}, + data={ + HOSTNAME: "broadlink", + IP_ADDRESS: "1.2.3.4", + MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "invalid_host" + + +async def test_dhcp_connect_unknown_error(hass): + """Test DHCP discovery flow that fails to connect with an unknown error.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch(DEVICE_DISCOVERY, side_effect=ValueError("Unknown failure")): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "dhcp"}, + data={ + HOSTNAME: "broadlink", + IP_ADDRESS: "1.2.3.4", + MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "unknown" + + +async def test_dhcp_already_exists(hass): + """Test DHCP discovery flow that fails to connect.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + device = get_device("Living Room") + mock_entry = device.get_mock_entry() + mock_entry.add_to_hass(hass) + device.host = "1.2.3.4" + mock_api = device.get_mock_api() + + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "dhcp"}, + data={ + HOSTNAME: "broadlink", + IP_ADDRESS: "1.2.3.4", + MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "already_configured"