From 7314247ce397492f06fca45d7f1f5c58406e0b10 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 May 2021 17:20:03 -0500 Subject: [PATCH] Add dhcp support to iSmartGate (#50309) --- .../components/gogogate2/config_flow.py | 28 +++- .../components/gogogate2/manifest.json | 7 +- .../components/gogogate2/strings.json | 3 +- .../components/gogogate2/translations/en.json | 3 +- homeassistant/generated/dhcp.py | 4 + tests/components/gogogate2/__init__.py | 138 ++++++++++++++++++ .../components/gogogate2/test_config_flow.py | 111 +++++++++++++- tests/components/gogogate2/test_cover.py | 133 +---------------- 8 files changed, 292 insertions(+), 135 deletions(-) diff --git a/homeassistant/components/gogogate2/config_flow.py b/homeassistant/components/gogogate2/config_flow.py index b70a6120153..6fd61b79795 100644 --- a/homeassistant/components/gogogate2/config_flow.py +++ b/homeassistant/components/gogogate2/config_flow.py @@ -6,6 +6,8 @@ from ismartgate.common import AbstractInfoResponse, ApiError from ismartgate.const import GogoGate2ApiErrorCode, ISmartGateApiErrorCode 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.const import ( CONF_DEVICE, @@ -17,6 +19,11 @@ from homeassistant.const import ( from .common import get_api 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): """Gogogate2 config flow.""" @@ -31,13 +38,25 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_homekit(self, discovery_info): """Handle homekit discovery.""" 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._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 return await self.async_step_user() @@ -83,6 +102,11 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): except Exception: # pylint: disable=broad-except 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( step_id="user", data_schema=vol.Schema( diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index a4c07fa1fb8..94a57c47be7 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -1,6 +1,6 @@ { "domain": "gogogate2", - "name": "Gogogate2 and iSmartGate", + "name": "Gogogate2 and ismartgate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gogogate2", "requirements": ["ismartgate==4.0.0"], @@ -8,5 +8,10 @@ "homekit": { "models": ["iSmartGate"] }, + "dhcp": [ + { + "hostname": "ismartgate*" + } + ], "iot_class": "local_polling" } diff --git a/homeassistant/components/gogogate2/strings.json b/homeassistant/components/gogogate2/strings.json index f5385ff5d54..7c165f90d06 100644 --- a/homeassistant/components/gogogate2/strings.json +++ b/homeassistant/components/gogogate2/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{device} ({ip_address})", "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" @@ -9,7 +10,7 @@ }, "step": { "user": { - "title": "Setup GogoGate2 or iSmartGate", + "title": "Setup Gogogate2 or ismartgate", "description": "Provide requisite information below.", "data": { "ip_address": "[%key:common::config_flow::data::ip%]", diff --git a/homeassistant/components/gogogate2/translations/en.json b/homeassistant/components/gogogate2/translations/en.json index b39bdfd7bb7..53e578526b2 100644 --- a/homeassistant/components/gogogate2/translations/en.json +++ b/homeassistant/components/gogogate2/translations/en.json @@ -7,6 +7,7 @@ "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication" }, + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { @@ -15,7 +16,7 @@ "username": "Username" }, "description": "Provide requisite information below.", - "title": "Setup GogoGate2 or iSmartGate" + "title": "Setup Gogogate2 or ismartgate" } } } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index a4436a1cebe..74287c3a9e4 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -76,6 +76,10 @@ DHCP = [ "hostname": "guardian*", "macaddress": "30AEA4*" }, + { + "domain": "gogogate2", + "hostname": "ismartgate*" + }, { "domain": "hunterdouglas_powerview", "hostname": "hunter*", diff --git a/tests/components/gogogate2/__init__.py b/tests/components/gogogate2/__init__.py index bc867ab646b..f7e3d40a44b 100644 --- a/tests/components/gogogate2/__init__.py +++ b/tests/components/gogogate2/__init__.py @@ -1 +1,139 @@ """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=""), + ) diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index 3cc70ddf7ab..0722874e9e5 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -1,7 +1,7 @@ """Tests for the GogoGate2 component.""" from unittest.mock import MagicMock, patch -from ismartgate import GogoGate2Api +from ismartgate import GogoGate2Api, ISmartGateApi from ismartgate.common import ApiError from ismartgate.const import GogoGate2ApiErrorCode @@ -19,7 +19,13 @@ from homeassistant.const import ( CONF_USERNAME, ) 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 @@ -75,6 +81,24 @@ async def test_auth_fail( assert result["type"] == RESULT_TYPE_FORM 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): """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_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" diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py index 5391bf1e4aa..d3507283426 100644 --- a/tests/components/gogogate2/test_cover.py +++ b/tests/components/gogogate2/test_cover.py @@ -9,8 +9,6 @@ from ismartgate.common import ( GogoGate2ActivateResponse, GogoGate2Door, GogoGate2InfoResponse, - ISmartGateDoor, - ISmartGateInfoResponse, Network, Outputs, TransitionDoorStatus, @@ -47,135 +45,14 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant 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 -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") async def test_open_close_update(gogogate2api_mock, hass: HomeAssistant) -> None: """Test open and close and data update."""