From d7bf783da67659baeb1e2140db3badab4c604242 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 21 Nov 2020 10:21:23 +0000 Subject: [PATCH] Add reauth support for OVO Energy (#38882) --- .../components/ovo_energy/__init__.py | 30 +++++-- .../components/ovo_energy/config_flow.py | 49 ++++++++++- homeassistant/components/ovo_energy/const.py | 2 - homeassistant/components/ovo_energy/sensor.py | 5 +- .../components/ovo_energy/strings.json | 10 ++- .../ovo_energy/translations/en.json | 10 ++- .../components/ovo_energy/test_config_flow.py | 88 +++++++++++++++++++ 7 files changed, 176 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 445ae733ec5..0130ba30c30 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -15,6 +15,7 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, + UpdateFailed, ) from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN @@ -33,23 +34,38 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool client = OVOEnergy() try: - await client.authenticate(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]) + authenticated = await client.authenticate( + entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] + ) except aiohttp.ClientError as exception: _LOGGER.warning(exception) raise ConfigEntryNotReady from exception + if not authenticated: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=entry.data + ) + ) + return False + async def async_update_data() -> OVODailyUsage: """Fetch data from OVO Energy.""" - now = datetime.utcnow() async with async_timeout.timeout(10): try: - await client.authenticate( + authenticated = await client.authenticate( entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] ) - return await client.get_daily_usage(now.strftime("%Y-%m")) except aiohttp.ClientError as exception: - _LOGGER.warning(exception) - return None + raise UpdateFailed(exception) from exception + if not authenticated: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=entry.data + ) + ) + raise UpdateFailed("Not authenticated with OVO Energy") + return await client.get_daily_usage(datetime.utcnow().strftime("%Y-%m")) coordinator = DataUpdateCoordinator( hass, @@ -137,6 +153,6 @@ class OVOEnergyDeviceEntity(OVOEnergyEntity): return { "identifiers": {(DOMAIN, self._client.account_id)}, "manufacturer": "OVO Energy", - "name": self._client.account_id, + "name": self._client.username, "entry_type": "service", } diff --git a/homeassistant/components/ovo_energy/config_flow.py b/homeassistant/components/ovo_energy/config_flow.py index dfedf780592..f395415d89e 100644 --- a/homeassistant/components/ovo_energy/config_flow.py +++ b/homeassistant/components/ovo_energy/config_flow.py @@ -7,8 +7,9 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import CONF_ACCOUNT_ID, DOMAIN # pylint: disable=unused-import +from .const import DOMAIN # pylint: disable=unused-import +REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) USER_SCHEMA = vol.Schema( {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} ) @@ -20,6 +21,10 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + def __init__(self): + """Initialize the flow.""" + self.username = None + async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" errors = {} @@ -37,11 +42,10 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return self.async_create_entry( - title=client.account_id, + title=client.username, data={ CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], - CONF_ACCOUNT_ID: client.account_id, }, ) @@ -50,3 +54,42 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=USER_SCHEMA, errors=errors ) + + async def async_step_reauth(self, user_input): + """Handle configuration by re-auth.""" + errors = {} + + if user_input and user_input.get(CONF_USERNAME): + self.username = user_input[CONF_USERNAME] + + # pylint: disable=no-member + self.context["title_placeholders"] = {CONF_USERNAME: self.username} + + if user_input is not None and user_input.get(CONF_PASSWORD) is not None: + client = OVOEnergy() + try: + authenticated = await client.authenticate( + self.username, user_input[CONF_PASSWORD] + ) + except aiohttp.ClientError: + errors["base"] = "connection_error" + else: + if authenticated: + await self.async_set_unique_id(self.username) + + for entry in self._async_current_entries(): + if entry.unique_id == self.unique_id: + self.hass.config_entries.async_update_entry( + entry, + data={ + CONF_USERNAME: self.username, + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + return self.async_abort(reason="reauth_successful") + + errors["base"] = "authorization_error" + + return self.async_show_form( + step_id="reauth", data_schema=REAUTH_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/ovo_energy/const.py b/homeassistant/components/ovo_energy/const.py index e836bb2ca8a..f691eb9bc49 100644 --- a/homeassistant/components/ovo_energy/const.py +++ b/homeassistant/components/ovo_energy/const.py @@ -3,5 +3,3 @@ DOMAIN = "ovo_energy" DATA_CLIENT = "ovo_client" DATA_COORDINATOR = "coordinator" - -CONF_ACCOUNT_ID = "account_id" diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index 2a64fbe2d22..2f2e1b8dd50 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -50,10 +50,7 @@ async def async_setup_entry( ) ) - async_add_entities( - entities, - True, - ) + async_add_entities(entities, True) class OVOEnergySensor(OVOEnergyDeviceEntity): diff --git a/homeassistant/components/ovo_energy/strings.json b/homeassistant/components/ovo_energy/strings.json index fac7c97bcbe..df19d4898f2 100644 --- a/homeassistant/components/ovo_energy/strings.json +++ b/homeassistant/components/ovo_energy/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "OVO Energy: {username}", "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", @@ -13,7 +14,14 @@ }, "description": "Set up an OVO Energy instance to access your energy usage.", "title": "Add OVO Energy Account" + }, + "reauth": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "description": "Authentication failed for OVO Energy. Please enter your current credentials.", + "title": "Reauthentication" + } } - } } } diff --git a/homeassistant/components/ovo_energy/translations/en.json b/homeassistant/components/ovo_energy/translations/en.json index 160f47ae23f..6a002b6e08a 100644 --- a/homeassistant/components/ovo_energy/translations/en.json +++ b/homeassistant/components/ovo_energy/translations/en.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "OVO Energy: {username}", "error": { "already_configured": "Account is already configured", "cannot_connect": "Failed to connect", @@ -13,7 +14,14 @@ }, "description": "Set up an OVO Energy instance to access your energy usage.", "title": "Add OVO Energy Account" + }, + "reauth": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "description": "Authentication failed for OVO Energy. Please enter your current credentials.", + "title": "Reauthentication" + } } - } } } \ No newline at end of file diff --git a/tests/components/ovo_energy/test_config_flow.py b/tests/components/ovo_energy/test_config_flow.py index 57192933572..a7f6ea9b9f2 100644 --- a/tests/components/ovo_energy/test_config_flow.py +++ b/tests/components/ovo_energy/test_config_flow.py @@ -7,9 +7,13 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from tests.async_mock import patch +from tests.common import MockConfigEntry +FIXTURE_REAUTH_INPUT = {CONF_PASSWORD: "something1"} FIXTURE_USER_INPUT = {CONF_USERNAME: "example@example.com", CONF_PASSWORD: "something"} +UNIQUE_ID = "example@example.com" + async def test_show_form(hass: HomeAssistant) -> None: """Test that the setup form is served.""" @@ -94,3 +98,87 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result2["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] + + +async def test_reauth_authorization_error(hass: HomeAssistant) -> None: + """Test we show user form on authorization error.""" + with patch( + "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate", + return_value=False, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_REAUTH_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "reauth" + assert result2["errors"] == {"base": "authorization_error"} + + +async def test_reauth_connection_error(hass: HomeAssistant) -> None: + """Test we show user form on connection error.""" + with patch( + "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate", + side_effect=aiohttp.ClientError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_REAUTH_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "reauth" + assert result2["errors"] == {"base": "connection_error"} + + +async def test_reauth_flow(hass: HomeAssistant) -> None: + """Test reauth works.""" + with patch( + "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate", + return_value=False, + ): + mock_config = MockConfigEntry( + domain=DOMAIN, unique_id=UNIQUE_ID, data=FIXTURE_USER_INPUT + ) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth" + assert result["errors"] == {"base": "authorization_error"} + + with patch( + "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate", + return_value=True, + ), patch( + "homeassistant.components.ovo_energy.config_flow.OVOEnergy.username", + return_value=FIXTURE_USER_INPUT[CONF_USERNAME], + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_REAUTH_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful"