From e287160f72d3f6f16ffa02ba3b158ac595760979 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 29 Dec 2020 20:13:31 +0100 Subject: [PATCH] Add discovery to Motion Blinds (#44615) * Add discovery to Motion Blinds * Update test_config_flow.py * ommit keys() Co-authored-by: Allen Porter * use _ to indicate private variables * disregard changes to en.json * remove unused errors * clearify multicast=None * improve tests * make self._key a local variable * fix styling Co-authored-by: Allen Porter --- .../components/motion_blinds/config_flow.py | 69 +++++-- .../components/motion_blinds/strings.json | 21 ++- .../motion_blinds/test_config_flow.py | 175 +++++++++++++++++- 3 files changed, 244 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/motion_blinds/config_flow.py b/homeassistant/components/motion_blinds/config_flow.py index 497f11760fe..cb85b45e0e0 100644 --- a/homeassistant/components/motion_blinds/config_flow.py +++ b/homeassistant/components/motion_blinds/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure Motion Blinds using their WLAN API.""" import logging +from motionblinds import MotionDiscovery import voluptuous as vol from homeassistant import config_entries @@ -15,7 +16,12 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( { - vol.Required(CONF_HOST): str, + vol.Optional(CONF_HOST): str, + } +) + +CONFIG_SETTINGS = vol.Schema( + { vol.Required(CONF_API_KEY): vol.All(str, vol.Length(min=16, max=16)), } ) @@ -29,35 +35,64 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the Motion Blinds flow.""" - self.host = None - self.key = None + self._host = None + self._ips = [] async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" errors = {} if user_input is not None: - self.host = user_input[CONF_HOST] - self.key = user_input[CONF_API_KEY] - return await self.async_step_connect() + self._host = user_input.get(CONF_HOST) + + if self._host is not None: + return await self.async_step_connect() + + # Use MotionGateway discovery + discover_class = MotionDiscovery() + gateways = await self.hass.async_add_executor_job(discover_class.discover) + self._ips = list(gateways) + + if len(self._ips) == 1: + self._host = self._ips[0] + return await self.async_step_connect() + + if len(self._ips) > 1: + return await self.async_step_select() + + errors["base"] = "discovery_error" return self.async_show_form( step_id="user", data_schema=CONFIG_SCHEMA, errors=errors ) + async def async_step_select(self, user_input=None): + """Handle multiple motion gateways found.""" + if user_input is not None: + self._host = user_input["select_ip"] + return await self.async_step_connect() + + select_schema = vol.Schema({vol.Required("select_ip"): vol.In(self._ips)}) + + return self.async_show_form(step_id="select", data_schema=select_schema) + async def async_step_connect(self, user_input=None): """Connect to the Motion Gateway.""" + if user_input is not None: + key = user_input[CONF_API_KEY] - connect_gateway_class = ConnectMotionGateway(self.hass, None) - if not await connect_gateway_class.async_connect_gateway(self.host, self.key): - return self.async_abort(reason="connection_error") - motion_gateway = connect_gateway_class.gateway_device + connect_gateway_class = ConnectMotionGateway(self.hass, multicast=None) + if not await connect_gateway_class.async_connect_gateway(self._host, key): + return self.async_abort(reason="connection_error") + motion_gateway = connect_gateway_class.gateway_device - mac_address = motion_gateway.mac + mac_address = motion_gateway.mac - await self.async_set_unique_id(mac_address) - self._abort_if_unique_id_configured() + await self.async_set_unique_id(mac_address) + self._abort_if_unique_id_configured() - return self.async_create_entry( - title=DEFAULT_GATEWAY_NAME, - data={CONF_HOST: self.host, CONF_API_KEY: self.key}, - ) + return self.async_create_entry( + title=DEFAULT_GATEWAY_NAME, + data={CONF_HOST: self._host, CONF_API_KEY: key}, + ) + + return self.async_show_form(step_id="connect", data_schema=CONFIG_SETTINGS) diff --git a/homeassistant/components/motion_blinds/strings.json b/homeassistant/components/motion_blinds/strings.json index d9c8a4099ac..d922923d472 100644 --- a/homeassistant/components/motion_blinds/strings.json +++ b/homeassistant/components/motion_blinds/strings.json @@ -3,14 +3,30 @@ "flow_title": "Motion Blinds", "step": { "user": { + "title": "Motion Blinds", + "description": "Connect to your Motion Gateway, if the IP address is not set, auto-discovery is used", + "data": { + "host": "[%key:common::config_flow::data::ip%]" + } + }, + "connect": { "title": "Motion Blinds", "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions", "data": { - "host": "[%key:common::config_flow::data::ip%]", "api_key": "[%key:common::config_flow::data::api_key%]" } + }, + "select": { + "title": "Select the Motion Gateway that you wish to connect", + "description": "Run the setup again if you want to connect additional Motion Gateways", + "data": { + "select_ip": "[%key:common::config_flow::data::ip%]" + } } }, + "error": { + "discovery_error": "Failed to discover a Motion Gateway" + }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", @@ -18,3 +34,6 @@ } } } + + + diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py index 4514beda8c0..4a25026959c 100644 --- a/tests/components/motion_blinds/test_config_flow.py +++ b/tests/components/motion_blinds/test_config_flow.py @@ -11,8 +11,51 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST from tests.async_mock import Mock, patch TEST_HOST = "1.2.3.4" +TEST_HOST2 = "5.6.7.8" TEST_API_KEY = "12ab345c-d67e-8f" -TEST_DEVICE_LIST = {"mac": Mock()} +TEST_MAC = "ab:cd:ef:gh" +TEST_MAC2 = "ij:kl:mn:op" +TEST_DEVICE_LIST = {TEST_MAC: Mock()} + +TEST_DISCOVERY_1 = { + TEST_HOST: { + "msgType": "GetDeviceListAck", + "mac": TEST_MAC, + "deviceType": "02000002", + "ProtocolVersion": "0.9", + "token": "12345A678B9CDEFG", + "data": [ + {"mac": "abcdefghujkl", "deviceType": "02000002"}, + {"mac": "abcdefghujkl0001", "deviceType": "10000000"}, + {"mac": "abcdefghujkl0002", "deviceType": "10000000"}, + ], + } +} + +TEST_DISCOVERY_2 = { + TEST_HOST: { + "msgType": "GetDeviceListAck", + "mac": TEST_MAC, + "deviceType": "02000002", + "ProtocolVersion": "0.9", + "token": "12345A678B9CDEFG", + "data": [ + {"mac": "abcdefghujkl", "deviceType": "02000002"}, + {"mac": "abcdefghujkl0001", "deviceType": "10000000"}, + ], + }, + TEST_HOST2: { + "msgType": "GetDeviceListAck", + "mac": TEST_MAC2, + "deviceType": "02000002", + "ProtocolVersion": "0.9", + "token": "12345A678B9CDEFG", + "data": [ + {"mac": "abcdefghujkl", "deviceType": "02000002"}, + {"mac": "abcdefghujkl0001", "deviceType": "10000000"}, + ], + }, +} @pytest.fixture(name="motion_blinds_connect", autouse=True) @@ -27,6 +70,9 @@ def motion_blinds_connect_fixture(): ), patch( "homeassistant.components.motion_blinds.gateway.MotionGateway.device_list", TEST_DEVICE_LIST, + ), patch( + "homeassistant.components.motion_blinds.config_flow.MotionDiscovery.discover", + return_value=TEST_DISCOVERY_1, ), patch( "homeassistant.components.motion_blinds.async_setup_entry", return_value=True ): @@ -45,7 +91,16 @@ async def test_config_flow_manual_host_success(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: TEST_HOST, CONF_API_KEY: TEST_API_KEY}, + {CONF_HOST: TEST_HOST}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "connect" + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: TEST_API_KEY}, ) assert result["type"] == "create_entry" @@ -56,6 +111,87 @@ async def test_config_flow_manual_host_success(hass): } +async def test_config_flow_discovery_1_success(hass): + """Successful flow with 1 gateway discovered.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "connect" + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: TEST_API_KEY}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == DEFAULT_GATEWAY_NAME + assert result["data"] == { + CONF_HOST: TEST_HOST, + CONF_API_KEY: TEST_API_KEY, + } + + +async def test_config_flow_discovery_2_success(hass): + """Successful flow with 2 gateway discovered.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.motion_blinds.config_flow.MotionDiscovery.discover", + return_value=TEST_DISCOVERY_2, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "select" + assert result["data_schema"].schema["select_ip"].container == [ + TEST_HOST, + TEST_HOST2, + ] + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"select_ip": TEST_HOST2}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "connect" + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: TEST_API_KEY}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == DEFAULT_GATEWAY_NAME + assert result["data"] == { + CONF_HOST: TEST_HOST2, + CONF_API_KEY: TEST_API_KEY, + } + + async def test_config_flow_connection_error(hass): """Failed flow manually initialized by the user with connection timeout.""" result = await hass.config_entries.flow.async_init( @@ -66,14 +202,47 @@ async def test_config_flow_connection_error(hass): assert result["step_id"] == "user" assert result["errors"] == {} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: TEST_HOST}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "connect" + assert result["errors"] is None + with patch( "homeassistant.components.motion_blinds.gateway.MotionGateway.GetDeviceList", side_effect=socket.timeout, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: TEST_HOST, CONF_API_KEY: TEST_API_KEY}, + {CONF_API_KEY: TEST_API_KEY}, ) assert result["type"] == "abort" assert result["reason"] == "connection_error" + + +async def test_config_flow_discovery_fail(hass): + """Failed flow with no gateways discovered.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.motion_blinds.config_flow.MotionDiscovery.discover", + return_value={}, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"base": "discovery_error"}