diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index 1d1c1958420..9db4e834571 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -6,6 +6,7 @@ import logging from typing import cast from pyoctoprintapi import ApiError, OctoprintClient, PrinterOffline +from pyoctoprintapi.exceptions import UnauthorizedException import voluptuous as vol from yarl import URL @@ -24,6 +25,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo @@ -226,6 +228,8 @@ class OctoprintDataUpdateCoordinator(DataUpdateCoordinator): printer = None try: job = await self._octoprint.get_job_info() + except UnauthorizedException as err: + raise ConfigEntryAuthFailed from err except ApiError as err: raise UpdateFailed(err) from err @@ -238,6 +242,8 @@ class OctoprintDataUpdateCoordinator(DataUpdateCoordinator): if not self._printer_offline: _LOGGER.debug("Unable to retrieve printer information: Printer offline") self._printer_offline = True + except UnauthorizedException as err: + raise ConfigEntryAuthFailed from err except ApiError as err: raise UpdateFailed(err) from err else: diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index 1bd54e2214e..c1bdc623291 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -1,5 +1,9 @@ """Config flow for OctoPrint integration.""" +from __future__ import annotations + +from collections.abc import Mapping import logging +from typing import Any from pyoctoprintapi import ApiError, OctoprintClient, OctoprintException import voluptuous as vol @@ -16,6 +20,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -46,6 +51,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 api_key_task = None + _reauth_data = None def __init__(self) -> None: """Handle a config flow for OctoPrint.""" @@ -114,8 +120,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._user_input = user_input return self.async_show_progress_done(next_step_id="user") - async def _finish_config(self, user_input): + async def _finish_config(self, user_input: dict): """Finish the configuration setup.""" + existing_entry = await self.async_set_unique_id(self.unique_id) + if existing_entry is not None: + self.hass.config_entries.async_update_entry(existing_entry, data=user_input) + # Reload the config entry otherwise devices will remain unavailable + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) + + return self.async_abort(reason="reauth_successful") + octoprint = self._get_octoprint_client(user_input) octoprint.set_api_key(user_input[CONF_API_KEY]) @@ -127,6 +143,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(discovery.upnp_uuid, raise_on_progress=False) self._abort_if_unique_id_configured() + return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) async def async_step_auth_failed(self, user_input): @@ -188,6 +205,41 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() + async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: + """Handle reauthorization request from Octoprint.""" + self._reauth_data = dict(config) + + self.context.update( + { + "title_placeholders": {CONF_HOST: config[CONF_HOST]}, + } + ) + + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle reauthorization flow.""" + assert self._reauth_data is not None + + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required( + CONF_USERNAME, default=self._reauth_data[CONF_USERNAME] + ): str, + } + ), + ) + + self.api_key_task = None + self._reauth_data[CONF_USERNAME] = user_input[CONF_USERNAME] + + return await self.async_step_get_api_key(self._reauth_data) + async def _async_get_auth_key(self, user_input: dict): """Get application api key.""" octoprint = self._get_octoprint_client(user_input) diff --git a/homeassistant/components/octoprint/strings.json b/homeassistant/components/octoprint/strings.json index 89e44a6a3a6..23cdf6ce56e 100644 --- a/homeassistant/components/octoprint/strings.json +++ b/homeassistant/components/octoprint/strings.json @@ -11,6 +11,11 @@ "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]", "username": "[%key:common::config_flow::data::username%]" } + }, + "reauth_confirm": { + "data": { + "username": "[%key:common::config_flow::data::username%]" + } } }, "error": { @@ -21,7 +26,8 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "unknown": "[%key:common::config_flow::error::unknown%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "auth_failed": "Failed to retrieve application api key" + "auth_failed": "Failed to retrieve application api key", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "progress": { "get_api_key": "Open the OctoPrint UI and click 'Allow' on the Access Request for 'Home Assistant'." diff --git a/homeassistant/components/octoprint/translations/en.json b/homeassistant/components/octoprint/translations/en.json index e0729b27856..035274d6e9c 100644 --- a/homeassistant/components/octoprint/translations/en.json +++ b/homeassistant/components/octoprint/translations/en.json @@ -4,6 +4,7 @@ "already_configured": "Device is already configured", "auth_failed": "Failed to retrieve application api key", "cannot_connect": "Failed to connect", + "reauth_successful": "Re-authentication was successful", "unknown": "Unexpected error" }, "error": { @@ -24,6 +25,11 @@ "username": "Username", "verify_ssl": "Verify SSL certificate" } + }, + "reauth_confirm": { + "data": { + "username": "Username" + } } } } diff --git a/tests/components/octoprint/test_config_flow.py b/tests/components/octoprint/test_config_flow.py index e9de98206d1..b4e6c5b0666 100644 --- a/tests/components/octoprint/test_config_flow.py +++ b/tests/components/octoprint/test_config_flow.py @@ -533,3 +533,55 @@ async def test_duplicate_ssdp_ignored(hass: HomeAssistant) -> None: ) assert result["type"] == "abort" assert result["reason"] == "already_configured" + + +async def test_reauth_form(hass): + """Test we get the form.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "username": "testuser", + "host": "1.1.1.1", + "name": "Printer", + "port": 81, + "ssl": True, + "path": "/", + }, + unique_id="1234", + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "entry_id": entry.entry_id, + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + }, + data=entry.data, + ) + assert result["type"] == "form" + assert not result["errors"] + + with patch( + "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "testuser", + }, + ) + await hass.async_block_till_done() + assert result["type"] == "progress" + + with patch( + "homeassistant.components.octoprint.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful"