Add discovery to Motion Blinds (#44615)

* Add discovery to Motion Blinds

* Update test_config_flow.py

* ommit keys()

Co-authored-by: Allen Porter <allen.porter@gmail.com>

* 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 <allen.porter@gmail.com>
This commit is contained in:
starkillerOG 2020-12-29 20:13:31 +01:00 committed by GitHub
parent 85d89c16ab
commit e287160f72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 244 additions and 21 deletions

View File

@ -1,6 +1,7 @@
"""Config flow to configure Motion Blinds using their WLAN API.""" """Config flow to configure Motion Blinds using their WLAN API."""
import logging import logging
from motionblinds import MotionDiscovery
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
@ -15,7 +16,12 @@ _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema( 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)), 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): def __init__(self):
"""Initialize the Motion Blinds flow.""" """Initialize the Motion Blinds flow."""
self.host = None self._host = None
self.key = None self._ips = []
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
errors = {} errors = {}
if user_input is not None: if user_input is not None:
self.host = user_input[CONF_HOST] self._host = user_input.get(CONF_HOST)
self.key = user_input[CONF_API_KEY]
return await self.async_step_connect() 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( return self.async_show_form(
step_id="user", data_schema=CONFIG_SCHEMA, errors=errors 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): async def async_step_connect(self, user_input=None):
"""Connect to the Motion Gateway.""" """Connect to the Motion Gateway."""
if user_input is not None:
key = user_input[CONF_API_KEY]
connect_gateway_class = ConnectMotionGateway(self.hass, None) connect_gateway_class = ConnectMotionGateway(self.hass, multicast=None)
if not await connect_gateway_class.async_connect_gateway(self.host, self.key): if not await connect_gateway_class.async_connect_gateway(self._host, key):
return self.async_abort(reason="connection_error") return self.async_abort(reason="connection_error")
motion_gateway = connect_gateway_class.gateway_device 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) await self.async_set_unique_id(mac_address)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return self.async_create_entry( return self.async_create_entry(
title=DEFAULT_GATEWAY_NAME, title=DEFAULT_GATEWAY_NAME,
data={CONF_HOST: self.host, CONF_API_KEY: self.key}, data={CONF_HOST: self._host, CONF_API_KEY: key},
) )
return self.async_show_form(step_id="connect", data_schema=CONFIG_SETTINGS)

View File

@ -3,14 +3,30 @@
"flow_title": "Motion Blinds", "flow_title": "Motion Blinds",
"step": { "step": {
"user": { "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", "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", "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions",
"data": { "data": {
"host": "[%key:common::config_flow::data::ip%]",
"api_key": "[%key:common::config_flow::data::api_key%]" "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": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
@ -18,3 +34,6 @@
} }
} }
} }

View File

@ -11,8 +11,51 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST
from tests.async_mock import Mock, patch from tests.async_mock import Mock, patch
TEST_HOST = "1.2.3.4" TEST_HOST = "1.2.3.4"
TEST_HOST2 = "5.6.7.8"
TEST_API_KEY = "12ab345c-d67e-8f" 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) @pytest.fixture(name="motion_blinds_connect", autouse=True)
@ -27,6 +70,9 @@ def motion_blinds_connect_fixture():
), patch( ), patch(
"homeassistant.components.motion_blinds.gateway.MotionGateway.device_list", "homeassistant.components.motion_blinds.gateway.MotionGateway.device_list",
TEST_DEVICE_LIST, TEST_DEVICE_LIST,
), patch(
"homeassistant.components.motion_blinds.config_flow.MotionDiscovery.discover",
return_value=TEST_DISCOVERY_1,
), patch( ), patch(
"homeassistant.components.motion_blinds.async_setup_entry", return_value=True "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 = await hass.config_entries.flow.async_configure(
result["flow_id"], 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" 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): async def test_config_flow_connection_error(hass):
"""Failed flow manually initialized by the user with connection timeout.""" """Failed flow manually initialized by the user with connection timeout."""
result = await hass.config_entries.flow.async_init( 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["step_id"] == "user"
assert result["errors"] == {} 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( with patch(
"homeassistant.components.motion_blinds.gateway.MotionGateway.GetDeviceList", "homeassistant.components.motion_blinds.gateway.MotionGateway.GetDeviceList",
side_effect=socket.timeout, side_effect=socket.timeout,
): ):
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], 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["type"] == "abort"
assert result["reason"] == "connection_error" 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"}