Motion Blinds auto interface (#68852)

Co-authored-by: J. Nick Koston <nick@koston.org>
pull/69311/head
starkillerOG 2022-04-04 22:55:36 +02:00 committed by GitHub
parent c6e2bdcea0
commit 4dade9668a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 151 additions and 114 deletions

View File

@ -113,9 +113,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.async_on_unload(entry.add_update_listener(update_listener))
# check multicast interface
check_multicast_class = ConnectMotionGateway(hass, interface=multicast_interface)
working_interface = await check_multicast_class.async_check_interface(host, key)
if working_interface != multicast_interface:
data = {**entry.data, CONF_INTERFACE: working_interface}
hass.config_entries.async_update_entry(entry, data=data)
_LOGGER.debug(
"Motion Blinds interface updated from %s to %s, "
"this should only occur after a network change",
multicast_interface,
working_interface,
)
# Create multicast Listener
if KEY_MULTICAST_LISTENER not in hass.data[DOMAIN]:
multicast = AsyncMotionMulticast(interface=multicast_interface)
multicast = AsyncMotionMulticast(interface=working_interface)
hass.data[DOMAIN][KEY_MULTICAST_LISTENER] = multicast
# start listening for local pushes (only once)
await multicast.Start_listen()

View File

@ -1,11 +1,9 @@
"""Config flow to configure Motion Blinds using their WLAN API."""
from socket import gaierror
from motionblinds import AsyncMotionMulticast, MotionDiscovery
from motionblinds import MotionDiscovery
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import dhcp, network
from homeassistant.components import dhcp
from homeassistant.const import CONF_API_KEY, CONF_HOST
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
@ -131,27 +129,20 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors = {}
if user_input is not None:
key = user_input[CONF_API_KEY]
multicast_interface = user_input[CONF_INTERFACE]
# check socket interface
if multicast_interface != DEFAULT_INTERFACE:
motion_multicast = AsyncMotionMulticast(interface=multicast_interface)
try:
await motion_multicast.Start_listen()
motion_multicast.Stop_listen()
except gaierror:
errors[CONF_INTERFACE] = "invalid_interface"
return self.async_show_form(
step_id="connect",
data_schema=self._config_settings,
errors=errors,
)
connect_gateway_class = ConnectMotionGateway(self.hass, multicast=None)
connect_gateway_class = ConnectMotionGateway(self.hass)
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
# check socket interface
check_multicast_class = ConnectMotionGateway(
self.hass, interface=DEFAULT_INTERFACE
)
multicast_interface = await check_multicast_class.async_check_interface(
self._host, key
)
mac_address = motion_gateway.mac
await self.async_set_unique_id(mac_address, raise_on_progress=False)
@ -172,38 +163,12 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
},
)
(interfaces, default_interface) = await self.async_get_interfaces()
self._config_settings = vol.Schema(
{
vol.Required(CONF_API_KEY): vol.All(str, vol.Length(min=16, max=16)),
vol.Optional(CONF_INTERFACE, default=default_interface): vol.In(
interfaces
),
}
)
return self.async_show_form(
step_id="connect", data_schema=self._config_settings, errors=errors
)
async def async_get_interfaces(self):
"""Get list of interface to use."""
interfaces = [DEFAULT_INTERFACE, "0.0.0.0"]
enabled_interfaces = []
default_interface = DEFAULT_INTERFACE
adapters = await network.async_get_adapters(self.hass)
for adapter in adapters:
if ipv4s := adapter["ipv4"]:
ip4 = ipv4s[0]["address"]
interfaces.append(ip4)
if adapter["enabled"]:
enabled_interfaces.append(ip4)
if adapter["default"]:
default_interface = ip4
if len(enabled_interfaces) == 1:
default_interface = enabled_interfaces[0]
return (interfaces, default_interface)

View File

@ -1,8 +1,13 @@
"""Code to handle a Motion Gateway."""
import contextlib
import logging
import socket
from motionblinds import MotionGateway
from motionblinds import AsyncMotionMulticast, MotionGateway
from homeassistant.components import network
from .const import DEFAULT_INTERFACE
_LOGGER = logging.getLogger(__name__)
@ -10,11 +15,12 @@ _LOGGER = logging.getLogger(__name__)
class ConnectMotionGateway:
"""Class to async connect to a Motion Gateway."""
def __init__(self, hass, multicast):
def __init__(self, hass, multicast=None, interface=None):
"""Initialize the entity."""
self._hass = hass
self._multicast = multicast
self._gateway_device = None
self._interface = interface
@property
def gateway_device(self):
@ -48,3 +54,78 @@ class ConnectMotionGateway:
self.gateway_device.protocol,
)
return True
def check_interface(self):
"""Check if the current interface supports multicast."""
with contextlib.suppress(socket.timeout):
return self.gateway_device.Check_gateway_multicast()
return False
async def async_get_interfaces(self):
"""Get list of interface to use."""
interfaces = [DEFAULT_INTERFACE, "0.0.0.0"]
enabled_interfaces = []
default_interface = DEFAULT_INTERFACE
adapters = await network.async_get_adapters(self._hass)
for adapter in adapters:
if ipv4s := adapter["ipv4"]:
ip4 = ipv4s[0]["address"]
interfaces.append(ip4)
if adapter["enabled"]:
enabled_interfaces.append(ip4)
if adapter["default"]:
default_interface = ip4
if len(enabled_interfaces) == 1:
default_interface = enabled_interfaces[0]
interfaces.remove(default_interface)
interfaces.insert(0, default_interface)
if self._interface is not None:
interfaces.remove(self._interface)
interfaces.insert(0, self._interface)
return interfaces
async def async_check_interface(self, host, key):
"""Connect to the Motion Gateway."""
interfaces = await self.async_get_interfaces()
for interface in interfaces:
_LOGGER.debug(
"Checking Motion Blinds interface '%s' with host %s", interface, host
)
# initialize multicast listener
check_multicast = AsyncMotionMulticast(interface=interface)
try:
await check_multicast.Start_listen()
except socket.gaierror:
continue
# trigger test multicast
self._gateway_device = MotionGateway(
ip=host, key=key, multicast=check_multicast
)
result = await self._hass.async_add_executor_job(self.check_interface)
# close multicast listener again
try:
check_multicast.Stop_listen()
except socket.gaierror:
continue
if result:
# successfully received multicast
_LOGGER.debug(
"Success using Motion Blinds interface '%s' with host %s",
interface,
host,
)
return interface
_LOGGER.error(
"Could not find working interface for Motion Blinds host %s, using interface '%s'",
host,
self._interface,
)
return self._interface

View File

@ -3,7 +3,7 @@
"name": "Motion Blinds",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
"requirements": ["motionblinds==0.6.2"],
"requirements": ["motionblinds==0.6.4"],
"dependencies": ["network"],
"dhcp": [
{ "registered_devices": true },

View File

@ -11,8 +11,7 @@
"connect": {
"description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions",
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
"interface": "The network interface to use"
"api_key": "[%key:common::config_flow::data::api_key%]"
}
},
"select": {
@ -24,8 +23,7 @@
}
},
"error": {
"discovery_error": "Failed to discover a Motion Gateway",
"invalid_interface": "Invalid network interface"
"discovery_error": "Failed to discover a Motion Gateway"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%], connection settings are updated",

View File

@ -6,15 +6,13 @@
"connection_error": "Failed to connect"
},
"error": {
"discovery_error": "Failed to discover a Motion Gateway",
"invalid_interface": "Invalid network interface"
"discovery_error": "Failed to discover a Motion Gateway"
},
"flow_title": "{short_mac} ({ip_address})",
"step": {
"connect": {
"data": {
"api_key": "API Key",
"interface": "The network interface to use"
"api_key": "API Key"
},
"description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions",
"title": "Motion Blinds"

View File

@ -1020,7 +1020,7 @@ mitemp_bt==0.0.5
moehlenhoff-alpha2==1.1.2
# homeassistant.components.motion_blinds
motionblinds==0.6.2
motionblinds==0.6.4
# homeassistant.components.motioneye
motioneye-client==0.3.12

View File

@ -688,7 +688,7 @@ minio==5.0.10
moehlenhoff-alpha2==1.1.2
# homeassistant.components.motion_blinds
motionblinds==0.6.2
motionblinds==0.6.4
# homeassistant.components.motioneye
motioneye-client==0.3.12

View File

@ -76,6 +76,9 @@ def motion_blinds_connect_fixture(mock_get_source_ip):
), patch(
"homeassistant.components.motion_blinds.gateway.MotionGateway.Update",
return_value=True,
), patch(
"homeassistant.components.motion_blinds.gateway.MotionGateway.Check_gateway_multicast",
return_value=True,
), patch(
"homeassistant.components.motion_blinds.gateway.MotionGateway.device_list",
TEST_DEVICE_LIST,
@ -86,13 +89,13 @@ def motion_blinds_connect_fixture(mock_get_source_ip):
"homeassistant.components.motion_blinds.config_flow.MotionDiscovery.discover",
return_value=TEST_DISCOVERY_1,
), patch(
"homeassistant.components.motion_blinds.config_flow.AsyncMotionMulticast.Start_listen",
"homeassistant.components.motion_blinds.gateway.AsyncMotionMulticast.Start_listen",
return_value=True,
), patch(
"homeassistant.components.motion_blinds.config_flow.AsyncMotionMulticast.Stop_listen",
"homeassistant.components.motion_blinds.gateway.AsyncMotionMulticast.Stop_listen",
return_value=True,
), patch(
"homeassistant.components.motion_blinds.config_flow.network.async_get_adapters",
"homeassistant.components.motion_blinds.gateway.network.async_get_adapters",
return_value=TEST_INTERFACES,
), patch(
"homeassistant.components.motion_blinds.async_setup_entry", return_value=True
@ -129,7 +132,7 @@ async def test_config_flow_manual_host_success(hass):
assert result["data"] == {
CONF_HOST: TEST_HOST,
CONF_API_KEY: TEST_API_KEY,
const.CONF_INTERFACE: TEST_HOST_HA,
const.CONF_INTERFACE: TEST_HOST_ANY,
}
@ -152,17 +155,21 @@ async def test_config_flow_discovery_1_success(hass):
assert result["step_id"] == "connect"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_API_KEY: TEST_API_KEY},
)
with patch(
"homeassistant.components.motion_blinds.gateway.AsyncMotionMulticast.Stop_listen",
side_effect=socket.gaierror,
):
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,
const.CONF_INTERFACE: TEST_HOST_HA,
const.CONF_INTERFACE: TEST_HOST_ANY,
}
@ -202,17 +209,21 @@ async def test_config_flow_discovery_2_success(hass):
assert result["step_id"] == "connect"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_API_KEY: TEST_API_KEY},
)
with patch(
"homeassistant.components.motion_blinds.gateway.MotionGateway.Check_gateway_multicast",
side_effect=socket.timeout,
):
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,
const.CONF_INTERFACE: TEST_HOST_HA,
const.CONF_INTERFACE: TEST_HOST_ANY,
}
@ -272,39 +283,6 @@ async def test_config_flow_discovery_fail(hass):
assert result["errors"] == {"base": "discovery_error"}
async def test_config_flow_interface(hass):
"""Successful flow manually initialized by the user with interface specified."""
result = await hass.config_entries.flow.async_init(
const.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"],
{CONF_HOST: TEST_HOST},
)
assert result["type"] == "form"
assert result["step_id"] == "connect"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_API_KEY: TEST_API_KEY, const.CONF_INTERFACE: TEST_HOST_HA},
)
assert result["type"] == "create_entry"
assert result["title"] == DEFAULT_GATEWAY_NAME
assert result["data"] == {
CONF_HOST: TEST_HOST,
CONF_API_KEY: TEST_API_KEY,
const.CONF_INTERFACE: TEST_HOST_HA,
}
async def test_config_flow_invalid_interface(hass):
"""Failed flow manually initialized by the user with invalid interface."""
result = await hass.config_entries.flow.async_init(
@ -325,17 +303,21 @@ async def test_config_flow_invalid_interface(hass):
assert result["errors"] == {}
with patch(
"homeassistant.components.motion_blinds.config_flow.AsyncMotionMulticast.Start_listen",
"homeassistant.components.motion_blinds.gateway.AsyncMotionMulticast.Start_listen",
side_effect=socket.gaierror,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_API_KEY: TEST_API_KEY, const.CONF_INTERFACE: TEST_HOST_HA},
{CONF_API_KEY: TEST_API_KEY},
)
assert result["type"] == "form"
assert result["step_id"] == "connect"
assert result["errors"] == {const.CONF_INTERFACE: "invalid_interface"}
assert result["type"] == "create_entry"
assert result["title"] == DEFAULT_GATEWAY_NAME
assert result["data"] == {
CONF_HOST: TEST_HOST,
CONF_API_KEY: TEST_API_KEY,
const.CONF_INTERFACE: TEST_HOST_ANY,
}
async def test_dhcp_flow(hass):
@ -364,7 +346,7 @@ async def test_dhcp_flow(hass):
assert result["data"] == {
CONF_HOST: TEST_HOST,
CONF_API_KEY: TEST_API_KEY,
const.CONF_INTERFACE: TEST_HOST_HA,
const.CONF_INTERFACE: TEST_HOST_ANY,
}
@ -433,7 +415,7 @@ async def test_change_connection_settings(hass):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_API_KEY: TEST_API_KEY2, const.CONF_INTERFACE: TEST_HOST_ANY},
{CONF_API_KEY: TEST_API_KEY2},
)
assert result["type"] == "abort"