diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 6c2df3ad614..bc14af9ff11 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -174,6 +174,18 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=self.bridge_id, data=self.deconz_config) + async def async_step_reauth(self, config: dict): + """Trigger a reauthentication flow.""" + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = {CONF_HOST: config[CONF_HOST]} + + self.deconz_config = { + CONF_HOST: config[CONF_HOST], + CONF_PORT: config[CONF_PORT], + } + + return await self.async_step_link() + async def async_step_ssdp(self, discovery_info): """Handle a discovered deCONZ bridge.""" if ( diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 881ea883c4c..a6cbb2acef9 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -4,6 +4,7 @@ import asyncio import async_timeout from pydeconz import DeconzSession, errors +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady @@ -19,7 +20,7 @@ from .const import ( DEFAULT_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_NEW_DEVICES, - DOMAIN, + DOMAIN as DECONZ_DOMAIN, LOGGER, NEW_GROUP, NEW_LIGHT, @@ -34,7 +35,7 @@ from .errors import AuthenticationRequired, CannotConnect @callback def get_gateway_from_config_entry(hass, config_entry): """Return gateway with a matching bridge id.""" - return hass.data[DOMAIN][config_entry.unique_id] + return hass.data[DECONZ_DOMAIN][config_entry.unique_id] class DeconzGateway: @@ -152,7 +153,7 @@ class DeconzGateway: # Gateway service device_registry.async_get_or_create( config_entry_id=self.config_entry.entry_id, - identifiers={(DOMAIN, self.api.config.bridgeid)}, + identifiers={(DECONZ_DOMAIN, self.api.config.bridgeid)}, manufacturer="Dresden Elektronik", model=self.api.config.modelid, name=self.api.config.name, @@ -173,8 +174,14 @@ class DeconzGateway: except CannotConnect as err: raise ConfigEntryNotReady from err - except Exception as err: # pylint: disable=broad-except - LOGGER.error("Error connecting with deCONZ gateway: %s", err, exc_info=True) + except AuthenticationRequired: + self.hass.async_create_task( + self.hass.config_entries.flow.async_init( + DECONZ_DOMAIN, + context={"source": SOURCE_REAUTH}, + data=self.config_entry.data, + ) + ) return False for component in SUPPORTED_PLATFORMS: diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 6a8066e98fb..e18418ff9ae 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -15,14 +15,19 @@ from homeassistant.components.deconz.const import ( CONF_ALLOW_DECONZ_GROUPS, CONF_ALLOW_NEW_DEVICES, CONF_MASTER_GATEWAY, - DOMAIN, + DOMAIN as DECONZ_DOMAIN, ) from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, ATTR_UPNP_MANUFACTURER_URL, ATTR_UPNP_SERIAL, ) -from homeassistant.config_entries import SOURCE_HASSIO, SOURCE_SSDP, SOURCE_USER +from homeassistant.config_entries import ( + SOURCE_HASSIO, + SOURCE_REAUTH, + SOURCE_SSDP, + SOURCE_USER, +) from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -47,7 +52,7 @@ async def test_flow_discovered_bridges(hass, aioclient_mock): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DECONZ_DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == RESULT_TYPE_FORM @@ -88,7 +93,7 @@ async def test_flow_manual_configuration_decision(hass, aioclient_mock): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DECONZ_DOMAIN, context={"source": SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( @@ -140,7 +145,7 @@ async def test_flow_manual_configuration(hass, aioclient_mock): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DECONZ_DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == RESULT_TYPE_FORM @@ -184,7 +189,7 @@ async def test_manual_configuration_after_discovery_timeout(hass, aioclient_mock aioclient_mock.get(pydeconz.utils.URL_DISCOVER, exc=asyncio.TimeoutError) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DECONZ_DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == RESULT_TYPE_FORM @@ -197,7 +202,7 @@ async def test_manual_configuration_after_discovery_ResponseError(hass, aioclien aioclient_mock.get(pydeconz.utils.URL_DISCOVER, exc=pydeconz.errors.ResponseError) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DECONZ_DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == RESULT_TYPE_FORM @@ -216,7 +221,7 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DECONZ_DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == RESULT_TYPE_FORM @@ -262,7 +267,7 @@ async def test_manual_configuration_dont_update_configuration(hass, aioclient_mo ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DECONZ_DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == RESULT_TYPE_FORM @@ -305,7 +310,7 @@ async def test_manual_configuration_timeout_get_bridge(hass, aioclient_mock): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DECONZ_DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == RESULT_TYPE_FORM @@ -346,7 +351,7 @@ async def test_link_get_api_key_ResponseError(hass, aioclient_mock): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DECONZ_DOMAIN, context={"source": SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( @@ -367,10 +372,46 @@ async def test_link_get_api_key_ResponseError(hass, aioclient_mock): assert result["errors"] == {"base": "no_key"} +async def test_reauth_flow_update_configuration(hass, aioclient_mock): + """Verify reauth flow can update gateway API key.""" + config_entry = await setup_deconz_integration(hass) + + result = await hass.config_entries.flow.async_init( + DECONZ_DOMAIN, + data=config_entry.data, + context={"source": SOURCE_REAUTH}, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "link" + + new_api_key = "new_key" + + aioclient_mock.post( + "http://1.2.3.4:80/api", + json=[{"success": {"username": new_api_key}}], + headers={"content-type": CONTENT_TYPE_JSON}, + ) + + aioclient_mock.get( + f"http://1.2.3.4:80/api/{new_api_key}/config", + json={"bridgeid": BRIDGEID}, + headers={"content-type": CONTENT_TYPE_JSON}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert config_entry.data[CONF_API_KEY] == new_api_key + + async def test_flow_ssdp_discovery(hass, aioclient_mock): """Test that config flow for one discovered bridge works.""" result = await hass.config_entries.flow.async_init( - DOMAIN, + DECONZ_DOMAIN, data={ ATTR_SSDP_LOCATION: "http://1.2.3.4:80/", ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, @@ -410,7 +451,7 @@ async def test_ssdp_discovery_update_configuration(hass): return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( - DOMAIN, + DECONZ_DOMAIN, data={ ATTR_SSDP_LOCATION: "http://2.3.4.5:80/", ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, @@ -431,7 +472,7 @@ async def test_ssdp_discovery_dont_update_configuration(hass): config_entry = await setup_deconz_integration(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, + DECONZ_DOMAIN, data={ ATTR_SSDP_LOCATION: "http://1.2.3.4:80/", ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, @@ -450,7 +491,7 @@ async def test_ssdp_discovery_dont_update_existing_hassio_configuration(hass): config_entry = await setup_deconz_integration(hass, source=SOURCE_HASSIO) result = await hass.config_entries.flow.async_init( - DOMAIN, + DECONZ_DOMAIN, data={ ATTR_SSDP_LOCATION: "http://1.2.3.4:80/", ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, @@ -467,7 +508,7 @@ async def test_ssdp_discovery_dont_update_existing_hassio_configuration(hass): async def test_flow_hassio_discovery(hass): """Test hassio discovery flow works.""" result = await hass.config_entries.flow.async_init( - DOMAIN, + DECONZ_DOMAIN, data={ "addon": "Mock Addon", CONF_HOST: "mock-deconz", @@ -511,7 +552,7 @@ async def test_hassio_discovery_update_configuration(hass): return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( - DOMAIN, + DECONZ_DOMAIN, data={ CONF_HOST: "2.3.4.5", CONF_PORT: 8080, @@ -535,7 +576,7 @@ async def test_hassio_discovery_dont_update_configuration(hass): await setup_deconz_integration(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, + DECONZ_DOMAIN, data={ CONF_HOST: "1.2.3.4", CONF_PORT: 80, diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 023d4d32da5..1790b6ed6e1 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -181,6 +181,18 @@ async def test_update_address(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_gateway_trigger_reauth_flow(hass): + """Failed authentication trigger a reauthentication flow.""" + with patch( + "homeassistant.components.deconz.gateway.get_gateway", + side_effect=AuthenticationRequired, + ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: + await setup_deconz_integration(hass) + mock_flow_init.assert_called_once() + + assert hass.data[DECONZ_DOMAIN] == {} + + async def test_reset_after_successful_setup(hass): """Make sure that connection status triggers a dispatcher send.""" config_entry = await setup_deconz_integration(hass)