From 19c505c0f06239fb07c1cf9e0c671f496334647a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 May 2021 17:40:07 +0200 Subject: [PATCH] Add Supervisor discovery to motionEye (#50901) Co-authored-by: Martin Hjelmare --- .../components/motioneye/config_flow.py | 41 ++++- .../components/motioneye/strings.json | 4 + .../components/motioneye/translations/en.json | 4 + .../components/motioneye/test_config_flow.py | 144 ++++++++++++++++++ 4 files changed, 189 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index a8562189d1f..463c804028a 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 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): 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 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): 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 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): # 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 = "Add-on" + return self.async_create_entry( - title=f"{user_input[CONF_URL]}", + title=title, data=user_input, ) @@ -134,3 +148,22 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): ) -> 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 d365ba272ea..d89b5cab275 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 dd4f337e9f9..b93e4f66894 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 f193c79f3ef..fbdabdadb41 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", "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"} + 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") == "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( @@ -287,3 +339,95 @@ 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", "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", "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_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