Add dhcp support to iSmartGate (#50309)

This commit is contained in:
J. Nick Koston 2021-05-11 17:20:03 -05:00 committed by GitHub
parent 3b272ec54c
commit 7314247ce3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 292 additions and 135 deletions

View File

@ -6,6 +6,8 @@ from ismartgate.common import AbstractInfoResponse, ApiError
from ismartgate.const import GogoGate2ApiErrorCode, ISmartGateApiErrorCode from ismartgate.const import GogoGate2ApiErrorCode, ISmartGateApiErrorCode
import voluptuous as vol import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS
from homeassistant.config_entries import ConfigFlow from homeassistant.config_entries import ConfigFlow
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE, CONF_DEVICE,
@ -17,6 +19,11 @@ from homeassistant.const import (
from .common import get_api from .common import get_api
from .const import DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, DOMAIN from .const import DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, DOMAIN
DEVICE_NAMES = {
DEVICE_TYPE_GOGOGATE2: "Gogogate2",
DEVICE_TYPE_ISMARTGATE: "ismartgate",
}
class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN):
"""Gogogate2 config flow.""" """Gogogate2 config flow."""
@ -31,13 +38,25 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN):
async def async_step_homekit(self, discovery_info): async def async_step_homekit(self, discovery_info):
"""Handle homekit discovery.""" """Handle homekit discovery."""
await self.async_set_unique_id(discovery_info["properties"]["id"]) await self.async_set_unique_id(discovery_info["properties"]["id"])
self._abort_if_unique_id_configured({CONF_IP_ADDRESS: discovery_info["host"]}) return await self._async_discovery_handler(discovery_info["host"])
ip_address = discovery_info["host"] async def async_step_dhcp(self, discovery_info):
"""Handle dhcp discovery."""
await self.async_set_unique_id(discovery_info[MAC_ADDRESS])
return await self._async_discovery_handler(discovery_info[IP_ADDRESS])
async def _async_discovery_handler(self, ip_address):
"""Start the user flow from any discovery."""
self.context[CONF_IP_ADDRESS] = ip_address
self._abort_if_unique_id_configured({CONF_IP_ADDRESS: ip_address})
self._async_abort_entries_match({CONF_IP_ADDRESS: ip_address}) self._async_abort_entries_match({CONF_IP_ADDRESS: ip_address})
self._ip_address = ip_address self._ip_address = ip_address
for progress in self._async_in_progress():
if progress.get("context", {}).get(CONF_IP_ADDRESS) == self._ip_address:
raise data_entry_flow.AbortFlow("already_in_progress")
self._device_type = DEVICE_TYPE_ISMARTGATE self._device_type = DEVICE_TYPE_ISMARTGATE
return await self.async_step_user() return await self.async_step_user()
@ -83,6 +102,11 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN):
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
if self._ip_address and self._device_type:
self.context["title_placeholders"] = {
CONF_DEVICE: DEVICE_NAMES[self._device_type],
CONF_IP_ADDRESS: self._ip_address,
}
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",
data_schema=vol.Schema( data_schema=vol.Schema(

View File

@ -1,6 +1,6 @@
{ {
"domain": "gogogate2", "domain": "gogogate2",
"name": "Gogogate2 and iSmartGate", "name": "Gogogate2 and ismartgate",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/gogogate2", "documentation": "https://www.home-assistant.io/integrations/gogogate2",
"requirements": ["ismartgate==4.0.0"], "requirements": ["ismartgate==4.0.0"],
@ -8,5 +8,10 @@
"homekit": { "homekit": {
"models": ["iSmartGate"] "models": ["iSmartGate"]
}, },
"dhcp": [
{
"hostname": "ismartgate*"
}
],
"iot_class": "local_polling" "iot_class": "local_polling"
} }

View File

@ -1,5 +1,6 @@
{ {
"config": { "config": {
"flow_title": "{device} ({ip_address})",
"error": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
@ -9,7 +10,7 @@
}, },
"step": { "step": {
"user": { "user": {
"title": "Setup GogoGate2 or iSmartGate", "title": "Setup Gogogate2 or ismartgate",
"description": "Provide requisite information below.", "description": "Provide requisite information below.",
"data": { "data": {
"ip_address": "[%key:common::config_flow::data::ip%]", "ip_address": "[%key:common::config_flow::data::ip%]",

View File

@ -7,6 +7,7 @@
"cannot_connect": "Failed to connect", "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication" "invalid_auth": "Invalid authentication"
}, },
"flow_title": "{device} ({ip_address})",
"step": { "step": {
"user": { "user": {
"data": { "data": {
@ -15,7 +16,7 @@
"username": "Username" "username": "Username"
}, },
"description": "Provide requisite information below.", "description": "Provide requisite information below.",
"title": "Setup GogoGate2 or iSmartGate" "title": "Setup Gogogate2 or ismartgate"
} }
} }
} }

View File

@ -76,6 +76,10 @@ DHCP = [
"hostname": "guardian*", "hostname": "guardian*",
"macaddress": "30AEA4*" "macaddress": "30AEA4*"
}, },
{
"domain": "gogogate2",
"hostname": "ismartgate*"
},
{ {
"domain": "hunterdouglas_powerview", "domain": "hunterdouglas_powerview",
"hostname": "hunter*", "hostname": "hunter*",

View File

@ -1 +1,139 @@
"""Tests for the GogoGate2 component.""" """Tests for the GogoGate2 component."""
from ismartgate.common import (
DoorMode,
DoorStatus,
GogoGate2Door,
GogoGate2InfoResponse,
ISmartGateDoor,
ISmartGateInfoResponse,
Network,
Outputs,
Wifi,
)
def _mocked_gogogate_open_door_response():
return GogoGate2InfoResponse(
user="user1",
gogogatename="gogogatename0",
model="gogogate2",
apiversion="",
remoteaccessenabled=False,
remoteaccess="abc123.blah.blah",
firmwareversion="222",
apicode="",
door1=GogoGate2Door(
door_id=1,
permission=True,
name="Door1",
gate=False,
mode=DoorMode.GARAGE,
status=DoorStatus.OPENED,
sensor=True,
sensorid=None,
camera=False,
events=2,
temperature=None,
voltage=40,
),
door2=GogoGate2Door(
door_id=2,
permission=True,
name=None,
gate=True,
mode=DoorMode.GARAGE,
status=DoorStatus.UNDEFINED,
sensor=True,
sensorid=None,
camera=False,
events=0,
temperature=None,
voltage=40,
),
door3=GogoGate2Door(
door_id=3,
permission=True,
name=None,
gate=False,
mode=DoorMode.GARAGE,
status=DoorStatus.UNDEFINED,
sensor=True,
sensorid=None,
camera=False,
events=0,
temperature=None,
voltage=40,
),
outputs=Outputs(output1=True, output2=False, output3=True),
network=Network(ip=""),
wifi=Wifi(SSID="", linkquality="", signal=""),
)
def _mocked_ismartgate_closed_door_response():
return ISmartGateInfoResponse(
user="user1",
ismartgatename="ismartgatename0",
model="ismartgatePRO",
apiversion="",
remoteaccessenabled=False,
remoteaccess="abc321.blah.blah",
firmwareversion="555",
pin=123,
lang="en",
newfirmware=False,
door1=ISmartGateDoor(
door_id=1,
permission=True,
name="Door1",
gate=False,
mode=DoorMode.GARAGE,
status=DoorStatus.CLOSED,
sensor=True,
sensorid=None,
camera=False,
events=2,
temperature=None,
enabled=True,
apicode="apicode0",
customimage=False,
voltage=40,
),
door2=ISmartGateDoor(
door_id=2,
permission=True,
name="Door2",
gate=True,
mode=DoorMode.GARAGE,
status=DoorStatus.CLOSED,
sensor=True,
sensorid=None,
camera=False,
events=2,
temperature=None,
enabled=True,
apicode="apicode0",
customimage=False,
voltage=40,
),
door3=ISmartGateDoor(
door_id=3,
permission=True,
name=None,
gate=False,
mode=DoorMode.GARAGE,
status=DoorStatus.UNDEFINED,
sensor=True,
sensorid=None,
camera=False,
events=0,
temperature=None,
enabled=True,
apicode="apicode0",
customimage=False,
voltage=40,
),
network=Network(ip=""),
wifi=Wifi(SSID="", linkquality="", signal=""),
)

View File

@ -1,7 +1,7 @@
"""Tests for the GogoGate2 component.""" """Tests for the GogoGate2 component."""
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from ismartgate import GogoGate2Api from ismartgate import GogoGate2Api, ISmartGateApi
from ismartgate.common import ApiError from ismartgate.common import ApiError
from ismartgate.const import GogoGate2ApiErrorCode from ismartgate.const import GogoGate2ApiErrorCode
@ -19,7 +19,13 @@ from homeassistant.const import (
CONF_USERNAME, CONF_USERNAME,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
RESULT_TYPE_CREATE_ENTRY,
RESULT_TYPE_FORM,
)
from . import _mocked_ismartgate_closed_door_response
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -75,6 +81,24 @@ async def test_auth_fail(
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {"base": "cannot_connect"} assert result["errors"] == {"base": "cannot_connect"}
api.reset_mock()
api.async_info.side_effect = ApiError(0, "blah")
result = await hass.config_entries.flow.async_init(
"gogogate2", context={"source": SOURCE_USER}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_DEVICE: DEVICE_TYPE_GOGOGATE2,
CONF_IP_ADDRESS: "127.0.0.2",
CONF_USERNAME: "user0",
CONF_PASSWORD: "password0",
},
)
assert result
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {"base": "cannot_connect"}
async def test_form_homekit_unique_id_already_setup(hass): async def test_form_homekit_unique_id_already_setup(hass):
"""Test that we abort from homekit if gogogate2 is already setup.""" """Test that we abort from homekit if gogogate2 is already setup."""
@ -145,3 +169,86 @@ async def test_form_homekit_ip_address(hass):
CONF_PASSWORD: "password", CONF_PASSWORD: "password",
CONF_USERNAME: "username", CONF_USERNAME: "username",
} }
@patch("homeassistant.components.gogogate2.async_setup_entry", return_value=True)
@patch("homeassistant.components.gogogate2.common.ISmartGateApi")
async def test_discovered_dhcp(
ismartgateapi_mock, async_setup_entry_mock, hass
) -> None:
"""Test we get the form with homekit and abort for dhcp source when we get both."""
api: ISmartGateApi = MagicMock(spec=ISmartGateApi)
ismartgateapi_mock.return_value = api
api.reset_mock()
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data={"ip": "1.2.3.4", "macaddress": MOCK_MAC_ADDR},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {}
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_DEVICE: DEVICE_TYPE_ISMARTGATE,
CONF_IP_ADDRESS: "1.2.3.4",
CONF_USERNAME: "user0",
CONF_PASSWORD: "password0",
},
)
assert result2
assert result2["type"] == RESULT_TYPE_FORM
assert result2["errors"] == {"base": "cannot_connect"}
api.reset_mock()
closed_door_response = _mocked_ismartgate_closed_door_response()
api.async_info.return_value = closed_door_response
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
user_input={
CONF_DEVICE: DEVICE_TYPE_ISMARTGATE,
CONF_IP_ADDRESS: "1.2.3.4",
CONF_USERNAME: "user0",
CONF_PASSWORD: "password0",
},
)
assert result3
assert result3["type"] == RESULT_TYPE_CREATE_ENTRY
assert result3["data"] == {
"device": "ismartgate",
"ip_address": "1.2.3.4",
"password": "password0",
"username": "user0",
}
async def test_discovered_by_homekit_and_dhcp(hass):
"""Test we get the form with homekit and abort for dhcp source when we get both."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_HOMEKIT},
data={"host": "1.2.3.4", "properties": {"id": MOCK_MAC_ADDR}},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {}
result2 = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data={"ip": "1.2.3.4", "macaddress": MOCK_MAC_ADDR},
)
assert result2["type"] == RESULT_TYPE_ABORT
assert result2["reason"] == "already_in_progress"
result3 = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data={"ip": "1.2.3.4", "macaddress": "00:00:00:00:00:00"},
)
assert result3["type"] == RESULT_TYPE_ABORT
assert result3["reason"] == "already_in_progress"

View File

@ -9,8 +9,6 @@ from ismartgate.common import (
GogoGate2ActivateResponse, GogoGate2ActivateResponse,
GogoGate2Door, GogoGate2Door,
GogoGate2InfoResponse, GogoGate2InfoResponse,
ISmartGateDoor,
ISmartGateInfoResponse,
Network, Network,
Outputs, Outputs,
TransitionDoorStatus, TransitionDoorStatus,
@ -47,135 +45,14 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from . import (
_mocked_gogogate_open_door_response,
_mocked_ismartgate_closed_door_response,
)
from tests.common import MockConfigEntry, async_fire_time_changed, mock_device_registry from tests.common import MockConfigEntry, async_fire_time_changed, mock_device_registry
def _mocked_gogogate_open_door_response():
return GogoGate2InfoResponse(
user="user1",
gogogatename="gogogatename0",
model="gogogate2",
apiversion="",
remoteaccessenabled=False,
remoteaccess="abc123.blah.blah",
firmwareversion="222",
apicode="",
door1=GogoGate2Door(
door_id=1,
permission=True,
name="Door1",
gate=False,
mode=DoorMode.GARAGE,
status=DoorStatus.OPENED,
sensor=True,
sensorid=None,
camera=False,
events=2,
temperature=None,
voltage=40,
),
door2=GogoGate2Door(
door_id=2,
permission=True,
name=None,
gate=True,
mode=DoorMode.GARAGE,
status=DoorStatus.UNDEFINED,
sensor=True,
sensorid=None,
camera=False,
events=0,
temperature=None,
voltage=40,
),
door3=GogoGate2Door(
door_id=3,
permission=True,
name=None,
gate=False,
mode=DoorMode.GARAGE,
status=DoorStatus.UNDEFINED,
sensor=True,
sensorid=None,
camera=False,
events=0,
temperature=None,
voltage=40,
),
outputs=Outputs(output1=True, output2=False, output3=True),
network=Network(ip=""),
wifi=Wifi(SSID="", linkquality="", signal=""),
)
def _mocked_ismartgate_closed_door_response():
return ISmartGateInfoResponse(
user="user1",
ismartgatename="ismartgatename0",
model="ismartgatePRO",
apiversion="",
remoteaccessenabled=False,
remoteaccess="abc321.blah.blah",
firmwareversion="555",
pin=123,
lang="en",
newfirmware=False,
door1=ISmartGateDoor(
door_id=1,
permission=True,
name="Door1",
gate=False,
mode=DoorMode.GARAGE,
status=DoorStatus.CLOSED,
sensor=True,
sensorid=None,
camera=False,
events=2,
temperature=None,
enabled=True,
apicode="apicode0",
customimage=False,
voltage=40,
),
door2=ISmartGateDoor(
door_id=2,
permission=True,
name="Door2",
gate=True,
mode=DoorMode.GARAGE,
status=DoorStatus.CLOSED,
sensor=True,
sensorid=None,
camera=False,
events=2,
temperature=None,
enabled=True,
apicode="apicode0",
customimage=False,
voltage=40,
),
door3=ISmartGateDoor(
door_id=3,
permission=True,
name=None,
gate=False,
mode=DoorMode.GARAGE,
status=DoorStatus.UNDEFINED,
sensor=True,
sensorid=None,
camera=False,
events=0,
temperature=None,
enabled=True,
apicode="apicode0",
customimage=False,
voltage=40,
),
network=Network(ip=""),
wifi=Wifi(SSID="", linkquality="", signal=""),
)
@patch("homeassistant.components.gogogate2.common.GogoGate2Api") @patch("homeassistant.components.gogogate2.common.GogoGate2Api")
async def test_open_close_update(gogogate2api_mock, hass: HomeAssistant) -> None: async def test_open_close_update(gogogate2api_mock, hass: HomeAssistant) -> None:
"""Test open and close and data update.""" """Test open and close and data update."""