Motion Blinds auto interface (#68852)
Co-authored-by: J. Nick Koston <nick@koston.org>pull/69311/head
parent
c6e2bdcea0
commit
4dade9668a
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue