From 1fe70f438d4e57587e18966e3583c912098b6dd1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 20 May 2021 16:51:18 +0200 Subject: [PATCH 1/7] Add Supervisor discovery to motionEye --- .../components/motioneye/config_flow.py | 41 +++++++++++++-- .../components/motioneye/strings.json | 4 ++ .../components/motioneye/translations/en.json | 4 ++ .../components/motioneye/test_config_flow.py | 52 +++++++++++++++++++ 4 files changed, 97 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index a8562189d1f40..59f2a06923634 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -32,6 +32,7 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for motionEye.""" VERSION = 1 + _hassio_discovery: dict[str, Any] | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -42,13 +43,18 @@ def _get_form( user_input: dict[str, Any], errors: dict[str, str] | None = None ) -> FlowResult: """Show the form to the user.""" + url_schema: dict[vol.Required, type[str]] = {} + if not self._hassio_discovery: + # Only ask for URL when not discovered + url_schema = { + vol.Required(CONF_URL, default=user_input.get(CONF_URL, "")): str + } + return self.async_show_form( step_id="user", data_schema=vol.Schema( { - vol.Required( - CONF_URL, default=user_input.get(CONF_URL, "") - ): str, + **url_schema, vol.Optional( CONF_ADMIN_USERNAME, default=user_input.get(CONF_ADMIN_USERNAME), @@ -81,6 +87,10 @@ def _get_form( cast(Dict[str, Any], reauth_entry.data) if reauth_entry else {} ) + if self._hassio_discovery: + # In case of Supervisor discovery, use pushed URL + user_input[CONF_URL] = self._hassio_discovery[CONF_URL] + try: # Cannot use cv.url validation in the schema itself, so # apply extra validation here. @@ -123,8 +133,12 @@ def _get_form( # at least prevent entries with the same motionEye URL. self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]}) + title = user_input[CONF_URL] + if self._hassio_discovery: + title = self._hassio_discovery["addon"] + return self.async_create_entry( - title=f"{user_input[CONF_URL]}", + title=title, data=user_input, ) @@ -134,3 +148,22 @@ async def async_step_reauth( ) -> FlowResult: """Handle a reauthentication flow.""" return await self.async_step_user(config_data) + + async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResult: + """Handle Supervisor discovery.""" + self._hassio_discovery = discovery_info + await self._async_handle_discovery_without_unique_id() + + return await self.async_step_hassio_confirm() + + async def async_step_hassio_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm Supervisor discovery.""" + if user_input is None and self._hassio_discovery is not None: + return self.async_show_form( + step_id="hassio_confirm", + description_placeholders={"addon": self._hassio_discovery["addon"]}, + ) + + return await self.async_step_user() diff --git a/homeassistant/components/motioneye/strings.json b/homeassistant/components/motioneye/strings.json index d365ba272ea43..d89b5cab275fe 100644 --- a/homeassistant/components/motioneye/strings.json +++ b/homeassistant/components/motioneye/strings.json @@ -9,6 +9,10 @@ "surveillance_username": "Surveillance [%key:common::config_flow::data::username%]", "surveillance_password": "Surveillance [%key:common::config_flow::data::password%]" } + }, + "hassio_confirm": { + "title": "motionEye via Home Assistant add-on", + "description": "Do you want to configure Home Assistant to connect to the motionEye service provided by the add-on: {addon}?" } }, "error": { diff --git a/homeassistant/components/motioneye/translations/en.json b/homeassistant/components/motioneye/translations/en.json index dd4f337e9f974..b93e4f668940a 100644 --- a/homeassistant/components/motioneye/translations/en.json +++ b/homeassistant/components/motioneye/translations/en.json @@ -11,6 +11,10 @@ "unknown": "Unexpected error" }, "step": { + "hassio_confirm": { + "description": "Do you want to configure Home Assistant to connect to the motionEye service provided by the add-on: {addon}?", + "title": "motionEye via Home Assistant add-on" + }, "user": { "data": { "admin_password": "Admin Password", diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index f193c79f3ef11..63f02088b6571 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -69,6 +69,58 @@ async def test_user_success(hass: HomeAssistant) -> None: assert mock_client.async_client_close.called +async def test_hassio_success(hass: HomeAssistant) -> None: + """Test successful Supervisor flow.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"addon": "motionEye Add-on", "url": TEST_URL}, + context={"source": config_entries.SOURCE_HASSIO}, + ) + + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("step_id") == "hassio_confirm" + assert result.get("description_placeholders") == {"addon": "motionEye Add-on"} + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result2.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result2.get("step_id") == "user" + assert "flow_id" in result2 + + mock_client = create_mock_motioneye_client() + + with patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=mock_client, + ), patch( + "homeassistant.components.motioneye.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ADMIN_USERNAME: "admin-username", + CONF_ADMIN_PASSWORD: "admin-password", + CONF_SURVEILLANCE_USERNAME: "surveillance-username", + CONF_SURVEILLANCE_PASSWORD: "surveillance-password", + }, + ) + await hass.async_block_till_done() + + assert result3.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3.get("title") == "motionEye Add-on" + assert result3.get("data") == { + CONF_URL: TEST_URL, + CONF_ADMIN_USERNAME: "admin-username", + CONF_ADMIN_PASSWORD: "admin-password", + CONF_SURVEILLANCE_USERNAME: "surveillance-username", + CONF_SURVEILLANCE_PASSWORD: "surveillance-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + assert mock_client.async_client_close.called + + async def test_user_invalid_auth(hass: HomeAssistant) -> None: """Test invalid auth is handled correctly.""" result = await hass.config_entries.flow.async_init( From 124616b5ff0370015cc0c1c8211ef4c601978f8a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 20 May 2021 16:58:01 +0200 Subject: [PATCH 2/7] Add bad tests --- .../components/motioneye/test_config_flow.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index 63f02088b6571..1aad5835b8a3f 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -339,3 +339,34 @@ async def test_duplicate(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" assert mock_client.async_client_close.called + + +async def test_hassio_already_configured(hass: HomeAssistant) -> None: + """Test we don't discover when already configured.""" + MockConfigEntry( + domain=DOMAIN, + data={CONF_URL: TEST_URL}, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"addon": "motionEye Add-on", "url": TEST_URL}, + context={"source": config_entries.SOURCE_HASSIO}, + ) + assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" + + +async def test_hassio_ignored(hass: HomeAssistant) -> None: + """Test Supervisor discovered instance can be ignored.""" + MockConfigEntry(domain=DOMAIN, source=config_entries.SOURCE_IGNORE).add_to_hass( + hass + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"addon": "motionEye Add-on", "url": TEST_URL}, + context={"source": config_entries.SOURCE_HASSIO}, + ) + assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" From 783635ffeb36bdfdf1ab91a92310ee406d503e8d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 22 May 2021 18:32:37 +0200 Subject: [PATCH 3/7] Adjust tests with actual add-on title --- tests/components/motioneye/test_config_flow.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index 1aad5835b8a3f..b9c891290b825 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -74,13 +74,13 @@ async def test_hassio_success(hass: HomeAssistant) -> None: await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, - data={"addon": "motionEye Add-on", "url": TEST_URL}, + data={"addon": "motionEye", "url": TEST_URL}, context={"source": config_entries.SOURCE_HASSIO}, ) assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM assert result.get("step_id") == "hassio_confirm" - assert result.get("description_placeholders") == {"addon": "motionEye Add-on"} + assert result.get("description_placeholders") == {"addon": "motionEye"} assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -109,7 +109,7 @@ async def test_hassio_success(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result3.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result3.get("title") == "motionEye Add-on" + assert result3.get("title") == "motionEye" assert result3.get("data") == { CONF_URL: TEST_URL, CONF_ADMIN_USERNAME: "admin-username", @@ -350,7 +350,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, - data={"addon": "motionEye Add-on", "url": TEST_URL}, + data={"addon": "motionEye", "url": TEST_URL}, context={"source": config_entries.SOURCE_HASSIO}, ) assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT @@ -365,7 +365,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, - data={"addon": "motionEye Add-on", "url": TEST_URL}, + data={"addon": "motionEye", "url": TEST_URL}, context={"source": config_entries.SOURCE_HASSIO}, ) assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT From 7835c21c73eedd0a00361b97176eeaacbcf1e904 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 22 May 2021 20:43:37 +0200 Subject: [PATCH 4/7] Change config entry title --- homeassistant/components/motioneye/config_flow.py | 2 +- tests/components/motioneye/test_config_flow.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index 59f2a06923634..89be966c67072 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -135,7 +135,7 @@ def _get_form( title = user_input[CONF_URL] if self._hassio_discovery: - title = self._hassio_discovery["addon"] + title = "Add-on" return self.async_create_entry( title=title, diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index b9c891290b825..edbedeb2d9991 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -109,7 +109,7 @@ async def test_hassio_success(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result3.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result3.get("title") == "motionEye" + assert result3.get("title") == "Add-on" assert result3.get("data") == { CONF_URL: TEST_URL, CONF_ADMIN_USERNAME: "admin-username", From ff69d8f646697adf18479d43a4344ee0823cd781 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 May 2021 16:08:47 +0200 Subject: [PATCH 5/7] Expand discovery tests --- .../components/motioneye/test_config_flow.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index edbedeb2d9991..fbdabdadb41d9 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -370,3 +370,64 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: ) assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT assert result.get("reason") == "already_configured" + + +async def test_hassio_abort_if_already_in_progress(hass: HomeAssistant) -> None: + """Test Supervisor discovered flow aborts if user flow in progress.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + data={"addon": "motionEye", "url": TEST_URL}, + context={"source": config_entries.SOURCE_HASSIO}, + ) + assert result2.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result2.get("reason") == "already_in_progress" + + +async def test_hassio_clean_up_on_user_flow(hass: HomeAssistant) -> None: + """Test Supervisor discovered flow is clean up when doing user flow.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"addon": "motionEye", "url": TEST_URL}, + context={"source": config_entries.SOURCE_HASSIO}, + ) + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + + result2 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result2.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert "flow_id" in result2 + + mock_client = create_mock_motioneye_client() + + with patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=mock_client, + ), patch( + "homeassistant.components.motioneye.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_URL: TEST_URL, + CONF_ADMIN_USERNAME: "admin-username", + CONF_ADMIN_PASSWORD: "admin-password", + CONF_SURVEILLANCE_USERNAME: "surveillance-username", + CONF_SURVEILLANCE_PASSWORD: "surveillance-password", + }, + ) + await hass.async_block_till_done() + + assert result3.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert len(mock_setup_entry.mock_calls) == 1 + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 0 From d667b4251c83539a40b2e35261925bac63e7e162 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 May 2021 16:17:46 +0200 Subject: [PATCH 6/7] update existing dict --- homeassistant/components/motioneye/config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index 89be966c67072..f893c3b8de2a5 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -46,9 +46,9 @@ def _get_form( url_schema: dict[vol.Required, type[str]] = {} if not self._hassio_discovery: # Only ask for URL when not discovered - url_schema = { - vol.Required(CONF_URL, default=user_input.get(CONF_URL, "")): str - } + url_schema.update( + {vol.Required(CONF_URL, default=user_input.get(CONF_URL, "")): str} + ) return self.async_show_form( step_id="user", From e89263d3efea64e551be4d79a312e39619a9be2a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 May 2021 16:39:08 +0200 Subject: [PATCH 7/7] Update homeassistant/components/motioneye/config_flow.py Co-authored-by: Martin Hjelmare --- homeassistant/components/motioneye/config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index f893c3b8de2a5..463c804028a57 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -46,9 +46,9 @@ def _get_form( url_schema: dict[vol.Required, type[str]] = {} if not self._hassio_discovery: # Only ask for URL when not discovered - url_schema.update( - {vol.Required(CONF_URL, default=user_input.get(CONF_URL, "")): str} - ) + url_schema[ + vol.Required(CONF_URL, default=user_input.get(CONF_URL, "")) + ] = str return self.async_show_form( step_id="user",